目录

  • 前情引入
    • 简单介绍
    • 预备知识
  • 代码及详解
    • 简单代码
    • 基本解释
      • 生产者线程类
      • 消费者线程类
      • 测试类
      • 执行流程
      • 控制台输出
    • 自我提高
      • 问题一
      • 问题二
    • 升级代码
  • 总结

前情引入

做一些简单的认识和告知一些前置知识

简单介绍

生产者和消费者是一种特殊的业务需求的抽象,这种业务就是:需求和供给达到平衡关系,生产一个,就消费一个,或者是生产一部分,就消费一部分。

利用多线程,可以对这种业务需求进行简单的模拟和实现,主要是利用Object中的wait方法和notify方法。

注意,不能同时生产和消费,因为在多线程下,对共享的数据进行了修改,必须使用同步机制,不然会出现数据安全问题。

预备知识

首先对java中的多线程,有一定的认识。

再者呢,就是Object中的wait方法和notify方法的作用。

  • void wait():在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。 简单来说,就是让当前线程进入阻塞,直到被唤醒,并且会释放调用当前线程占用的对象锁
  • void notify() :唤醒在此对象监视器上等待的单个线程。 如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。简单来说,就是唤醒被wait方法进入阻塞的线程。
  • 注意:这两个方法都只能在同步代码块(包括同步方法)中使用
  • 顺带提一点,sleep虽然也能使当前线程进入阻塞状态,但是是不会释放锁和资源的

代码及详解

先上代码,再基本详解,最后再提高。

简单代码

import java.util.ArrayList;//使用wait和notify实现生产者和消费者模式
public class PCMode1
{public static void main(String[] args){ArrayList<Object> arrayList = new ArrayList<>();new Thread(new Producer1(arrayList), "Producer").start();new Thread(new Consumer1(arrayList), "Consumer").start();}
}@SuppressWarnings("all")
class Producer1 implements Runnable
{private final ArrayList arrayList;public Producer1(ArrayList arrayList){this.arrayList = arrayList;}@Overridepublic void run(){while (true){synchronized (arrayList){System.out.println(Thread.currentThread().getName()+"抢到了对象锁");if (arrayList.size() > 0){System.out.println("已有食物,请消费者消费!");System.out.println();try{ arrayList.wait(); } catch (InterruptedException e){ e.printStackTrace(); }} else{arrayList.add(0,"a");try{ Thread.sleep(100); } catch (InterruptedException e){ e.printStackTrace(); }System.out.println("已生产食物,请消费者消费!");arrayList.notify();}}}}
}@SuppressWarnings("all")
class Consumer1 implements Runnable
{private final ArrayList arrayList;public Consumer1(ArrayList arrayList){ this.arrayList = arrayList; }@Overridepublic void run(){while (true){synchronized (arrayList){System.out.println(Thread.currentThread().getName()+"抢到了对象锁");if (arrayList.size()>0){arrayList.remove(0);try{ Thread.sleep(100); } catch (InterruptedException e){ e.printStackTrace(); }System.out.println("消费者消费了一个食物,请生产者生产!");arrayList.notify();} else{System.out.println("没有食物,请生产者生产!!");System.out.println();try{ arrayList.wait(); }catch (InterruptedException e){ e.printStackTrace(); }}}}}
}

@SuppressWarnings(“all”) 这个注解是为了去掉代码中那些难看的提示,可以直接忽略。

能看懂的话,那就是大佬咯,大佬可以看看后面的提高。看不懂也没关系,我们来一步一步的分析。

基本解释

我把三个类写在一个java文件中的,一个是测试类,另外两个,分别是生产者线程类和消费者线程类。测试类就是公共类,其中有main方法,用来测试用的,没啥好说的,主要是两个线程类。

先说一下大概的思路,两个线程类里都一个ArrayList类型的变量,这两个线程都是共享的同一个变量,这就是那个共享的数据。

简单起见,我模拟的是生产一个,消费一个的情况。我将这个ArrayList作为一个容器,生产者每生产一个食物,就放进这个容器,然后等消费者来消费,消费一个之后,生产者又进行生产,如此往复……

生产者线程类

Producer1是生产者线程,之前说了,有一个ArrayList类型的成员变量,构造方法是为了给这个变量赋值。

重写线程任务的run方法,先来一个while(true)死循环,意思就是,生产者线程一直生产。然后是synchronized同步代码块,因为是对共享的数据进行修改操作,所以要使用同步机制,来保证数据的安全,synchronized的锁对象就是共享的对象:arrayList。synchronized的对象选取原则就是:想要那些线程排队执行,就选择一个这些线程共享的对象

进入synchronized代码块中做的第一件事情,我先打印了一句话,方便后面观察控制台的输出情况,然后是正事。

我们要进行生产,首先第一件要做的事情是什么?当然是判断arrayList这个容器里是不是已经有食物了。如果已经有食物了,咱就不能生产,得让消费者来消费是吧。如果没有,咱们才能生产食物,并且添加到容器。

34行到42行,就是容器中存在食物的逻辑,先打印提示语句,然后用arrayList这个对象调用wait方法。还记得这个方法的作用吗?会让当前线程进入阻塞,并且会释放占用的对象锁。释放了对象锁,那么消费者线程就可能拿到对象锁,然后进行消费。当然,我这里只说了一个大概,具体细节后面再分析。

43行到53行,就是容器中不存在食物,我们生产食物并添加进容器的逻辑。就是往arrayList中添加一个元素,然后模拟一下生产的消耗时间(主要是为控制台输出可控,不然控制台飞一般的跑),再打印提示信息,最后再调用arrayList的notify方法。还记得这个方法的作用吗?唤醒在此对象监视器上等待的单个线程。那此时谁在arrayList对象上等待呢?我们当前生产者线程在执行,那么肯定就是消费者线程进入了阻塞状态撒。而调用这个方法,就可以唤醒消费者线程,进行消费。

消费者线程类

消费者线程类的处理逻辑和生产者线程非常类似,只是处理的业务不同,一个是进行生产的,一个是进行消费的,我们来简单的过一遍。

一样的代码就不说了,一样的意思。
76行到85行,是容器中存在食物,进行消费的逻辑,先将容器中这个食物移除,然后在模拟一下耗时,最后调用arrayList的notify方法,因为我们已经消费了容器里的食物,现在要通知在等待中的生产者生产了。

87行到94行,是容器中不存食物的处理逻辑,很简单,打印控制信息,然后调用arrayList对象的wait方法,让当线程进入等待状态,等待生产者生产。

测试类

测试类里,就是创建了两个线程对象,然后将生产者线程和消费线程传了进去,然后启动。其实就算这样挨着挨着分析了代码,可能能还是会不清楚,我觉得最好的办法是:自己来跟着代码走一遍流程,我自己屡试不爽,我们一起来走走吧

执行流程
  1. 程序的执行从main函数开始,我先new了一个生产者线程并且启动,所以是生产者先抢到了arrayList的对象锁,然后开始生产食物……不对,虽然生产者线程先启动,但是如果在生产者线程还没有进入同步代码块,也就是还没有拿到arrayList的对象锁时,有没有可能消费者线程就启动了,并且拿到了CPU的执行权,先拿到了arrayList的对象锁呢?其实是完全有可能的,因为在没有进入同步代码块的时候,两个线程是共同抢夺CPU的执行权的,先启动的,不一定就能占到便宜。

  2. 那怎么办呢?我生产者还没生产呢,消费者就来消费了。别急,假设消费者先拿到了arrayList的对象锁,我们就随着消费者线程的逻辑往下走,进入了消费者线程,经过条件判断,直接就进入了else分支里,因为此时容器里是没有食物的,然后它干了一件什么事情?原地wait,直接就原地阻塞,而且还将拿到的对象锁给释放了。那生产者呢?开始arrayList的对象锁被消费者给拿走了,它肯定就一直在自己线程里的synchronized代码块外面等待,拿不到对象锁,它只能在synchronized代码块外面等待。它肯定在想,是哪个个天杀的,抢了我的对象锁,害我一直在这里等。消费者一旦释放了锁,而且此时消费者本身进入了阻塞状态,不会和生产者去抢arrayList的对象锁,所以一定是生产者拿到对象锁,然后进入synchronized代码块生产食物。

  3. 生产者线程拿到对象锁之后,就美滋滋的去生产食物去了,那是它存在的唯一使命。它还是很严谨的,先判断容器中是否已经存在食物,呀,没有,就进入了else分支了,先生产了一个食物,将其存入了容器中,然后再用notify方法唤醒了正在arrayList对象上等待的线程,然后自己再退出了同步代码块,释放了对象锁。(退出synchronized代码块,也是会释放锁的!

  4. 消费者被唤醒了,而且生产者释放了arrayList的对象锁,消费者终于可拿到对象锁,并且大吃特吃了。但是作为程序出生的吃货,虽然爱吃,但是也是严谨的。它也是先判断容器中是否有食物,没有的话,那还瞎费什么劲呢,赶紧睡一觉(wait方法),让生产者那家伙生产。但是这次它运气不错,容器里是有食物的,然后它饱餐了一顿,然后再用同样的方法(notify)唤醒正在arrayList对象上等待的线程,自己退出synchronized代码块的时候,将锁给释放了。

  5. 生产者又拿到对象锁了,……如此往复

当然,我这里只说了大致的流程,有疑惑的小伙伴可以多分析几遍。还有一些细节,我们在提高中分析。

控制台输出

为了看到效果,在运行的时候,我特意将消费者线程的创建放在前面的,让它先抢到对象锁的概率大一些。

自我提高

问题一
  1. 生产者/消费者释放了锁之后,可以再次拿到锁吗?再次拿到,会有什么影响吗?再次拿到的概率如何?为什么?

我的意思就是,在上面执行流程中第三步中,生产者最后将对象锁释放了,而且自己退出了synchronized代码块。但是别忘了,虽然生产者线程退出了同步代码块,但它还是一个正常执行的线程,并没有进入阻塞状态,它还是会和消费者抢锁的。

那会不会有问题?生产了之后生产者又抢到了锁。其实不会,就和我们在在执行流程第二步中的分析类似,即便生产者再次抢到了锁,但是此时容器中已经有食物了,它抢到了也会调用wait方法进入阻塞状态。生产者它此时的内心活动一定是:我靠,消费者那家伙还没吃?动作真慢,那我再睡会吧。然后消费者就能拿到对象锁,进行消费了。消费者再次抢到锁,情况也是类似,消费者前一次已经将食物消费了,再次抢到锁,发容器中已经没有食物了,就会调用arrayList对象wait方法,进入阻塞,释放锁。

其实上面程序的输出结果,也佐证了这一点。生产者总是在生产之后再次在控制台打印“已有食物,请消费者消费!”,消费者总是在消费之后,再次在控制台打印“没有食物,请生产者生产!!”,这其实就是因为再次抢到了锁,被wait方法进入阻塞之前打印的信息。

但是如果仔细分析,就会发现有问题。为什么每次都会再次抢到锁?每次都是,生产者先生产了一个食物,然后它再次抢到锁,再打印已有食物的提示信息。或者是,消费者先消费了一个食物,然后再次抢到了锁,并且打印没有食物的提示信息。这是为什么呢?按道理来说,他们俩都是有机会抢到锁的,为什么总是一个抢到两次,直到它自己被wait方法阻塞了,另一个线程才有执行的机会?如果你多刷几遍,可能会发现,偶尔一次,另一个还是后可能抢到锁的,只不过几率很小很小,以至于我一开始都怀疑我的代码有问题。

特意找到了这种情况,截图如下:(代码是上面的代码,没有动过哦)

要解释这个问题,就需要仔细分析wait和notify方法的执行时机了,也就是第二个问题,往下面看。

问题二
  1. 生产者/消费者被唤醒了之后,是马上就执行的吗?

我们思考一个场景:假设容器里现在是有食物的,生产者在31行阻塞,消费者拿到锁在执行。当消费者消费了食物之后,它就调用了arrayList对象的notify方法,之前被wait方法阻塞的生产者线程,此时就被唤醒了。那么问题就来了:消费者线程在执行,生产者线程也被唤醒了,就有两个线程同时在使用了共享对象的synchronized代码块里面

那么,synchronized还有用吗?还能保证共享数据的安全吗?java的设计者肯定不会允许这样的事情发生。我做了简单的测试,得出了结论:即便唤醒了arrayList对象上等待的线程,但是被唤醒的线程并不会第一时间执行,而是等待当前线程执行完毕,被唤醒的那个线程才可以继续执行。应该是,即便等待的线程被唤醒了,但是锁时被当前的线程占用着的,被唤醒的那个线程拿不到锁,所以无法执行。

也就是说,即便生产者将消费者唤醒了,但是由于arrayList的对象锁是在生产者手上的,所以消费者不能第一时间执行,必须等生产者自动退出了synchronized代码块,将锁给释放了,被唤醒的那个线程才能继续从上次等待的那个位置继续执行。所以,为什么两个线程连续抢到的概率为什么那么大的疑问,也能解释了。

因为当前线程将另外一个线程唤醒了之后,当前线程会继续执行,直到退出了synchronized代码块,退出了synchronized代码块,那么被唤醒的那个线程就会接着上次等待的地方继续执行,但是别人在执行的时候,当前这个线程也没闲着呀,他自己也会自己继续执行,直到再次遇到了synchronized关键字,此时arrayList的对象锁在被唤醒的那个线程手上,当前线程就只能卡在synchronized关键字这里。但是被唤醒那个线程马上就会退出synchronized代码块,一旦退出,由于当前线程之前就已经卡synchronized关键字这里了,所以当前线程马上就能获取到arrayList的对象锁,直到当前线程被wait方法给阻塞了,才没有能力去抢那个锁了。

那为什么有会出现,不是一个线程连续两次抢到锁,别的线程也能在中间抢到锁呢?这是因为java中的线程调度用的是抢占式,这个东西就和玄学一样,谁能抢到,不能确定。所以可能被唤醒的那个线程抢夺能力很强,从他被唤醒并且当前线程释放了对象锁之后,他一直在占用CPU的执行权,没有给当前线程留时间,被唤醒的那个线程就抢到了锁,但是这种几率很小,除非是刻意的去制造,增大概率。

升级代码

import java.util.Random;@SuppressWarnings("all")
public class PCMode
{public static void main(String[] args){Bun[] buns = new Bun[4];new Thread(new Consumer(buns),"consumer").start();new Thread(new Producer(buns),"producer").start();}
}@SuppressWarnings("all")
class Producer implements Runnable
{private Bun [] buns;private Random random = new Random();private String[] skins = {"冰皮儿","薄皮儿","厚皮儿"};private String[] stuffings = {"牛肉馅儿","大葱馅儿","韭菜馅儿","酱肉馅儿"};public Producer() { }public Producer(Bun[] buns){ this.buns = buns; }// 生产者生产包子@Overridepublic void run(){while (true)//死循环,一直生产{synchronized (buns)//涉及到了共享的成员变量,而且要对其修改,所以要用同步机制{System.out.println(Thread.currentThread().getName()+"抢到了对象锁");boolean isFull = true;//用来标记包子库是否已满for (int i = 0; i < buns.length;i++ )//尝试生产包子并将包子存入包子库{//如果有空位就可以生产包子并且存入if (buns[i] == null){isFull = false;//生产一个包子,随机馅儿和皮Bun bun = new Bun(skins[random.nextInt(skins.length)],stuffings[random.nextInt(stuffings.length)]);buns[i] = bun;//将该空位置上加上包子//模拟产生包子的耗时int time = random.nextInt(5);try{Thread.sleep(time*100);System.out.println("生产者生产一个“"+bun.toString()+"”,耗时:"+time+"百毫秒");//每生产一个包子,都唤醒包子库对象上所有的等待线程。分析和下面消费者的类似buns.notify();} catch (InterruptedException e){ e.printStackTrace(); }}}/*这里的分析和消费者那里类似,如果包子库已经满了,就自己进入等待状态,并且释放包子库对象上的锁。让消费者来执行*/if (isFull){try{System.out.println("包子铺满了,吃货快来吃!");System.out.println();buns.wait();//自己进入等待状态}catch (InterruptedException e){ e.printStackTrace(); }}}}}
}//生产者线程
@SuppressWarnings("all")
class Consumer implements Runnable
{private Bun [] buns; //包子库private Random random = new Random();public Consumer() { }public Consumer(Bun[] buns){ this.buns = buns; }//尝试消费包子@Overridepublic void run(){Bun bun = null;while (true) //死循环,一直消费{synchronized (buns)//涉及到了共享的成员变量,而且要对其修改,所以要用同步机制{System.out.println(Thread.currentThread().getName()+"抢到了对象锁");boolean isEmpty = true;//用来标记包子库是否为空for (int i = 0; i < buns.length; i++)//遍历包子库,消费包子{if (buns[i] != null)//不为null,就说明有包子,进行消费{isEmpty = false;//将标记标为falsebun = buns[i];//后面要用到这个包子对象buns[i] = null;//包子消费后,将其置为null//模拟消耗包子的耗时int time = random.nextInt(5);try{Thread.sleep(time * 100);System.out.println("消费者消费了一个“" + bun.toString() + "”,耗时:" + time + "百毫秒");}catch (InterruptedException e){ e.printStackTrace(); }/*每次消费一个包子后,都唤醒在仓库对象上等待的线程,但是由于对象锁还在当前线程,所以其他线程是不会执行的。当当前先线程跳出了synchronized代码块时,就将对象锁释放了,而生产者线程也是被唤醒了的,所以有可能也会抢到对象锁,但是抢到了也没事,因为条件判断又会使其进入等待,并且释放锁。但是不知道为什么,这种可能性很小很小,我刻意找了很久,只找到了一次*/buns.notify();}}/*如果一个包子都没有,自己进入等待状态。由于每次消费一个包子后,都唤醒了包子库对象上的所有线程,所以生产者线程早就在等待了。一旦当前线程(消费者线程)使用wait方法,释放了锁,并且自己进入了等待状态,立马就被生产者线程抢到。*/if (isEmpty){try{System.out.println("老板没包子了,快做包子!");System.out.println();buns.wait();}catch (Exception e){ e.printStackTrace(); }}}}}
}//包子类
@SuppressWarnings("all")
class Bun
{private String skin;//包子皮儿private String stuffing;//包子馅儿public Bun() { }public Bun(String skin, String stuffing){this.skin = skin;this.stuffing = stuffing;}public String getSkin(){ return skin; }public void setSkin(String skin){ this.skin = skin; }public String getStuffing(){ return stuffing; }public void setStuffing(String stuffing){ this.stuffing = stuffing; }@Overridepublic String toString(){ return skin+stuffing+"包子"; }
}

升级代码中,增加了仓库的容量,并且生产的时候,变成了随机生产的,模拟时间消耗也变成了随机性的。但其实核心的逻辑和最开始的是一样的,我这里就不分析了,大家有兴趣的可以去分析一下,我代码中写了很多注释,方便大家分析。

运行结果

在这个程序中,想去找那种特殊情况,就很难找了。

总结

模拟生产者和消费者,要记住几个要点

  1. while(true)死循环,因为生产者要一直生产,消费者要一直消费。

  2. synchronized,同步代码块,因为要对共享的数据进行修改,必须使用同步机制,而且wait和notify只能在同步代码块中使用。注意要用两个线程共享的对象,不用arrayList,用其他两个线程共享的对象也是可以的。比如用所有线程都共享的字符串,如果用字符串,那wait和notify方法也需要用那个字符串对象去调用。

  3. 还有一个很重要,但是不容易把握的要点,我详细说一下:就是wait方法和notify方法该在什么时候调用。比如说,我是生产者线程,根据容器里是否有食物,有两种状态,有或者没有。如果已经有,我该用wait方法阻塞当前线程,释放资源?还是该用notify方法唤醒在等待的线程呢?
    如果仅仅从字面上分析:如果已经有食物了,那我就自己就进入阻塞,释放锁对象。或者如果已经有食物了,我就唤醒在等待的线程(消费者线程)。两者好像都说得通。那如果没有呢,我生产一个食物存入容器之后,又该用那个方法呢?好像也都能说得通。
    针对这种情况,我发现了一个诀窍:要保证两次被同一个线程抢到了对象锁时,这个线程要进入阻塞状态,。我们再来分析上面的疑惑,如果是生产者,在容器中有没有食物的情况下,我调用了wait方法,将当前线程进入了阻塞状态。那么如果生产者两次抢到了对象锁,第二次进入的时候,容器里已经有食物了,因为第一次会生产并存入。那么第二次就不会进入“容器中没有食物”的那个选项,也就不能进入阻塞。所以我们选择在有食物的情况下,使用wait方法,进入阻塞状态。然后在另一种情况下,选择用notify方法唤醒等待的线程。消费者也可以用类似的方法判断。这种方法是可取的,但是不知道是否有其它更好的办法。

另外还有一点就是:当某个线程被另一个线程唤醒了,此时两个线程会在由同一个对象作锁的两个synchronized代码块里面,但是这两个线程并不会同时执行(同时执行的话,就可能会出现线程安全问题,那么synchronized也没有意义了),情况是:被唤醒的那个线程并不会第一时间执行,而是主动唤醒别的线程的那个线程先执行,直到释放了锁,被唤醒的那个线程才会接着上次休眠的地方继续执行。

有什么不对或不懂的地方,欢迎一起讨论

java多线程之——生产者和消费者(详解及提高)相关推荐

  1. java多线程中的join方法详解

    java多线程中的join方法详解 方法Join是干啥用的? 简单回答,同步,如何同步? 怎么实现的? 下面将逐个回答. 自从接触Java多线程,一直对Join理解不了.JDK是这样说的:join p ...

  2. Java多线程技术~生产者和消费者问题

    Java多线程技术~生产者和消费者问题 本文是上一篇文章的后续,详情点击该连接 线程通信 应用场景:生产者和消费者问题 假设仓库中只能存放一件产品,生产者将生产出来的产品放入仓库,消费者将仓库中产品取 ...

  3. Java多线程系列(六):深入详解Synchronized同步锁的底层实现

    谈到多线程就不得不谈到Synchronized,很多同学只会使用,缺不是很明白整个Synchronized的底层实现原理,这也是面试经常被问到的环节,比如: synchronized的底层实现原理 s ...

  4. Java多线程案例--生产者和消费者模型(送奶人和喝奶人的故事!)

    文章目录 一.进程和线程 1.进程 2.线程 3.进程与线程的区别 二.生产者和消费者模型 1.生产者消费者模式概述 2.奶箱类 3.生产者类 4.消费者类 三.测试 1.测试类(BoxDemo) 2 ...

  5. Kafka生产者与消费者详解

    什么是 Kafka Kafka 是由 Linkedin 公司开发的,它是一个分布式的,支持多分区.多副本,基于 Zookeeper 的分布式消息流平台,它同时也是一款开源的基于发布订阅模式的消息引擎系 ...

  6. Java多线程读写锁ReentrantReadWriteLock原理详解

    ReentrantLock属于排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个线程访问,但是在写线程访问时,所有的读和其他写线程都被阻塞.读写锁维护了一对锁,一个读锁和一 ...

  7. java多线程之生产者和消费者问题

    线程通信:不同的线程执行不同的任务,如果这些任务有某种关系,线程之间必须能够通信,协调完成工作. 经典的生产者和消费者案例(Producer/Consumer): 分析案例: 1):生产者和消费者应该 ...

  8. Java多线程系列之“JUC集合“详解

    Java集合包 在"Java 集合系列01之 总体框架"中,介绍java集合的架构.主体内容包括Collection集合和Map类:而Collection集合又可以划分为List( ...

  9. Java 多线程断点下载文件_详解

    本文转载于:http://blog.csdn.net/ibm_hoojo/article/details/6838222 基本原理:利用URLConnection获取要下载文件的长度.头部等相关信息, ...

最新文章

  1. linux部分基础命令总结,Linux 基础命令总结3
  2. 基于PYQT编写一个人脸识别软件(2)
  3. centos7 yum安装ifconfig
  4. BZOJ 3083: 遥远的国度(树链剖分+DFS序)
  5. selenium svg标签定位元素
  6. 95-190-040-源码-window-Session Window
  7. 《node.js开发指南》读后感
  8. cakephp2.X教程第一部分(基于cakephp1.3.4在线教程的改编)
  9. 新中大计算机知识,新中大财务软件操作步骤
  10. keil5破解失败【经验分享】
  11. 给自己做个文件的保险箱
  12. 设计一个自然数类,该类的对象能表示一个自然数
  13. 在 Word 中如何画底线、直线、虚线?
  14. windows驱动开发推荐书籍
  15. DHCP:(5)华为防火墙USG上部署DHCP服务以及DHCP中继
  16. 比尔盖茨在哈佛大学的演讲(中英版)
  17. C++最佳实践 | 5. 可移植性及多线程
  18. pika详解 (一)
  19. 福利图网站的正确使用姿势
  20. html实现点击部分页面跳转,打造营销服务闭环,在基木鱼如何利用美洽高效赋能客户...

热门文章

  1. 想法也疯狂--创造一门语言
  2. CNI failed to retrieve network namespace path
  3. 白皮书显示,近半企业用过派遣外包员工,三成企业用过退休返聘员工
  4. 【3D建模制作技巧分享】使用Maya与ZBrush制作CG人像
  5. 卖货文案一:激发行动欲望
  6. centos7基本使用教程
  7. 人工智能入门与实战-前言
  8. 保姆级人工智能入门攻略,谁都能玩的AI算法!
  9. flutter 数据持久化之sqflite
  10. *.sln和*.sdf及*.ipch