线程阻塞工具类:LockSupport
LockSupport是一个非常方便实用的线程阻塞工具,它可以在线程内任意位置让线程阻塞。和Thread.suspend()相比,它弥补了由于resume()在前发生,导致线程无法继续执行的情况;和 Object.wait()相比,它不需要先获取某个对象的锁,也不会抛出InterruptedException异常。
LockSupport接口提供的方法如下:
1 public static void park(); 2 public static void parkNanos(long nanos); 3 public static void parkUntil(long deadline); 4 public static void park(Object blocker); 5 public static void parkNanos(Object blocker, long nanos); 6 public static void parkUntil(Object blocker, long deadline); 7 public static void unpark(Thread thread);
以上方法的含义如下:
❤ park():通过调用本地(Native)方法来阻塞线程;
❤ parkNanos(long nanos):阻塞当前线程,最长不超过nanos纳秒;
❤ parkUntil(long deadline):阻塞当前线程,直到deadline;
❤ park(Object blocker):阻塞当前线程,记录当前等待对象blocker;
❤ parkNanos(Object blocker,long nanos) 和 parkUntil(Object blocker,long deadline) 和上面签名一样的方法一致,只是多了记录等待的对象;
❤ unpark(Thread thread):唤醒阻塞的线程;
下面举例说明LockSupport的用法:
1 public class LockSupportDemo { 2 public static Object u = new Object(); 3 static ChangeObjectThread t1 = new ChangeObjectThread("t1"); 4 static ChangeObjectThread t2 = new ChangeObjectThread("t2"); 5 6 public static class ChangeObjectThread extends Thread{ 7 public ChangeObjectThread(String name){ 8 super.setName(name); 9 } 10 11 @Override 12 public void run() { 13 synchronized (u){ 14 System.out.println(" 阻塞线程: " + getName() + "时间:" + System.currentTimeMillis()); 15 LockSupport.park(); 16 System.out.println(" 唤醒线程: " + getName() + "时间:" + System.currentTimeMillis()); 17 } 18 } 19 } 20 //测试 21 public static void main(String[] args) throws InterruptedException { 22 t1.start(); 23 t2.start(); 24 Thread.sleep(1000); 25 LockSupport.unpark(t1); 26 LockSupport.unpark(t2); 27 t1.join(); 28 t2.join(); 29 } 30 }
输出结果:
阻塞线程: t1时间:1537924774519 唤醒线程: t1时间:1537924775518 阻塞线程: t2时间:1537924775518 唤醒线程: t2时间:1537924775518
注意:
上面代码我们无法保证unpark()发生在park()方法之后,也就是说,有可能先执行unpark(),再执行park();但是运行上述代码,你会发现,代码都会运行成功,不会因为park()方法而导致线程永久性的挂起。
原因:
这是因为LockSupport类使用了类似信号量的机制。它为每一个线程准备了一个许可,如果许可可用,那么park()函数会立即返回,并且消费这个许可(也就是将许可变为不可用),如果许可不可用,就会阻塞。而unpark()则使得一个许可变为可用(但是和信号量不同的是,许可不能累加,你不能拥有超过一个许可,也就是说一个线程最多只能拥有一个许可),这个特点使得:即使unpark()发生在park()之前,它也可以使下一次的park()操作立即返回;因为执行完unpark()方法后,许可是一定可用的,那么再执行park()方法,park()方法就会立即返回,唤醒线程,这点从上述结果t2的时间戳也可以看出,唤醒和阻塞都是同一时间。
LockSupport的优点:
❤ 即使unpark()发生在park()方法之前,LockSupport也会在下一次的park()方法立即返回,不会导致线程挂起;
❤ 处于park()方法挂起的线程的状态是WAITTING,还会标注是park()引起的,这对代码的调试是非常重要的,不会像suspend()方法给出一个令人费解的Runnable状态;
❤ 可以使用park(Object blocker)方法,这样可以为当前线程设置一个阻塞对象,并且这个阻塞对象会出现在线程Dump中,分析问题,就会更加方便;
❤ 有定时阻塞和阻塞时间自由决定,这样对线程阻塞控制非常灵活;
❤ LockSupport.park()支持中断影响,但是不会抛出InterruptedException异常,它会默默的返回,但可以从Thread.interrupted()方法等获得这个中断标记;
下面举例看一下LockSupport对中断的支持:
1 public class LockSupportInt { 2 public static Object u = new Object(); 3 static ChangeThread t1 = new ChangeThread("t1"); 4 static ChangeThread t2 = new ChangeThread("t2"); 5 6 public static class ChangeThread extends Thread{ 7 public ChangeThread(String name){ 8 super.setName(name); 9 } 10 11 @Override 12 public void run() { 13 synchronized (u){ 14 System.out.println("in " + getName()); 15 LockSupport.park(); 16 if (Thread.interrupted()){ 17 System.out.println(getName() + "被中断了!"); 18 } 19 } 20 System.out.println(getName() + "执行结束!"); 21 } 22 } 23 //测试 24 public static void main(String[] args) throws InterruptedException { 25 t1.start(); 26 Thread.sleep(1000); 27 t2.start(); 28 t1.interrupt(); 29 LockSupport.unpark(t2); 30 } 31 }
输出结果:
in t1
t1被中断了!
t1执行结束!
in t2
t2执行结束!
上述代码在第28行,中断了处于park()状态的t1。t1响应了这个中断,并且返回。之后在外面等待对象u锁的t2才进入临界区,并由代码第29行,唤醒t2,使其操作结束。这点在输出结果中也可以看出。
挂起(suspend)和继续执行(resume)
这两个方法也是对线程进行阻塞和唤醒的,目前所有的IDE都已不推荐使用。下面就简单介绍下这两个方法的缺点,即为什么不推荐使用:
①suspend()方法在挂起线程的同时,不会释放任何资源,这样导致如果其他线程想要访问它占用的锁资源时,都会被阻塞;
②被suspend()方法挂起的线程的状态是RUNNABLE,这会对我们排查系统的问题时,造成很大的迷惑性,不利于排查问题;
③线程挂起suspend()和线程唤醒resume()方法必须严格控制顺序,即先挂起,再唤醒。若反之,则会导致线程永远挂起;
将上述演示LockSupport的代码换为这两个方法,就会看出问题所在:
1 public class LockSupportDemo { 2 public static Object u = new Object(); 3 static ChangeObjectThread t1 = new ChangeObjectThread("t1"); 4 static ChangeObjectThread t2 = new ChangeObjectThread("t2"); 5 6 public static class ChangeObjectThread extends Thread{ 7 public ChangeObjectThread(String name){ 8 super.setName(name); 9 } 10 11 @Override 12 public void run() { 13 synchronized (u){ 14 System.out.println(" 阻塞线程: " + getName() + "时间:" + System.currentTimeMillis()); 15 // LockSupport.park(); 16 Thread.currentThread().suspend(); 17 System.out.println(" 唤醒线程: " + getName() + "时间:" + System.currentTimeMillis()); 18 } 19 } 20 } 21 //测试 22 public static void main(String[] args) throws InterruptedException { 23 t1.start(); 24 t2.start(); 25 Thread.sleep(1000); 26 //LockSupport.unpark(t1); 27 //LockSupport.unpark(t2); 28 t1.resume(); 29 t2.resume(); 30 t1.join(); 31 t2.join(); 32 } 33 }
输出结果:
阻塞线程: t1时间:1537929982937 唤醒线程: t1时间:1537929983937 阻塞线程: t2时间:1537929983937
运行代码,发现并没有唤醒线程t2,导致线程t2永远挂起,并且程序不会退出,这就是由于resume()和suspend()方法调用顺序出错导致resume()方法并没有生效,并且t2永远占用了u对象,如果发生在系统中,这可能是致命的。
参考:《Java高并发程序设计》 葛一鸣 郭超 编著;