Java并发编程专题之LockSupport

LockSupport

概述

LockSupport是一个编程工具类,主要是为了阻塞和唤醒线程。它的所有方法都是静态方法,它可以让线程在任意位置阻塞,也可以在任意位置唤醒。

它可以在阻塞线程时为线程设置一个blocker,这个blocker是用来记录线程被阻塞时被谁阻塞的,用于线程监控和分析工具来定位原因。

LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的。

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
        System.out.println("线程开始执行");
        LockSupport.park();
        System.out.println("线程执行结束");
    });
    thread.start();
    TimeUnit.SECONDS.sleep(3);
    System.out.println("执行unpark");
    LockSupport.unpark(thread);
}

和wait/notify区别

  1. wait和notify都必须先获得锁对象才能调用,但是park不需要获取某个对象的锁就可以锁住线程。
  2. notify只能随机选择一个线程唤醒,无法唤醒指定的线程,unpark却可以唤醒一个指定的线程。

重要方法

这些方法都是调用Unsafe类的native方法

private static final sun.misc.Unsafe UNSAFE;

public final class Unsafe {
    public native void park(boolean isAbsolute, long time);
    public native void unpark(Thread jthread);
}

park(Object blocker)

setBlocker记录了当前线程是被blocker阻塞的,当线程在没有持有许可证的情况下调用park方法而被阻塞挂起时,这个blocker对象会被记录到该线程内部。使用诊断工具可以观察线程被阻塞的原因,诊断工具是通过调用getBlocker(Thread)方法来获取blocker对象的,所以推荐使用LockSupport.park(this);

如果调用park方法的线程已经拿到了与LockSupport关联的许可证,则调用LockSupport.park()时会马上返回,否则调用线程会被阻塞挂起。在其他线程调用unpark(Thread thread) 方法并且将当前线程作为参数时,调用park方法而被阻塞的线程会返回。另外,如果其他线程调用了阻塞线程的interrupt()方法,设置了中断标志或者被虚假唤醒,则阻塞线程也会返回。

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null); // 线程被激活后清除blocker变量
}
private static void setBlocker(Thread t, Object arg) {
    UNSAFE.putObject(t, parkBlockerOffset, arg);
}

unpark(Thread thread)

如果thread之前因调用park()而被挂起,则调用unpark后,该线程会被唤醒。

如果thread之前没有调用park,则让thread持有一个许可证,之后再调用park方法,则会立即返回。

public static void unpark(Thread thread) {
    if (thread != null) UNSAFE.unpark(thread);
}

parkNanos(Object blocker, long nanos)

如果没有拿到许可证,则阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回

public static void parkNanos(Object blocker, long nanos) {
    if (nanos > 0) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker);
        UNSAFE.park(false, nanos);
        setBlocker(t, null);
    }
}

parkUntil

阻塞当前线程,直到deadline;

public static void parkUntil(Object blocker, long deadline) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(true, deadline);
    setBlocker(t, null);
}

原理浅析

只讲部分重点,非重点代码略去了

概述

每个java线程都有一个Parker实例,Parker类定义:

class Parker {
private:
  volatile int _counter; // 记录许可
public:
  void park(bool isAbsolute, jlong time);
  void unpark();
}

LockSupport通过控制_counter进行线程的阻塞/唤醒,原理类似于信号量机制的PV操作,其中Semaphore初始为0,最多为1。

形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而unpark则相反,它会增加一个凭证,但凭证最多只能有1个。

_counter只能在0和1之间取值:当为1时,代表该类被unpark调用过,更多的调用,也不会增加_counter的值,当该线程调用park()时,不会阻塞,同时_counter立刻清零。当为0时, 调用park()会被阻塞。

  • 为什么可以先唤醒线程后阻塞线程?
    因为unpark获得了一个凭证,之后调用park因为有凭证消费,故不会阻塞。
  • 为什么唤醒两次后阻塞两次会阻塞线程。
    因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证。

park()

Java并发编程专题之LockSupport_第1张图片

  • 检查_counter是否大于零(之前调用过unpark),则通过原子操作将_counter设置为0。线程不用阻塞并返回。
  • 检查该线程是否有中断信号,如果有则清除该中断信号并返回(不抛出异常)。
  • 尝试通过pthread_mutex_trylock对_mutex加锁来达到线程互斥。
  • 检查park是否设置超时时间, 若设置了通过safe_cond_timedwait进行超时等待; 若没有设置,调用pthread_cond_wait进行阻塞等待。 这两个函数都在阻塞等待时都会放弃cpu的使用。 直到别的线程去唤醒它(调用pthread_cond_signal)。safe_cond_timedwait/pthread_cond_wait在执行之前肯定已经获取了锁_mutex, 在睡眠前释放了锁, 在被唤醒之前, 首先再去获取锁。
  • 将_counter设置为零
  • 通过pthread_mutex_unlock释放锁
void Parker::park(bool isAbsolute, jlong time) {
  if (Atomic::xchg(0, &_counter) > 0) return; // 调用过unpark
    
  if (Thread::is_interrupted(thread, false)) return; // 中断过
  
  // 对_mutex加锁
  if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
    	return;
  }
 
  // 进行超时等待或者阻塞等待,直到被signal唤醒
  if (time == 0) {
    status = pthread_cond_wait (&_cond[_cur_index], _mutex); 
  } else {
    status = os::Linux::safe_cond_timedwait (&_cond[_cur_index], _mutex, &absTime);
  }
  _counter = 0; // 唤醒后消耗掉这个凭证
  status = pthread_mutex_unlock(_mutex); // 解锁
}

unpark()

  • 首先获取锁_mutex。
  • 不管之前是什么值,都将_counter置为1,所以无论多少函数调用unpark(),都是无效的,只会记录一次。
  • 检查线程是否已经被阻塞了,阻塞则调用pthread_cond_signal唤醒
  • 最后释放锁_mutex。
void Parker::unpark() {
  status = pthread_mutex_lock(_mutex);   
  s = _counter;
  _counter = 1; // 将_counter置1
  if (s < 1)  status = pthread_cond_signal (&_cond[_cur_index]); // 进行线程唤醒
  pthread_mutex_unlock(_mutex);

你可能感兴趣的:(Java并发编程专题,多线程,java,面试,并发编程)