多线程系列(二)线程间通信
前言
上一篇文章我们叙述了少量多线程的基础,想要深入理解线程只掌握这些只是是远远不够的,这篇文章我将为大家带来多线程中略微高级一点的内容:线程间通信,以及线程间通信所带来的安全问题。
1 概述
什么是线程间通信呢?我们来看一下线程间通信比较官方的解释:
多个线程解决同一资源,但是任务不同
什么意思呢?其实说的直白一点就是,多个线程都有自己的run()方法但是它们解决的资源的确同一个,这样即可以实现线程间相互的通信。
2 单生产单消费
不是说讲线程间通信吗?你怎样又是生产者又是消费者的?这……别急别急,等我讲完你就明白了(゜-゜)。
什么是单生产单消费呢?给大家举一个例子,一个人负责做包子,一个人负责吃包子,对,就是这么简单。下面我结合代码为大家讲解
先定义一个形容资源的类:
class Resource{ private int id;//一辆车对应一个id private String name;//车名 private int whellNumber;//车的轮子数 private boolean flag = false;//标记 public void setId(int id){ this.id = id; } public void setName(String name){ this.name = name; } public void setWhell(int whellNumber){ this.whellNumber = whellNumber; } public void setFlag(boolean flag){ this.flag = flag; } public int getId(){ return id; } public String getName(){ return name; } public int getWhellNumber(){ return whellNumber; } public boolean isFlag(){ return flag; } }
生产者任务
//生产者任务class Input implements Runnable{ private Resource r; public Input(Resource r){ this.r = r; } public void run() { //无限生产车辆 for(int i =0;;i++){ if(i%2==0){ r.setId(i);//设置车的id r.setName(i+"----帕萨特");//设置车的型号 r.setWhell(4);//设置车的轮子数 }else{ r.setId(i);//设置车的id r.setName(i+"----爱玛电动车");//设置车的型 r.setWhell(2);//设置车的轮子数 } } } }
消费者任务
//消费者任务 class Output implements Runnable{ private Resource r; public Output(Resource r){ this.r = r; } public void run() { //无限消费车辆 for(;;){ System.out.println("name="+r.getName()+"---whellNumber="+r.getWhellNumber() +"---id="+r.getId()); } } }
Resource就是资源,Inout和Output就是两个任务,我们让Inout和Output操作同一个Resource对象,这样即可以实现单生产单消费,前面我们也说过了,线程间通信就是多个任务,解决同一资源,所以单生产单消费就是线程间通信的一种。
创立两个线程分别将Input和Output传入并开启
public class ThreadDemo { private Resource r; public static void main(String[] args) { // TODO Auto-generated method stub Resource r = new Resource(); Input in = new Input(r); Output out = new Output(r); Thread t1 = new Thread(in); Thread t2 = new Thread(out); t1.start();//开启生产者线程 t2.start();//开启消费者线程 }}
我们来看一下执行结果
name=爱玛电动车---whellNumber=4---id=24name=爱玛电动车---whellNumber=2---id=25name=帕萨特---whellNumber=4---id=26name=爱玛电动车---whellNumber=2---id=27name=帕萨特---whellNumber=4---id=28name=帕萨特---whellNumber=2---id=29
什么?我的帕萨特怎样变成俩轮了….哎呦,我的爱玛电动车变成四个轮啦,哈哈,皮一下很开心(゜-゜)。我们从打印结果可以看出,这是出现了线程安全问题,下面我来给大家一步一步的分析
- 生产者线程得到CPU执行权,将name和whellNumber分别设置为帕萨特和4,CPU切换到了消费者线程。
- 消费者线程得到CPU执行权,此时name和whellNumber别为帕萨特和4,随后打印name=帕萨特—whellNumber=4,CPU切换到了生产者线程。
- 生产者线程再次得到CPU执行权,将name设置为爱玛电动车(还未对whellNumber进行设置),此时name和whellNumber分别为爱玛电动车和4,CPU切换到了消费者线程。
- 消费者线程得到CPU执行权,此时name和whellNumber别为帕萨特和2,随后打印name=帕萨特—whellNumber=2。
既然找到了问题的根源,那么我们能不能试着去处理呢?一定能处理了,要不然我还讲个毛线(゜-゜)。上一篇文章我们是不是讲了同步synchronized这个关键字,没错就用它来处理,我们来看加上同步之后的代码:
生产者
//生产者任务 class Input implements Runnable{ private Resource r; public Input(Resource r){ this.r = r; } public void run() { //无限生产车辆 for(int i =0;;i++){ synchronized(r){ if(i%2==0){ r.setId(i);//设置车的id r.setName(i+"----帕萨特");//设置车的型号 r.setWhell(4);//设置车的轮子数 }else{ r.setId(i);//设置车的id r.setName(i+"----爱玛电动车");//设置车的型号 r.setWhell(2);//设置车的轮子数 } i++; } //加上sleep是为了让打印结果更显著 try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
消费者
//消费者任务 class Output implements Runnable{ private Resource r; public Output(Resource r){ this.r = r; } public void run() { //无限消费车辆 for(;;){ synchronized(r){ System.out.println("name="+r.getName()+"---whellNumber="+r.getWhellNumber() +"---id="+r.getId()); } //加上sleep是为了让打印结果更显著 try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
将生产者任务和消费者任务分别加上同步,好了,我们再来看一下运行结果
name爱玛电动车---whellNumber=2---id=633name爱玛电动车---whellNumber=2---id=635name爱玛电动车---whellNumber=2---id=635name爱玛电动车---whellNumber=2---id=637name爱玛电动车---whellNumber=2---id=637name帕萨特---whellNumber=4---id=638name爱玛电动车---whellNumber=2---id=639
轮子正常了,这里需要注意一下,生产者和消费者的锁,也就是synchronized中传入的对象,必需是同一个,只有持有同一个锁才能保证生产者在未执行完同步任务时消费者不能进入同步任务、消费者在未执行完同步任务时生产者不能进入同步任务。
尽管轮子正常了,但细心的同学可能发现,id为634的车辆未出现并且id为635的车辆被打印了两次,也就是说生产了一辆汽车卖给了两个用户,emmmm,还可以这样玩?哈哈哈,线程问题仍旧存在,废话不多说接着分析
- 生产者线程得到执行权,当前编号为633,生产者开始生产,尽管加了同步但循环可能执行屡次,我们假定执行了两次,此时编号为635,CPU切换到消费者线程
- 消费者线程得到执行权,此时编号为635,消费者开始消费,假定循环执行了两次,那么就会打印两次id为635的车辆
问题出现了,怎样办呢?能咋办啊……接着处理呗。我们在创立Resource的时候定义了一个布尔类型变量flag,我们就用这个flag处理这个问题,来看代码:
生产者代码
//生产者任务 class Input implements Runnable{ private Resource r; public Input(Resource r){ this.r = r; } public void run() { //无限生产车辆 for(int i =0;;i++){ synchronized(r){ //flag为true的时候代表已经生产过,此时将当前线程wait,等待消费者消费 if(r.isFlag()){ try { r.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(i%2==0){ r.setId(i);//设置车的id r.setName("帕萨特");//设置车的型号 r.setWhell(4);//设置车的轮子数 }else{ r.setId(i);//设置车的id r.setName("爱玛电动车");//设置车的型号 r.setWhell(2);//设置车的轮子数 } r.setFlag(true); //将线程池中的线程唤醒 r.notify(); } //加上sleep是为了让打印结果更显著 try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
消费者代码
//消费者任务 class Output implements Runnable{ private Resource r; public Output(Resource r){ this.r = r; } public void run() { //无限消费车辆 for(;;){ synchronized(r){ //flag为false,代表当前生产的汽车已经被消费掉, //进入wait状态等待生产者生产 if(!r.isFlag()){ try { r.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("name="+r.getName()+"---whellNumber="+r.getWhellNumber() +"---id="+r.getId()); r.setFlag(false); //将线程池中的线程唤醒 r.notify(); } //加上sleep是为了让打印结果更显著 try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
我们再来看看打印结果:
name=帕萨特---whellNumber=4---id=698name=爱玛电动车---whellNumber=2---id=699name=帕萨特---whellNumber=4---id=700name=爱玛电动车---whellNumber=2---id=701name=帕萨特---whellNumber=4---id=702name=爱玛电动车---whellNumber=2---id=703name=帕萨特---whellNumber=4---id=704name=爱玛电动车---whellNumber=2---id=705name=帕萨特---whellNumber=4---id=706name=爱玛电动车---whellNumber=2---id=707
完美运行。我再来带着大家捋一遍:
- 生产者生产汽车的时候先判断标记flag,假如flag为true代表生产的汽车还未被消费,不能再进行生产了,将当前线程wait()掉。假如标记flag为false代表生产的汽车已经被消费掉,此时可以进行生产,生产完毕后将标记置为true并notify将消费者唤醒。
- 消费者生消费汽车的时候先判断标记flag,假如flag为true代表生产的汽车还未被消费,此时可以进行消费,消费完毕后将标记置为false并notify将生产者唤醒。假如标记flag为false代表生产的汽车已经被消费掉,不能再进行消费了,将当前线程wait()掉。
疑点
通过wait()可以将线程进入等待状态并释放锁,并将线程放入线程池当中,notify()会唤醒线程池中的任意一个线程,并将线程从线程池中移出。需要注意的是一把锁对应一个线程池。
wait()和notify()为什么要用r来调用?由于r为同步的锁,一把锁对应一个线程池,所以wait()和notify()必需要由锁来调用。
整个流程就是这样的,可能有那么一丢丢绕,不过多看两遍问题不大的。
3 多生产多消费
3.1 死锁
在真正讲多生产多消费之前我先给大家普及一个概念,死锁,什么是死锁呢?
顾名思义,死锁的意思就是被锁死了,在多线程中,假如程序被锁死无法执行,就是死锁现象。
我们用一段代码表现一下死锁的现象
public class DeadlockDemo { private static final Object lock1 = new Object(); private static final Object lock2 = new Object(); /** * @param args */ public static void main(String[] args) { // TODO Auto-generated method stub Deadlock1 d1 = new Deadlock1(lock1,lock2); Deadlock2 d2 = new Deadlock2(lock1,lock2); Thread t1 = new Thread(d1); Thread t2 = new Thread(d2); t1.start(); t2.start(); }}class Deadlock1 implements Runnable{ private Object lock1; private Object lock2; public Deadlock1(Object obj1,Object obj2){ this.lock1 = obj1; this.lock2 = obj2; } public void run() { // TODO Auto-generated method stub while(true){ synchronized(lock1){ System.out.println("Deadlock1----lock1"); synchronized(lock2){ System.out.println("Deadlock1----lock2"); } } } } }class Deadlock2 implements Runnable{ private Object lock1; private Object lock2; public Deadlock2(Object obj1,Object obj2){ this.lock1 = obj1; this.lock2 = obj2; } public void run() { // TODO Auto-generated method stub while(true){ synchronized(lock2){ System.out.println("Deadlock2----lock2"); synchronized(lock1){ System.out.println("Deadlock2----lock1"); } } } } }
运行后打印结果
Deadlock1----lock1Deadlock2----lock2
注意:run()中写的是无限循环,所以按理来说应该是无限打印,我们运行程序后只打印了这两行数据,而且我没有终止控制台打印
我来跟大家分析一下原理:
- 任务Deadlock1执行,判断了第一个同步代码块,此时锁lock1可用,于是持着锁lock1进入了第一个同步代码块,打印了:Deadlock1—-lock1,就在此时,线程切换到了任务Deadlock2
- 任务Deadlock2执行,判断第一个同步代码块,此时锁lock2可用,于是持着锁lock2进入了第一个同步代码块,打印了:Deadlock2—-lock2,接着向下执行,判断锁lock1不可用(由于所lock1已经被任务Deadlock1所占用),于是就进行等待,线程任务再次切换到任务Deadlock1
- 任务Deadlock1执行,判断第二个同步代码块,此时锁lock2不可用(由于所lock2已经被任务Deadlock2所占用),任务Deadlock1也进入了等待状态
通过以上形容:Deadlock1持着Deadlock2需要的锁进行等待,Deadlock2持着Deadlock1所需要的锁进行等待,这时候两个任务各自拿着对方需要的锁处于一种僵持现象,这种现象即可以称为死锁现象。
3.2 多生产多消费及安全问题
说完了单生产单消费我们再来说说多生产多消费,什么是多生产多消费呢?很简单,就是多个线程生产多个线程消费嘛,消费者、资源我们还用上面的代码,生产者略微做一丢丢改动,而后多加两个线程再来走一波。
生产任务中我将for循环中的 i 定义成了成员变量,只是便于讲解下面的内容,没必要纠结这一点。
//生产者任务 class Input implements Runnable{ private Resource r; //将i写为成员变量而不是写在for循环中是为了方便讲解下面的内容,没必要纠结这点 private int i = 0; public Input(Resource r){ this.r = r; } public void run() { //无限生产车辆 for(;;){ synchronized(r){ //flag为true的时候代表已经生产过,此时将当前线程wait,等待消费者消费 if(r.isFlag()){ try { r.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(i%2==0){ r.setId(i);//设置车的id r.setName("帕萨特");//设置车的型号 r.setWhell(4);//设置车的轮子数 }else{ r.setId(i);//设置车的id r.setName("爱玛电动车");//设置车的型号 r.setWhell(2);//设置车的轮子数 } i++; r.setFlag(true); //将线程池中的线程唤醒 r.notify(); } //加上sleep是为了让打印结果更显著 try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
public class ThreadDemo { private Resource r; public static void main(String[] args) { // TODO Auto-generated method stub Resource r = new Resource(); Input in = new Input(r); Output out = new Output(r); Thread in1= new Thread(in); Thread in2 = new Thread(in); Thread out1 = new Thread(out); Thread out2 = new Thread(out); in1.start();//开启生产者线程 in2 .start();//开启生产者线程 out1 .start();//开启消费者线程 out2 .start();//开启消费者线程 }}
打印结果
name=帕萨特---whellNumber=4---id=4752name=爱玛电动车---whellNumber=2---id=4753name=帕萨特---whellNumber=4---id=4754name=帕萨特---whellNumber=4---id=6270name=帕萨特---whellNumber=4---id=6270
我们可以看到又出现了安全问题,id在4754-6270之间的车辆未被打印并且id为6270的车辆被打印了两次,前面不是已经加过标记了吗?怎样又出现这种现象了?别急,我接着给大家分析
- 生产者线程in1得到执行权,生产了id为0的车辆,将flag置为true,循环回来再判断标记为true,此时执wait()方法进入等待状态
- 生产者线程in2得到执行权,判断标记为true,执行wait()方法进入等待状态。
- 消费者线程out1得到执行权,判断标记为true,不进行等待而是选择了消费id为0的车辆,消费完毕后将标记置为false并执行notify()将线程池中的任意一个线程给唤醒,假设唤醒的是in1
- 生产者线程in1再次得到执行权,此时in1被唤醒后不会判断标记而是选择生产一辆id为1的车辆,随后将标记置为true并执行notify()将线程池中任意一个线程给唤醒,假设唤醒的是in2
- 线程in2再次得到执行权,此时in2被唤醒后不会判断标记而是直接生产了一辆id为2的车辆
通过以上形容,我们知道id为1的车辆未被消费随即生产了id为2的车辆,假如in2唤醒的一直是in1,in1唤醒的一直是in2这样就会出现大批的车辆未被消费,就出现了上面id在4754-6270之间空缺的情况,同理也会出现屡次消费同一车辆的情况。
出现了问题就得继续处理啊,通过上面分析我们可以知道,问题的根源就在于生产者唤醒的为生产者或者者消费者唤醒的为消费者,被唤醒后没能再次判断标记就接着往下执行了,这时候怎样才能让线程被唤醒后任务重新判断标记呢?其实很简单,把生产者和消费者任务重的if语句,就是wait()上面那个if,改为wiile()即可以,改完之后我们再来跑一遍
打印结果
name=帕萨特---whellNumber=4---id=0name=爱玛电动车---whellNumber=2---id=1name=帕萨特---whellNumber=4---id=2name=爱玛电动车---whellNumber=2---id=3name=帕萨特---whellNumber=4---id=4name=爱玛电动车---whellNumber=2---id=5name=帕萨特---whellNumber=4---id=6
问题貌似处理了,但你当你自己代码跑一遍你会发现一个问题,会中止打印,也就是说出现了我们上一小节讲的死锁现象,我来个大家一步一步分析为什么会出现死锁现象。
- 线程in1开始执行,生产了一辆车将flag置为true,循环回来判断flag进入wait()状态,此时线程池中进行等待的线程有:in1
- 线程in2开始执行,判断flag为true进入wait()状态,此时线程池中进行等待的线程有:in1,in2
- 线程out1开始执行,判断flag为true,消费了一辆汽车将flag置为false并唤醒一个线程,我们假定唤醒的为in1(这里需要注意,被唤醒并不意味着会立刻执行,只是当前具有着执行资格但并不具有执行权),循环回来判读flag进入wait状态,此时线程池中的线程有in2,out1,随后out2得到执行权
- 线程out2开始执行,判断标记为false,进入等待状态,此时线程池中的线程有in2,out1,out2
- 线程in1开始执行,判断标记为false,生产了一辆汽车必将flag置为true并唤醒线程池中的一个线程,我们假定唤醒的是in2,随后in1循环判断flag进入wait()状态,此时线程池中的线程有in1,out1,out2
*int2得到执行权,判断标记为false,进入wait()状态,此时线程池中的线程有in1,in2,out1,out2
通过上面分析我们知道,四个线程都进入了wait状态,这也是死锁的一种,那这种问题怎样处理呢?我们首先来分析问题的根源,假如生产者 唤醒生产者、消费者唤醒消费者是可能出现死锁现象的,那我们能不能把对方唤醒?是可以的,java为我们提供了一个方法notifyAll(),这个方法会唤醒线程池中所有的线程,我们将生产者和消费者中的notify换成notifyAll再来试试
打印结果
name=帕萨特---whellNumber=4---id=2372name=爱玛电动车---whellNumber=2---id=2373name=帕萨特---whellNumber=4---id=2374name=爱玛电动车---whellNumber=2---id=2375name=帕萨特---whellNumber=4---id=2376name=爱玛电动车---whellNumber=2---id=2377
完美运行,并且不会出现死锁现象。
3.3 显示锁Lock
我们上一小节讲述了多生产多消费以及碰到的安全问题,我们可以通过notifyAll来处理死锁问题,但是我们仔细考虑一下,每次notifyAll的时候都会唤醒所有线程,而我们需要的是只唤醒对方线程就够了,这样就会产生效率上的一个问题,java也考虑到了这个问题,于是就推出了另一个处理方案Lock,什么是Lock呢?我们先来看一下官方的解释:
Lock是JDK1.5的新特性,它提供了比synchronized更广泛的锁定操作。实现允许更灵活的结构,可以具备差别很大的属性,可以支持多个相关的Condition。
说实话官方解释我看的也是一脸懵比,百看不如一练,代码走一波
//创立一个锁对象Lock lock = new ReentrantLock();lock.lock();//获取锁System.out.print("Russell Westbrook");lock.unlock();//释放锁
synchronized(obj){ System.out.print("Russell Westbrook");}
上面这两段代码的作用是完全一样的。那位说话了,你丫的作用完全一样你还噼里啪啦写着 一大堆,没事找事?这位兄弟先喝口凉茶消消火,官方中对Lock的解释提到了,Lock具备比较灵活的结构并支持多个Condition,那什么是Condition呢?Condition·为监视器的意思,什么是监视器呢?其实synchronized中的锁即可以称为是监视器,Lock相当于synchronized而Condition就相当于锁,比较特别的是一个Lock可以有多个Condition,通过Condition可以进行选择性的唤醒线程,既然是选择性的唤醒那是不是就能处理notifyAll的效率问题呢?
下面我们用Lock来写一个多生产多消费
//生产者任务 class Input implements Runnable{ private Resource r; private int i = 0; private Lock lock; private Condition in_con;//生产者监视器 private Condition out_con;//消费者监视器 public Input(Resource r,Lock lock,Condition in_con,Condition out_con){ this.r = r; this.lock = lock; this.in_con = in_con; this.out_con = out_con; } public void run() { //无限生产车辆 for(;;){ lock.lock();//获取锁 //flag为true的时候代表已经生产过,此时将当前线程wait,等待消费者消费 while(r.isFlag()){ try { in_con.await();//跟wait作用相同 } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(i%2==0){ r.setId(i);//设置车的id r.setName("帕萨特");//设置车的型号 r.setWhell(4);//设置车的轮子数 }else{ r.setId(i);//设置车的id r.setName("爱玛电动车");//设置车的型号 r.setWhell(2);//设置车的轮子数 } i++; r.setFlag(true); //将线程池中的消费者线程唤醒 out_con.signal(); lock.unlock();//释放锁 //加上sleep是为了让打印结果更显著 try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
//消费者任务 class Output implements Runnable{ private Resource r; private Lock lock; private Condition in_con;//生产者监视器 private Condition out_con;//消费者监视器 public Output(Resource r,Lock lock,Condition in_con,Condition out_con){ this.r = r; this.lock = lock; this.in_con = in_con; this.out_con = out_con; } public void run() { //无限消费车辆 for(;;){ lock.lock();//获取锁 while(!r.isFlag()){ try { out_con.await();//将消费者线程wait } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("name="+r.getName()+"---whellNumber="+r.getWhellNumber() +"---id="+r.getId()); r.setFlag(false); in_con.signal();//唤醒生产者线程 lock.unlock();//释放锁 //加上sleep是为了让打印结果更显著 try { Thread.sleep(1); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } }
代码做了少量改动,线程进入等待状态是通过调用监视器的await()方法,唤醒线程是通过监视器的signal()方法,我们仔细观察可以发现在生产者中唤醒操作是通过消费者监视器调用signal()方法进行的,这就表现了显示锁的好处,生产者监视器只能唤醒由自己await()的线程,消费者监视仪也一样,这样即可以实现生产者中只唤醒消费者线程、消费者中只唤醒生产者线程,看似完美的处理了nofityAll的效率问题,我们跑一遍代码试试
public class LockDemo { public static void main(String[] args) { // TODO Auto-generated method stub Resource r = new Resource(); Lock lock = new ReentrantLock(); //生产者监视器 Condition in_con = lock.newCondition(); //消费者监视器 Condition out_con = lock.newCondition(); Input in = new Input(r,lock,in_con,out_con); Output out = new Output(r,lock,in_con,out_con); Thread t1 = new Thread(in); Thread t2 = new Thread(in); Thread t3 = new Thread(out); Thread t4 = new Thread(out); t1.start();//开启生产者线程 t2.start();//开启生产者线程 t3.start();//开启消费者线程 t4.start();//开启消费者线程 }}
打印结果
name=帕萨特---whellNumber=4---id=3970name=爱玛电动车---whellNumber=2---id=3971name=帕萨特---whellNumber=4---id=3972name=爱玛电动车---whellNumber=2---id=3973name=帕萨特---whellNumber=4---id=3974name=爱玛电动车---whellNumber=2---id=3975name=帕萨特---whellNumber=4---id=3976name=爱玛电动车---whellNumber=2---id=3977
这次是真的完美运行,所以以后碰到多生产多消费的问题可以通过显示锁Lock配合其监视器来实现。
总结
整篇文章内容大致可以细分为四块分别是:单生产单消费、死锁、多生产多消费、显示锁Lock,写出线程间通信的同时也要考虑安全问题,由于线程是最容易出现BUG的程序,处理安全问题的时候要充分的利用同步、锁、等待唤醒机制。Emmmmm,终于写完了,这篇文章是我目前为止写的字数最多、用时最长、同时也是最用心的一篇文章,写完之后我又检查了好几遍来查错,有几个地方的确有点绕,可能形容的略有问题,也希望发现问题的同学能够及时反馈给我,在此先行谢过。好了,线程间通信到这就要结束了,下篇文章我为大家带来多线程系列(三)阻塞队列。
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 多线程系列(二)线程间通信