LockSupport解决了什么问题:LockSupport使用静态方法可以让线程在任意位置阻塞, 当然也可以重新唤醒
针对线程的阻塞和重新唤醒, 有很多种方法, 其中基础方式有以下几种(重入锁等高级封装方式不在此文考虑)
1.Object自有的wait和notify
但是这种方式使用起来比较麻烦,需要获取辅助对象(以下例子的lock对象)的监听器,并且notify为随机唤醒,而notifyAll则是唤醒所有辅助对象
public static void main(String[] args) throws InterruptedException { String lock = "111"; Thread t = new Thread(()->{ synchronized (lock){ try { //无限等待 直到被notify lock.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("线程执行完"); }, "测试线程"); t.start(); System.out.println("解锁"); synchronized (flag){ flag.notifyAll(); } System.out.println("完成解锁"); } 执行结果: 解锁 完成解锁 线程执行完
2.使用Thread的挂起和唤醒方法: Thread.suspend() 挂起,Thread.resume() 唤醒,目前JDK注解为弃用, 但是如果无法保证resume()在suspend()之后执行,则线程将永远处于挂起状态,如果挂起线程中有同步模块,
后果很严重,被锁定的对象也将永远被释放
//错误示例: 下面模拟的是一个主线程被挂起的代码 public static void main(String[] args) throws InterruptedException { Object lockObject = new Object(); Thread t1=new Thread(()->{ System.out.println("t1 准备挂起"); try { //模拟一个3秒的业务执行 Thread.sleep(3000L); } catch (InterruptedException e) { e.printStackTrace(); } Thread.currentThread().suspend(); System.out.println("t1 结束等待"); },"t1"); t1.start(); //由于挂起之前模拟了一个3秒的等待 会导致唤醒先执行,挂起后执行,最终永远挂起 t1.resume(); } 结果: t1 准备挂起
3 使用LockSupport的静态方法进行 阻塞park() 和 唤醒unPark(Thread), 使用场景为可以获取线程对象时使用, 从方法unPark(Thread)很容易看出, 线程需要被其他线程(包括主线程)持有时才能解锁
public static void main(String[] args) throws InterruptedException { Object lockObject = new Object(); Thread t1=new Thread(()->{ System.out.println("t1 准备挂起"); try { //模拟一个3秒的业务执行 Thread.sleep(3000L); } catch (InterruptedException e) { e.printStackTrace(); } LockSupport.park(); System.out.println("t1 结束等待"); },"t1"); t1.start(); LockSupport.unpark(t1); System.out.println("挂起前就解锁"); } 结果: t1 准备挂起 挂起前就解锁 t1 结束等待
从结果可以看出LockSupport先执行解锁然后执行唤醒, 不会影响最终的执行流程,虽然park()方法后执行 但是任然可以唤醒
接下来再看一段代码
public static void main(String[] args) throws InterruptedException { Object lockObject = new Object(); Thread t1=new Thread(()->{ System.out.println("t1 准备挂起"); try { //模拟一个3秒的业务执行 Thread.sleep(3000L); } catch (InterruptedException e) { e.printStackTrace(); } LockSupport.park(); System.out.println("第一次被唤醒"); LockSupport.park(); System.out.println("第二次被唤醒"); },"t1"); t1.start(); LockSupport.unpark(t1); LockSupport.unpark(t1); System.out.println("挂起前就解锁"); } 结果: t1 准备挂起 挂起前就解锁 第一次被唤醒
从执行结果可以看到 提前执行两次unpark(t1), 但是只有第一个park()被唤醒, 第二个park()依旧阻塞线程, 接着我们思考若是两个park()都在unpark(t1)之前执行又将是怎样结果, 继续看代码
public static void main(String[] args) throws InterruptedException { Object lockObject = new Object(); Thread t1=new Thread(()->{ System.out.println("t1 准备挂起"); LockSupport.park(); System.out.println("第一次被唤醒"); LockSupport.park(); System.out.println("第二次被唤醒"); },"t1"); t1.start(); //保证t1第一次挂起 再解锁 Thread.sleep(1000L); LockSupport.unpark(t1); //保证t1第一次挂起 再解锁 Thread.sleep(1000L); LockSupport.unpark(t1); System.out.println("挂起前就解锁"); } 结果: t1 准备挂起 第一次被唤醒 挂起前就解锁 第二次被唤醒
上面代码执行顺序为 挂起->解锁->唤醒 ->挂起->解锁->唤醒
LockSupport原理: park消费一个许可, unpark提供一个许可, 通过信号量来判断是否继续, 若是这个许可未被消费, 则unpark时会检查是否许可是否存在,若存在则跳过提供许可, 不存在则提供一个,
相同原理, park消费许可, 若存在许可则消费掉, 并继续往下执行, 若不存在许可则阻塞当前线程直到许可提供为止
注:若是需要线程间通信,相较于使用object.wait和notity, 使用基于LockSupport实现的 Reentrantlock Condition更方便, 功能更强, 属于多线程的高级应用,后续Reentrantlock将会讲到
总结: 1 可以持有阻塞线程对象时使用LockSupport 编码量更低, 使用更简便
2 无法持有阻塞线程对象时, 则需要通过共享对象作为阻塞和唤醒对象, 则使用 wait和notify 方式更合理
3 LockSupport效率更高, 在park时检测是否有许可判定是否继续往下执行, unpark时则判定线程是否被挂起,因为持有线程对象,只需要将线程重新唤醒, 相比CPU自旋方式显然效率上更优