第一种:Object类中的wait和notify方法实现线程的等待和唤醒
下面标黄字的部分就是对一下两点总结的实现:
- 不能脱离synchronized代码块使用,否则会抛出IllegalMonitorStateException异常
- 先wait后notify、notifyAll,等待中的线程才能被唤醒,顺序不能改变
演示代码:
/** * @author zhangzhixi * @date 2021-5-23 18:21 */ public class LockDemo { static Object object = new Object(); public static void main(String[] args) { new Thread(() -> { synchronized (object) { System.out.println(Thread.currentThread().getName() + "==>线程Come in"); try { // 使当前线程等待 object.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + "==>线程被唤醒"); } }, "A").start(); new Thread(() -> { synchronized (object) { System.out.println(Thread.currentThread().getName() + "==>通知线程"); // 使当前线程唤醒 object.notifyAll(); } }, "B").start(); } }
我们来看下上面代码的执行结果,看起来很和谐:
我们来将上面代码动下手脚,注释掉10、23代码看下wait/notify脱离了Synchronized会出现什么?
可以看到报了异常,说明wait/notify不能够脱离Synchronized代码块
我们再来看一下使线程A暂停三秒会发生什么?
线程B先执行了,没有代码将线程A的wait状态进行唤醒
第二种:Condition接口中的await和single方法实现线程的等待和唤醒
- 必须配合lock()方法使用,否则抛出IllegalMonitorStateException异常
- 等待唤醒调用顺序不能改变
代码实现:
public class LockDemo { static Object object = new Object(); // 创建可重入锁 static Lock lock = new ReentrantLock(); static Condition condition = lock.newCondition(); public static void main(String[] args) { new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "==>线程Come in"); // 使当前线程等待 condition.await(); } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } System.out.println(Thread.currentThread().getName() + "==>线程被唤醒"); }, "A").start(); new Thread(() -> { lock.lock(); try { System.out.println(Thread.currentThread().getName() + "==>通知线程"); // 使当前线程唤醒 condition.signal(); } catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } }, "B").start(); } }
看下上面代码的执行结果:
我们来将上面代码动下手脚,注释掉9 17 24 32行代码看下wait/notify脱离了Synchronized会出现什么?
可以看到报了异常,说明wait/notify不能够脱离Synchronized代码块
我们再来看一下使线程A暂停三秒会发生什么?
线程B先执行了,没有代码将线程A的await状态进行唤醒
第三种:LockSupport类中的park等待和unpark唤醒
LockSupport提供park()和unpark()方法实现阻塞和唤醒线程的过程
LockSupport和每一个使用它的线程之间有一个许可(permit)进行关联,permit有0和1两种状态,默认为0,即无许可证状态。
调用一次unpark方法,permit加1变成1。每次调用park方法都会检查许可证状态,如果为1,则消耗掉permit(1 -> 0)并立刻返回;如果为0,则进入阻塞状态。permit最多只有一个,重复调用unpark也不会累积permit。
/** * @author zhangzhixi * @date 2021-5-23 18:21 */ public class LockDemo { static Object object = new Object(); // 创建可重入锁 static Lock lock = new ReentrantLock(); static Condition condition = lock.newCondition(); public static void main(String[] args) { Thread a = new Thread(() -> { System.out.println(Thread.currentThread().getName() + "==>Come in"); LockSupport.park();//阻塞当前线程 System.out.println(Thread.currentThread().getName() + "==>被唤醒"); }); a.setName("a"); a.start(); new Thread(() -> { LockSupport.unpark(a); System.out.println(Thread.currentThread().getName() + "==>通知"); }, "B").start(); } }
看下上面代码的执行结果:
下面给线程A进行等待3S,看会不会像一,二两个例子出现错误情况:
得出结论是:
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。
总结LockSupport:
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语
LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。归根
结底,LockSupport调用的Unsafe中的native代码。
LockSupport提供park()和unpark()方法实现阻塞线程和解除线程阻塞的过程
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,
调用一次unpark就加1变成1,
调用一次park会消费permit,也就是将1变成o,同时park立即返回。
如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。
每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。
形象的理解
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。
当调用park方法时
*如果有凭证,则会直接消耗掉这个凭证然后正常退出;
*如果无凭证,就必须阻塞等待凭证可用;
而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
面试题
为什么可以先唤醒线程后阻塞线程?
因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;
而调用两次park却需要消费两个凭证,证不够,不能放行。