https://www.cnblogs.com/moongeek/p/7631447.html
https://blog.51cto.com/12304309/2138845
1、wait()、notify/notifyAll() 方法是Object的本地final方法,无法被重写。
2、wait()使当前线程阻塞,前提是 必须先获得锁,一般配合synchronized 关键字使用,即,一般在synchronized 同步代码块里使用 wait()、notify/notifyAll() 方法。
3、 由于 wait()、notify/notifyAll() 在synchronized 代码块执行,说明当前线程一定是获取了锁的。
当线程执行wait()方法时候,会释放当前的锁,然后让出CPU,进入等待状态。
只有当 notify/notifyAll() 被执行时候,才会唤醒一个或多个正处于等待状态的线程,然后继续往下执行,直到执行完synchronized 代码块的代码或是中途遇到wait() ,再次释放锁。
也就是说,notify/notifyAll() 的执行只是唤醒沉睡的线程,而不会立即释放锁,锁的释放要看代码块的具体执行情况。所以在编程中,尽量在使用了notify/notifyAll() 后立即退出临界区,以唤醒其他线程
4、wait() 需要被try catch包围,中断也可以使wait等待的线程唤醒。
5、notify 和wait 的顺序不能错,如果A线程先执行notify方法,B线程在执行wait方法,那么B线程是无法被唤醒的。
6、notify 和 notifyAll的区别
notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。如果当前情况下有多个线程需要被唤醒,推荐使用notifyAll 方法。比如在生产者-消费者里面的使用,每次都需要唤醒所有的消费者或是生产者,以判断程序是否可以继续往下执行。
7、在多线程中要测试某个条件的变化,使用if 还是while?
要注意,notify唤醒沉睡的线程后,线程会接着上次的执行继续往下执行。所以在进行条件判断时候,可以先把 wait 语句忽略不计来进行考虑,显然,要确保程序一定要执行,并且要保证程序直到满足一定的条件再执行,要使用while来执行,以确保条件满足和一定执行。如下代码:
1 public class K { 2 //状态锁 3 private Object lock; 4 //条件变量 5 private int now,need; 6 public void produce(int num){ 7 //同步 8 synchronized (lock){ 9 //当前有的不满足需要,进行等待 10 while(now < need){ 11 try { 12 //等待阻塞 13 wait(); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 System.out.println("我被唤醒了!"); 18 } 19 // 做其他的事情 20 } 21 } 22 } 23
显然,只有当前值满足需要值的时候,线程才可以往下执行,所以,必须使用while 循环阻塞。注意,wait() 当被唤醒时候,只是让while循环继续往下走.如果此处用if的话,意味着if继续往下走,会跳出if语句块。但是,notifyAll 只是负责唤醒线程,并不保证条件云云,所以需要手动来保证程序的逻辑。
8、实现生产者和消费者问题
什么是生产者-消费者问题呢?
如上图,假设有一个公共的容量有限的池子,有两种人,一种是生产者,另一种是消费者。需要满足如下条件:
1、生产者产生资源往池子里添加,前提是池子没有满,如果池子满了,则生产者暂停生产,直到自己的生成能放下池子。
2、消费者消耗池子里的资源,前提是池子的资源不为空,否则消费者暂停消耗,进入等待直到池子里有资源数满足自己的需求。
- 仓库类
1 import java.util.LinkedList; 2 3 /** 4 * 生产者和消费者的问题 5 * wait、notify/notifyAll() 实现 6 */ 7 public class Storage1 implements AbstractStorage { 8 //仓库最大容量 9 private final int MAX_SIZE = 100; 10 //仓库存储的载体 11 private LinkedList list = new LinkedList(); 12 13 //生产产品 14 public void produce(int num){ 15 //同步 16 synchronized (list){ 17 //仓库剩余的容量不足以存放即将要生产的数量,暂停生产 18 while(list.size()+num > MAX_SIZE){ 19 System.out.println("【要生产的产品数量】:" + num + "\t【库存量】:" 20 + list.size() + "\t暂时不能执行生产任务!"); 21 22 try { 23 //条件不满足,生产阻塞 24 list.wait(); 25 } catch (InterruptedException e) { 26 e.printStackTrace(); 27 } 28 } 29 30 for(int i=0;i<num;i++){ 31 list.add(new Object()); 32 } 33 34 System.out.println("【已经生产产品数】:" + num + "\t【现仓储量为】:" + list.size()); 35 36 list.notifyAll(); 37 } 38 } 39 40 //消费产品 41 public void consume(int num){ 42 synchronized (list){ 43 44 //不满足消费条件 45 while(num > list.size()){ 46 System.out.println("【要消费的产品数量】:" + num + "\t【库存量】:" 47 + list.size() + "\t暂时不能执行生产任务!"); 48 49 try { 50 list.wait(); 51 } catch (InterruptedException e) { 52 e.printStackTrace(); 53 } 54 } 55 56 //消费条件满足,开始消费 57 for(int i=0;i<num;i++){ 58 list.remove(); 59 } 60 61 System.out.println("【已经消费产品数】:" + num + "\t【现仓储量为】:" + list.size()); 62 63 list.notifyAll(); 64 } 65 } 66 }
- 抽象仓库类
1 public interface AbstractStorage { 2 void consume(int num); 3 void produce(int num); 4 }
- 生产者
1 public class Producer extends Thread{ 2 //每次生产的数量 3 private int num ; 4 5 //所属的仓库 6 public AbstractStorage abstractStorage; 7 8 public Producer(AbstractStorage abstractStorage){ 9 this.abstractStorage = abstractStorage; 10 } 11 12 public void setNum(int num){ 13 this.num = num; 14 } 15 16 // 线程run函数 17 @Override 18 public void run() 19 { 20 produce(num); 21 } 22 23 // 调用仓库Storage的生产函数 24 public void produce(int num) 25 { 26 abstractStorage.produce(num); 27 } 28 }
- 消费者
1 public class Consumer extends Thread{ 2 // 每次消费的产品数量 3 private int num; 4 5 // 所在放置的仓库 6 private AbstractStorage abstractStorage1; 7 8 // 构造函数,设置仓库 9 public Consumer(AbstractStorage abstractStorage1) 10 { 11 this.abstractStorage1 = abstractStorage1; 12 } 13 14 // 线程run函数 15 public void run() 16 { 17 consume(num); 18 } 19 20 // 调用仓库Storage的生产函数 21 public void consume(int num) 22 { 23 abstractStorage1.consume(num); 24 } 25 26 public void setNum(int num){ 27 this.num = num; 28 } 29 }
- 测试
1 public class Test{ 2 public static void main(String[] args) { 3 // 仓库对象 4 AbstractStorage abstractStorage = new Storage1(); 5 6 // 生产者对象 7 Producer p1 = new Producer(abstractStorage); 8 Producer p2 = new Producer(abstractStorage); 9 Producer p3 = new Producer(abstractStorage); 10 Producer p4 = new Producer(abstractStorage); 11 Producer p5 = new Producer(abstractStorage); 12 Producer p6 = new Producer(abstractStorage); 13 Producer p7 = new Producer(abstractStorage); 14 15 // 消费者对象 16 Consumer c1 = new Consumer(abstractStorage); 17 Consumer c2 = new Consumer(abstractStorage); 18 Consumer c3 = new Consumer(abstractStorage); 19 20 // 设置生产者产品生产数量 21 p1.setNum(10); 22 p2.setNum(10); 23 p3.setNum(10); 24 p4.setNum(10); 25 p5.setNum(10); 26 p6.setNum(10); 27 p7.setNum(80); 28 29 // 设置消费者产品消费数量 30 c1.setNum(50); 31 c2.setNum(20); 32 c3.setNum(30); 33 34 // 线程开始执行 35 c1.start(); 36 c2.start(); 37 c3.start(); 38 39 p1.start(); 40 p2.start(); 41 p3.start(); 42 p4.start(); 43 p5.start(); 44 p6.start(); 45 p7.start(); 46 } 47 }
- 输出
【要消费的产品数量】:50 【库存量】:0 暂时不能执行生产任务! 【要消费的产品数量】:20 【库存量】:0 暂时不能执行生产任务! 【要消费的产品数量】:30 【库存量】:0 暂时不能执行生产任务! 【已经生产产品数】:10 【现仓储量为】:10 【要消费的产品数量】:30 【库存量】:10 暂时不能执行生产任务! 【要消费的产品数量】:20 【库存量】:10 暂时不能执行生产任务! 【要消费的产品数量】:50 【库存量】:10 暂时不能执行生产任务! 【已经生产产品数】:10 【现仓储量为】:20 【已经生产产品数】:10 【现仓储量为】:30 【要消费的产品数量】:50 【库存量】:30 暂时不能执行生产任务! 【已经消费产品数】:20 【现仓储量为】:10 【要消费的产品数量】:30 【库存量】:10 暂时不能执行生产任务! 【已经生产产品数】:10 【现仓储量为】:20 【要消费的产品数量】:50 【库存量】:20 暂时不能执行生产任务! 【要消费的产品数量】:30 【库存量】:20 暂时不能执行生产任务! 【已经生产产品数】:10 【现仓储量为】:30 【已经消费产品数】:30 【现仓储量为】:0 【要消费的产品数量】:50 【库存量】:0 暂时不能执行生产任务! 【已经生产产品数】:10 【现仓储量为】:10 【要消费的产品数量】:50 【库存量】:10 暂时不能执行生产任务! 【已经生产产品数】:80 【现仓储量为】:90 【已经消费产品数】:50 【现仓储量为】:40
我自己总结的Java学习的系统知识点以及面试问题,目前已经开源,会一直完善下去,欢迎建议和指导欢迎Star: https://github.com/Snailclimb/Java-Guide
本节思维导图:
一 等待/通知机制介绍
1.1 不使用等待/通知机制
当两个线程之间存在生产和消费者关系,也就是说第一个线程(生产者)做相应的操作然后第二个线程(消费者)感知到了变化又进行相应的操作。比如像下面的whie语句一样,假设这个value值就是第一个线程操作的结果,doSomething()是第二个线程要做的事,当满足条件value=desire后才执行doSomething()。
但是这里有个问题就是:第二个语句不停过通过轮询机制来检测判断条件是否成立。如果轮询时间的间隔太小会浪费CPU资源,轮询时间的间隔太大,就可能取不到自己想要的数据。所以这里就需要我们今天讲到的等待/通知(wait/notify)机制来解决这两个矛盾。
while(value=desire){
doSomething();
}
1.2 什么是等待/通知机制?
通俗来讲:
等待/通知机制在我们生活中比比皆是,一个形象的例子就是厨师和服务员之间就存在等待/通知机制。
- 厨师做完一道菜的时间是不确定的,所以菜到服务员手中的时间是不确定的;
- 服务员就需要去“等待(wait)”;
- 厨师把菜做完之后,按一下铃,这里的按铃就是“通知(nofity)”;
- 服务员听到铃声之后就知道菜做好了,他可以去端菜了。
用专业术语讲:
等待/通知机制,是指一个线程A调用了对象O的wait()方法进入等待状态,而另一个线程B调用了对象O的notify()/notifyAll()方法,线程A收到通知后退出等待队列,进入可运行状态,进而执行后续操作。上诉两个线程通过对象O来完成交互,而对象上的wait()方法和notify()/notifyAll()方法的关系就如同开关信号一样,用来完成等待方和通知方之间的交互工作。
1.3 等待/通知机制的相关方法
方法名称 | 描述 |
---|---|
notify() | 随机唤醒等待队列中等待同一共享资源的 “一个线程”,并使该线程退出等待队列,进入可运行状态,也就是notify()方法仅通知“一个线程” |
notifyAll() | 使所有正在等待队列中等待同一共享资源的 “全部线程” 退出等待队列,进入可运行状态。此时,优先级最高的那个线程最先执行,但也有可能是随机执行,这取决于JVM虚拟机的实现 |
wait() | 使调用该方法的线程释放共享资源锁,然后从运行状态退出,进入等待队列,直到被再次唤醒 |
wait(long) | 超时等待一段时间,这里的参数时间是毫秒,也就是等待长达n毫秒,如果没有通知就超时返回 |
wait(long,int) | 对于超时时间更细力度的控制,可以达到纳秒 |
二 等待/通知机制的实现
2.1 我的第一个等待/通知机制程序
MyList.java
public class MyList {
private static List<String> list = new ArrayList<String>();
public static void add() {
list.add("anyString");
}
public static int size() {
return list.size();
}
}
ThreadA.java
public class ThreadA extends Thread {
private Object lock;
public ThreadA(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
if (MyList.size() != 5) {
System.out.println("wait begin "
+ System.currentTimeMillis());
lock.wait();
System.out.println("wait end "
+ System.currentTimeMillis());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ThreadB.java
public class ThreadB extends Thread {
private Object lock;
public ThreadB(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
try {
synchronized (lock) {
for (int i = 0; i < 10; i++) {
MyList.add();
if (MyList.size() == 5) {
lock.notify();
System.out.println("已发出通知!");
}
System.out.println("添加了" + (i + 1) + "个元素!");
Thread.sleep(1000);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Run.java
public class Run {
public static void main(String[] args) {
try {
Object lock = new Object();
ThreadA a = new ThreadA(lock);
a.start();
Thread.sleep(50);
ThreadB b = new ThreadB(lock);
b.start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
运行结果:
从运行结果:"wait end 1521967322359"最后输出可以看出,<font color="red">notify()执行后并不会立即释放锁。</font>下面我们会补充介绍这个知识点。
synchronized关键字可以将任何一个Object对象作为同步对象来看待,而Java为每个Object都实现了等待/通知(wait/notify)机制的相关方法,它们必须用在synchronized关键字同步的Object的临界区内。通过调用wait()方法可以使处于临界区内的线程进入等待状态,同时释放被同步对象的锁。而notify()方法可以唤醒一个因调用wait操作而处于阻塞状态中的线程,使其进入就绪状态。被重新唤醒的线程会视图重新获得临界区的控制权也就是锁,并继续执行wait方法之后的代码。如果发出notify操作时没有处于阻塞状态中的线程,那么该命令会被忽略。
如果我们这里不通过等待/通知(wait/notify)机制实现,而是使用如下的while循环实现的话,我们上面也讲过会有很大的弊端。
while(MyList.size() == 5){
doSomething();
}
2.2线程的基本状态
上面几章的学习中我们已经掌握了与线程有关的大部分API,这些API可以改变线程对象的状态。如下图所示:
- 新建(new):新创建了一个线程对象。
- 可运行(runnable):线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获 取cpu的使用权。
- 运行(running):可运行状态(runnable)的线程获得了cpu时间片(timeslice),执行程序代码。
-
阻塞(block):阻塞状态是指线程因为某种原因放弃了cpu使用权,也即让出了cpu timeslice,暂时停止运行。直到线程进入可运行(runnable)状态,才有 机会再次获得cpu timeslice转到运行(running)状态。阻塞的情况分三种:
(一). 等待阻塞:运行(running)的线程执行o.wait()方法,JVM会把该线程放 入等待队列(waitting queue)中。
(二). **同步阻塞**:运行(running)的线程在获取对象的同步锁时,若该同步锁 被别的线程占用,则JVM会把该线程放入锁池(lock pool)中。
(三). **其他阻塞**: 运行(running)的线程执行Thread.sleep(long ms)或t.join()方法,或者发出了I/O请求时,JVM会把该线程置为阻塞状态。当sleep()状态超时join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入可运行(runnable)状态。
- 死亡(dead):线程run()、main()方法执行结束,或者因异常退出了run()方法,则该线程结束生命周期。死亡的线程不可再次复生。
备注:
可以用早起坐地铁来比喻这个过程:
还没起床:sleeping
起床收拾好了,随时可以坐地铁出发:Runnable
等地铁来:Waiting
地铁来了,但要排队上地铁:I/O阻塞
上了地铁,发现暂时没座位:synchronized阻塞
地铁上找到座位:Running
到达目的地:Dead
2.3 notify()锁不释放
<font color="red">当方法wait()被执行后,锁自动被释放,但执行完notify()方法后,锁不会自动释放。必须执行完notify()方法所在的synchronized代码块后才释放。</font>
下面我们通过代码验证一下:
(完整代码:https://github.com/Snailclimb/threadDemo/tree/master/src/wait\_notifyHoldLock)
<font size="2">带wait方法的synchronized代码块</font>
synchronized (lock) {
System.out.println("begin wait() ThreadName="
+ Thread.currentThread().getName());
lock.wait();
System.out.println(" end wait() ThreadName="
+ Thread.currentThread().getName());
}
<font size="2">带notify方法的synchronized代码块</font>
synchronized (lock) {
System.out.println("begin notify() ThreadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
lock.notify();
Thread.sleep(5000);
System.out.println(" end notify() ThreadName="
+ Thread.currentThread().getName() + " time="
+ System.currentTimeMillis());
}
如果有三个同一个对象实例的线程a,b,c,a线程执行带wait方法的synchronized代码块然后bb线程执行带notify方法的synchronized代码块紧接着c执行带notify方法的synchronized代码块。
<font size="2">运行效果如下:</font>
<font color="red">这也验证了我们刚开始的结论:必须执行完notify()方法所在的synchronized代码块后才释放。</font>
2.4 当interrupt方法遇到wait方法
<font color="red">当线程呈wait状态时,对线程对象调用interrupt方法会出现InterrupedException异常。</font>
<font size="2">Service.java</font>
public class Service {
public void testMethod(Object lock) {
try {
synchronized (lock) {
System.out.println("begin wait()");
lock.wait();
System.out.println(" end wait()");
}
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("出现异常了,因为呈wait状态的线程被interrupt了!");
}
}
}
<font size="2">ThreadA.java</font>
public class ThreadA extends Thread {
private Object lock;
public ThreadA(Object lock) {
super();
this.lock = lock;
}
@Override
public void run() {
Service service = new Service();
service.testMethod(lock);
}
}
<font size="2">Test.java</font>
public class Test {
public static void main(String[] args) {
try {
Object lock = new Object();
ThreadA a = new ThreadA(lock);
a.start();
Thread.sleep(5000);
a.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
<font size="2">运行结果:</font>
参考:
《Java多线程编程核心技术》
《Java并发编程的艺术》