引子: java编程中有时候会要求线程安全(注:多个线程同时访问同一代码的时候,不会产生不同的结果。编写线程安全的代码需要线程同步),这时候就需要进行多线程编程。从而用到线程间通信的技术。那么在java里面,线程间通信是怎么实现的?这篇文章将通过一个案例详细分析。
文章关键词: Object,wait,notify,notifyAll,锁,同步(synchronized).
详解一个经典的生产者消费者模型,其中用到了 wait和notifyAll方法。
源码如下:
1 2
3 importjava.util.LinkedList;4 importjava.util.Queue;5
6 public classMainTest {7 public static voidmain(String[] args) {8 test();9 }10
11 private static final long waitTime = 3000;12
13 private static voidtest() {14 Queue queue = new LinkedList<>();//队列对象,它就是所谓的“锁”
15 int maxsize = 2;//队列中的最大元素个数限制16
17 //下面4个线程,一瞬间只能有一个线程获得该对象的锁,而进入同步代码块
18 Producer producer = new Producer(queue, maxsize, "Producer");19 Consumer consumer1 = new Consumer(queue, maxsize, "Consumer1");20 Consumer consumer2 = new Consumer(queue, maxsize, "Consumer2");21 Consumer consumer3 = new Consumer(queue, maxsize, "Consumer3");22
23 //其实随便先启动哪个都无所谓,因为只有一个锁,每一次只会有一个线程能持有这个锁,来操作queue
24 producer.start();25 consumer2.start();26 consumer1.start();27 consumer3.start();28 }29
30 /**
31 * 生产者线程32 */
33 public static class Producer extendsThread {34 Queue queue;//queue,对象锁
35 int maxsize;//貌似是队列的最大产量
36
37 Producer(Queue queue, intmaxsize, String name) {38 this.queue =queue;39 this.maxsize =maxsize;40 this.setName(name);41 }42
43 @Override44 public voidrun() {45 while (true) {//无限循环,不停生产元素,直到达到上限,只要达到上限,那就wait等待。
46 synchronized (queue) {//同步代码块,只有持有queue这个锁的对象才能访问这个代码块
47 try{48 Thread.sleep(waitTime);49 //sleep和wait的区别,sleep会让当前执行的线程阻塞一段时间,但是不会释放锁,50 //但是wait,会阻塞,并且会释放锁
51 } catch(Exception e) {52 }53
54 System.out.println(this.getName() + "获得队列的锁");//只有你获得了queue对象的锁,你才能执行到这里55 //条件的判断一定要使用while而不是if
56 while (queue.size() == maxsize) {//判断生产有没有达到上限,如果达到了上限,就让当前线程等待
57 System.out.println("队列已满,生产者" + this.getName() + "等待");58 try{59 queue.wait();//让当前线程等待,直到其他线程调用notifyAll
60 } catch(Exception e) {61 }62 }63
64 //下面写的就是生产过程
65 int num = (int) (Math.random() * 100);66 queue.offer(num);//将一个int数字插入到队列中
67
68 System.out.println(this.getName() + "生产一个元素:" +num);69 //唤醒其他线程,在这里案例中是 "等待中"的消费者线程
70 queue.notifyAll();//(注:notifyAll的作用是71 //唤醒所有持有queue对象锁的正在等待的线程)
72
73 System.out.println(this.getName() + "退出一次生产过程!");74 }75 }76 }77 }78
79 public static class Consumer extendsThread {80 Queuequeue;81 intmaxsize;82
83 Consumer(Queue queue, intmaxsize, String name) {84 this.queue =queue;85 this.maxsize =maxsize;86 this.setName(name);87 }88
89 @Override90 public voidrun() {91 while (true) {92 synchronized (queue) {//要想进入下面的代码,就必须先获得锁。
93 try{94 Thread.sleep(waitTime);//sleep,让当前线程阻塞指定时长,但是并不会释放queue锁
95 } catch(Exception e) {96 }97
98 System.out.println(this.getName() + "获得队列的锁");//拿到了锁,才能执行到这里99 //条件的判断一定要使用while而不是if,
100 while (queue.isEmpty()) {//while判断队列是否为空,如果为空,当前消费者线程就必须wait,等生产者先生产元素101 //这里,消费者有多个(因为有多个consumer线程),每一个消费者如果发现了队列空了,就会wait。
102 System.out.println("队列为空,消费者" + this.getName() + "等待");103 try{104 queue.wait();105 } catch(Exception e) {106 }107 }108
109 //如果队列不是空,那么就弹出一个元素
110 int num =queue.poll();111 System.out.println(this.getName() + "消费一个元素:" +num);112 queue.notifyAll();//然后再唤醒所有线程,唤醒不会释放自己的锁
113
114 System.out.println(this.getName() + "退出一次消费过程!");115 }116 }117 }118 }119 }
案例解析:
1)此案例模拟的是,生产者线程 生产元素并且插入到Queue中,Queue有一个存储个数的限制。消费者线程,从Queue中拿出元素。两个线程都是无限循环执行的。
2)在生产者线程的生产过程(随机产生一个int然后插入到queue中)执行之前,首先检查Queue的存储个数有没有到达上限,如果到达了,那就不能生产,代码中调用了queue.wait();来使生产者线程进入等待状态并且释放锁。如果没超过,那就反复执行,直到到达上限。
3)消费者线程在执行消费过程(从queue中弹出一个元素)执行之前,首先要检查queue是不是空,如果是空,那就不能消费,调用queue.wait()让消费线程进入等待状态并且释放锁。
4)在生产过程 或 消费过程执行完毕之后,都会有queue.notifyAll();来唤醒等待锁的所有线程。
5)生产者中,判定queue的元素个数是不是到达上限。以及 消费者中,判定queue是不是空,这种判定queue.wait()的条件 所使用的关键字,并不是if,而是while.
因为在执行了wait之后,该线程的执行,会暂时停留在这个while循环中,等待被唤醒,一旦被唤醒,while循环会继续执行,从而会再次判断条件是否满足。
6)代码中能找到Thread.sleep(long);方法,它的作用,是当当前线程阻塞指定时间,但是它并不会释放锁。而wait除了阻塞之外,还会释放锁。
案例执行的结果打印:
Producer获得队列的锁
Producer生产一个元素:86
Producer退出一次生产过程!
Producer获得队列的锁
Producer生产一个元素:31
Producer退出一次生产过程!
Producer获得队列的锁
队列已满,生产者Producer等待
Consumer2获得队列的锁
Consumer2消费一个元素:86
Consumer2退出一次消费过程!
Consumer3获得队列的锁
Consumer3消费一个元素:31
Consumer3退出一次消费过程!
Consumer1获得队列的锁
队列为空,消费者Consumer1等待
Consumer3获得队列的锁
队列为空,消费者Consumer3等待
Consumer2获得队列的锁
队列为空,消费者Consumer2等待
Producer生产一个元素:29
Producer退出一次生产过程!
Producer获得队列的锁
Producer生产一个元素:82
Producer退出一次生产过程!
Producer获得队列的锁
队列已满,生产者Producer等待
Consumer2消费一个元素:29
Consumer2退出一次消费过程!
结果分析(请对照日志来看,大神请绕道,下面的描述比较啰嗦):
由于首先启动的是生产者线程(Producer),所以producer先获得了锁,进行了两次生产。再次尝试生产的时候发现queue满了,于是,生产者进入等待。
之后,consumer2的得到了锁,于是进行消费,消费执行了一次,锁被consumer3夺走,consumer3执行了一次消费。
之后,consumer1得到了锁,就当它准备开始消费的时候,发现queue空了,不能消费了,于是代码调用queue.wait().来让consumer1进入等待。
之后,consumer3和consumer2相继得到锁,但是他们都发现,queue空了,也不能消费,于是同样调用queue.wait()来让consumer3和consumer1进入等待。
再然后,生产者得到了锁(这里可能很奇怪,生产者不是在等待么?它什么时候被唤醒的,查看Consumer的代码,能发现,在每一次成功消费之后,都会有queue.notifyAll(),也就是说,在之前cunsumer2消费之后,生产者就已经被唤醒了,只是他没有得到锁,所以就没有执行生产过程)。
生产者得到锁之后,继续while循环,发现queue并没有填满,于是进入生产过程。之后···就是无限循环了。
这种模型在线程安全比较高的场景中,会被经常用到,比如买票系统,同一张票不能被卖两次。所以,这张票,在同一时间只能被一个线程访问。
-------------------
案例解析完毕,但是针对java多线程,也许有人会有其他疑问,下面列举几个比较重要的问题加以说明:
问:在java中,wait,notify以及notifyAll是用来做线程之间的通信的,但是为什么这3个方法不是在Thread类里面,而是在Object类里面?
答:
这3个方法虽然是用于线程间的通信,但是他们并不是直接就在Thread类里面,而是在Object类。
这是 因为 调用一个Object的wait,notify,notifyAll 必须保证该段代码对于该Object是同步的, 否则就可能会报异常IllegalMonitorStateException(具体可以进入Object类的源码搜索此异常,注释中有详细说明),通常的写法如下,
synchronized(obj){//在执行wait,notify,notifyAll时,必须保证这段代码持有obj对象的锁。
obj.wait();
...
obj.notify();
...
obj.notifyAll();
}
如果多个线程都写了上面的代码,那么同一时间,只会有一个线程能获取obj对象锁。
所以说,这3个方法在Object类里,而不是在Thread类里,其实是java框架的设定,通过Object锁来完成线程间的通信。
问:wait,notify,notifyAll的作用分别是什么?
答:
wait-让当前线程进入等待状态,并且释放锁;
notify -唤醒任意一个正在等待锁的线程,并且让它得到锁。
notifyAll,唤醒所有等待对象锁的线程,如果有多个线程都被唤醒,那么锁将会被他们争夺,同一时间只会有一个线程得到锁。
问:notify,notifyAll有啥区别?
答:
notify,让任意一个等待对象锁的线程得到锁,并且唤醒他。
notifyAll,唤醒所有等到对象锁的线程,如果有多个被唤醒的线程,锁将会被争夺,争夺到锁的线程就可以执行.
===================就写到这里了。上面的是基础知识,在复杂场景中可能会被复杂化千万倍,但是万变不离其宗,了解了原理,就能应对大部分场景了。