java多线程编程的概述以及案例详解
引子: java编程中有时候会要求线程安全(注:多个线程同时访问同一代码的时候,不会产生不同的结果。编写线程安全的代码需要线程同步),这时候就需要进行多线程编程。从而用到线程间通信的技术。那么在java里面,线程间通信是怎么实现的?这篇文章将通过一个案例详细分析。
文章关键词: Object,wait,notify,notifyAll,锁,同步(synchronized).
详解一个经典的生产者消费者模型,其中用到了 wait和notifyAll方法。
源码如下:
1 2
3 import java.util.LinkedList;
4 import java.util.Queue;
5
6 public class MainTest {
7 public static void main(String[] args) {
8 test();
9 }
10
11 private static final long waitTime = 3000;
12
13 private static void test() {
14 Queue<Integer> 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 extends Thread {
34 Queue<Integer> queue;// queue,对象锁
35 int maxsize;// 貌似是队列的最大产量
36
37 Producer(Queue<Integer> queue, int maxsize, String name) {
38 this.queue = queue;
39 this.maxsize = maxsize;
40 this.setName(name);
41 }
42
43 @Override
44 public void run() {
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 extends Thread {
80 Queue<Integer> queue;
81 int maxsize;
82
83 Consumer(Queue<Integer> queue, int maxsize, String name) {
84 this.queue = queue;
85 this.maxsize = maxsize;
86 this.setName(name);
87 }
88
89 @Override
90 public void run() {
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,唤醒所有等到对象锁的线程,如果有多个被唤醒的线程,锁将会被争夺,争夺到锁的线程就可以执行.
===================就写到这里了。上面的是基础知识,在复杂场景中可能会被复杂化千万倍,但是万变不离其宗,了解了原理,就能应对大部分场景了。