线程阻塞工具类: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高并发程序设计》 葛一鸣 郭超 编著;

 

posted on 2018-09-26 10:52  AoTuDeMan  阅读(357)  评论(0编辑  收藏  举报

导航