介绍:

LockSupport是一个线程阻塞工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。

接下面我来看看LockSupport有哪些常用的方法。主要有两类方法:parkunpark

// 暂停当前线程
public static void park(Object blocker); 

// 暂停当前线程,不过有超时时间的限制
public static void parkNanos(Object blocker, long nanos); 

// 暂停当前线程,直到某个时间
public static void parkUntil(Object blocker, long deadline); 

// 无期限暂停当前线程
public static void park(); 

// 暂停当前线程,不过有超时时间的限制
public static void parkNanos(long nanos); 

// 暂停当前线程,直到某个时间
public static void parkUntil(long deadline); 

// 恢复当前线程
public static void unpark(Thread thread); 

public static Object getBlocker(Thread t);

private static void setBlocker(Thread t, Object arg)

park英文意思为停车。我们如果把Thread看成一辆车的话,park就是让车停下,unpark就是让车启动然后跑起来。

park函数是将当前调用Thread阻塞,而unpark函数则是将指定线程Thread唤醒。与Object类的wait/notify机制相比,park/unpark有两个优点:

  • 以thread为操作对象更符合阻塞线程的直观定义

  • 操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性。

每个线程都有一个parkBlocker对象,对象声明为volatile,这个blocker是用来记录线程被阻塞时被谁阻塞的,用于线程监控和分析工具来定位原因。

官方的解释:

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。

  • LockSupport类使用了Permit(许可证)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零,默认是零。

  • 许可看成是一种(0和1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1

  • 通过park()和unpark(thread)方法来实现阻塞和唤醒线程的操作,此外,park()+park(Object blocker) 可以用来阻塞当前线程阻塞传入的具体线程

@Test
@SneakyThrows
@DisplayName("线程阻塞唤醒测试")
public void test(){
    Object u = new Object();
    Runnable r = () -> {
            System.out.println("进入:" + Thread.currentThread().getName());
            LockSupport.park();
            if(Thread.currentThread().isInterrupted()){
                System.out.println(Thread.currentThread().getName()+"被中断了!");
            }
            System.out.println(Thread.currentThread().getName()+"继续执行!");
    };
    Thread a = new Thread(r,"a");
    Thread b = new Thread(r,"b");

    a.start();
    TimeUnit.SECONDS.sleep(1);
    b.start();
    TimeUnit.SECONDS.sleep(3);
    a.interrupt();
    LockSupport.unpark(b);
    a.join();
    b.join();
}
---------

这儿parkunpark其实实现了waitnotify的功能,不过还是有一些差别的。

  1. park不需要获取某个对象的锁,wait和notify是必须在synchronized先获取到锁对象后执行

  2. 因为中断的时候park不会抛出InterruptedException异常,所以需要在park之后自行判断中断状态,然后做额外的处理

我们再来看看Object blocker,这是个什么东西呢?这其实就是方便在线程dump的时候看到具体的阻塞对象的信息。

"t1" #10 prio=5 os_prio=31 tid=0x00007f95030cc800 nid=0x4e03 waiting on condition [0x00007000011c9000]
   java.lang.Thread.State: WAITING (parking)
    at sun.misc.Unsafe.park(Native Method)
    at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
    // `下面的这个信息`
    at com.wtuoblist.beyond.concurrent.demo.chapter3.LockSupportDemo$ChangeObjectThread.run(LockSupportDemo.java:23) // 
    - locked <0x0000000795830950> (a java.lang.Object)

还有一个地方需要注意,相对于线程的stop和resumepark和unpark的先后顺序并不是那么严格。stop和resume如果顺序反了,会出现死锁现象。而park和unpark却不会。这又是为什么呢?还是看一个例子

@Test
@SneakyThrows
@DisplayName("线程阻塞唤醒测试")
public void test(){
    Object u = new Object();
    Runnable r = () -> {
            System.out.println("进入:" + Thread.currentThread().getName());
            LockSupport.park();
            if(Thread.currentThread().isInterrupted()){
                System.out.println(Thread.currentThread().getName()+"被中断了!");
            }
            System.out.println(Thread.currentThread().getName()+"继续执行!");
    };
    Thread a = new Thread(r,"a");
    Thread b = new Thread(r,"b");

    
    TimeUnit.SECONDS.sleep(1);
    b.start();
    TimeUnit.SECONDS.sleep(3);
    LockSupport.unpark(b);
    a.start();
}

t1内部有休眠1s的操作,所以unpark肯定先于park的调用,但是t1最终仍然可以完结。这是因为park和unpark会对每个线程维持一个许可(boolean值)

  1. unpark调用时,如果当前线程还未进入park,则许可为true

  2. park调用时,判断许可是否为true,如果是true,则继续往下执行;如果是false,则等待,直到许可为true

总结一下

  1. park和unpark可以实现类似wait和notify的功能,但是并不和wait和notify交叉,也就是说unpark不会对wait起作用,notify也不会对park起作用。

  2. park和unpark的使用不会出现死锁的情况

  3. blocker的作用是在dump线程的时候看到阻塞对象的信息

LockSupport面试题

为什么可以先唤醒线程后阻塞线程?

因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

因为凭证的数量最多为1(不能累加),连续调用两次 unpark和调用一次 unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。

底层使用了Unsafe.park和Unsafe.unpark的底层实现原理

在Linux系统下,是用的Posix线程库pthread中的mutex(互斥量),condition(条件变量)来实现的。

mutex和condition保护了一个_counter的变量,当park时,这个变量被设置为0,当unpark时,这个变量被设置为1。

concurrent包是基于AQS (AbstractQueuedSynchronizer)框架的,AQS框架借助于两个类:

Unsafe(提供CAS操作)

LockSupport(提供park/unpark操作)

因此,LockSupport非常重要。

park函数是将当前调用Thread阻塞,而unpark函数则是将指定线程Thread唤醒。

文章作者: 刘同学
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 刘同学的小站
后端 Java
喜欢就支持一下吧