由于线程的随机调度,可能会出现“线程饿死”的问题:也就是一个线程加锁执行,然后解锁,其他线程抢不到,一直是这个线程在重复操作
void wait() |
当前线程等待,直到被其他线程唤醒 |
void notify() |
随机唤醒单个线程 |
void notifyAll() |
唤醒所有线程 |
等待(wait):当一个线程执行到某个对象的wait()方法时,它会释放当前持有的锁(如果有的话),并进入等待状态。此时,线程不再参与CPU的调度,直到其他线程调用同一对象的notify()或notifyAll()方法将其唤醒,类似的,wait() 方法也可以传入一个参数表示等待的时间,不加参数就会一直等
唤醒(notify/notifyAll):
notify: 唤醒在该对象监视器上等待的某个线程,如果有多个线程在等待,那么具体唤醒哪一个是随机的
notifyAll: 唤醒在该对象监视器上等待的所有线程
上面的方法是Object提供的方法,所以任意的Object对象都可以调用,下面来演示一下:
public class ThreadDemo14 { public static void main(String[] args) throws InterruptedException { Object obj = new Object(); System.out.println("wait前"); obj.wait(); System.out.println("wait后"); } }
结果抛出了一个异常:非法的锁状态异常,也就是调用wait的时候,当前锁的状态是非法的
这是因为,在wait方法中,会先解锁然后再等待,所以要使用wait,就要先加个锁,阻塞等待就是把自己的锁释放掉再等待,不然一直拿着锁等待,其他线程就没机会了
把wait操作写在synchronized方法里就可以了,运行之后main线程就一直等待中,在jconsole中看到的也是waiting的状态
注意:wait操作进行解锁和阻塞等待是同时执行的(打包原子),如果不是同时执行就可能刚解锁就被其他线程抢占了,然后进行了唤醒操作,这时原来的线程再去等待,已经错过了唤醒操作,就会一直等
wait执行的操作:1. 释放锁并进入阻塞等待,准备接收唤醒通知 2. 收到通知后唤醒,并重新尝试获得锁
接下来再看一下notify方法:
public class ThreadDemo15 { private static Object lock = new Object(); public static void main(String[] args) { Thread t1 = new Thread(()->{ synchronized (lock){ System.out.println("t1 wait 前"); try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("t1 wait 后"); } }); Thread t2 = new Thread(()->{ synchronized (lock){ System.out.println("t2 notify 前"); Scanner sc = new Scanner(System.in); sc.next();//这里的输入主要是构造阻塞 lock.notify(); System.out.println("t2 notify 后"); } }); } }
然后就会发现又出错了,还是之前的错误,notify也需要先加锁才可以
把之前的notify也加进synchornized就可以了,并且还需要确保是同一把锁
调用wait方法的线程会释放其持有的锁,被唤醒的线程在执行之前,必须重新获取被释放的锁
public class Cook extends Thread { public void run() { while (true) { synchronized (Desk.lock) { if (Desk.count == 0) { break; } else { if (Desk.foodFlag == 0) { try { Desk.lock.wait();//厨师等待 } catch (InterruptedException e) { e.printStackTrace(); } } else { Desk.count--; System.out.println("还能再吃" + Desk.count + "碗"); Desk.lock.notifyAll();//唤醒所有线程 Desk.foodFlag = 0; } } } } } }
public class Foodie extends Thread { public void run() { while (true) { synchronized (Desk.lock) { if (Desk.count == 0) { break; } else { if (Desk.foodFlag == 1) { try { Desk.lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { System.out.println("已经做好了"); Desk.foodFlag = 1; Desk.lock.notifyAll(); } } } } } }
public class Desk { public static int foodFlag = 0; public static int count = 10; //锁对象 public static Object lock = new Object(); }
这里实现的功能就是,厨师做好食物放在桌子上,美食家开始品尝,如果桌子上没有食物,美食家就等待,有的话,厨师进行等待
sleep() 和 wait() 的区别:
这两个方法看起来都是让线程等待,但是是有本质区别的,使用wait的目的是为了提前唤醒,sleep就是固定时间的阻塞,不涉及唤醒,虽然之前说的Interrupt可以使sleep提前醒来,但是Interrupt是终止线程,并不是唤醒,wait必须和锁一起使用,wait会先释放锁再等待,sleep和锁无关,不加锁sleep可以正常使用,加上锁sleep不会释放锁,抱着锁一起睡,其他线程无法拿到锁
在刚开始提到过,如果有多个线程都在同一个对象上wait,那么唤醒哪一个线程是随机的:
public class ThreadDemo16 { private static Object lock = new Object(); public static void main(String[] args) { Thread t1 = new Thread(() -> { synchronized (lock) { System.out.println("t1 wait 前"); try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("t1 wait 后"); } }); Thread t2 = new Thread(() -> { synchronized (lock) { System.out.println("t2 wait 前"); try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("t2 wait 后"); } }); Thread t3 = new Thread(() -> { synchronized (lock) { System.out.println("t3 wait 前"); try { lock.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("t wait 后"); } }); Thread t4 = new Thread(() -> { synchronized (lock) { System.out.println("t4 notify 前"); Scanner sc = new Scanner(System.in); sc.next(); lock.notify(); System.out.println("t4 notify 后"); } }); t1.start(); t2.start(); t3.start(); t4.start(); } }
这次只是t1被唤醒了
还可以使用notifyAll,把全部的线程都唤醒