JDK java.util.concurrent.locks.LockSupport

博文目录


java.util.concurrent.locks.LockSupport

LockSupport:一个很灵活的线程工具类
为什么说LockSupport是Java并发的基石?

since 1.6

当需要阻塞或唤醒一个线程的时候,JVM都会使用LockSupport工具类来完成相应工作。LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也被称为构建同步组件的基础工具。并发组件和并发工具大都是基于AQS来实现的。而AQS中的控制线程又是通过LockSupport类来实现的,因此可以说,LockSupport是Java并发基础组件中的基础组件。

并发组件:线程池、阻塞队列、Future和FutureTask、Lock和Condition。
并发工具:CountDownLatch、CyclicBarrier、Semaphore和Exchanger。

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。

LockSupport 是一个线程工具类,所有的方法都是静态方法,可以让线程在任意位置阻塞,也可以在任意位置唤醒。LockSupport 提供 park() 和 unpark() 方法实现阻塞线程和解除线程阻塞,实现的阻塞和解除阻塞是基于”许可(permit)”作为关联, permit相当于一个信号量(0,1), 默认是0. 线程之间不再需要一个Object或者其它变量来存储状态, 不再需要关心对方的状态

类结构

在这里插入图片描述

类注释

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

此类以及每个使用它的线程与一个许可关联(从 Semaphore 类的意义上说)。如果该许可可用,并且可在进程中使用,则调用 park 将立即返回;否则可能 阻塞。如果许可尚不可用,则可以调用 unpark 使其可用。(但与 Semaphore 不同的是,许可不能累积,并且最多只能有一个许可。)

park 和 unpark 方法提供了阻塞和解除阻塞线程的有效方法,并且不会遇到导致过时方法 Thread.suspend 和 Thread.resume 因为以下目的变得不可用的问题:由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。此外,如果调用者线程被中断,并且支持超时,则 park 将返回。park 方法还可以在其他任何时间“毫无理由”地返回,因此通常必须在重新检查返回条件的循环里调用此方法。从这个意义上说,park 是“忙碌等待”的一种优化,它不会浪费这么多的时间进行自旋,但是必须将它与 unpark 配对使用才更高效。

三种形式的 park 还各自支持一个 blocker 对象参数。此对象在线程受阻塞时被记录,以允许监视工具和诊断工具确定线程受阻塞的原因。(这样的工具可以使用方法 getBlocker(java.lang.Thread) 访问 blocker。)建议最好使用这些形式,而不是不带此参数的原始形式。在锁实现中提供的作为 blocker 的普通参数是 this。

这些方法被设计用来作为创建高级同步实用工具的工具,对于大多数并发控制应用程序而言,它们本身并不是很有用。park 方法仅设计用于以下形式的构造:

while (!canProceed()) { ... LockSupport.park(this); }

在这里,在调用 park 之前, canProceed 和其他任何动作都不会锁定或阻塞。因为每个线程只与一个许可关联, park 的任何中间使用都可能干扰其预期效果。
示例用法。 以下是一个先进先出 (first-in-first-out) 非重入锁类的框架。

class FIFOMutex {
   private final AtomicBoolean locked = new AtomicBoolean(false);
   private final Queue<Thread> waiters
     = new ConcurrentLinkedQueue<Thread>();

   public void lock() {
     boolean wasInterrupted = false;
     Thread current = Thread.currentThread();
     waiters.add(current);

     // Block while not first in queue or cannot acquire lock
     while (waiters.peek() != current ||
            !locked.compareAndSet(false, true)) {
        LockSupport.park(this);
        if (Thread.interrupted()) // ignore interrupts while waiting
          wasInterrupted = true;
     }

     waiters.remove();
     if (wasInterrupted)          // reassert interrupt status on exit
        current.interrupt();
   }

   public void unlock() {
     locked.set(false);
     LockSupport.unpark(waiters.peek());
   }
 }

类源码

/**
除非许可可用,否则禁用当前线程以进行线程调度。 
如果许可可用,则使用它并立即返回调用;否则当前线程出于线程调度目的而被禁用并处于休眠状态,直到发生以下三种情况之一: 
- 其他一些线程以当前线程为目标调用 {@link unpark unpark};或
- 某个其他线程 {@linkplain Threadinterrupt interrupts} 当前线程;或
- 调用虚假(即无缘无故)返回。
这个方法确实不报告是哪一个导致方法返回。调用者应该重新检查导致线程首先停止的条件。例如,调用者还可以确定线程在返回时的中断状态。 
*/
public static void park();
// park + 超时时间
public static void parkNanos(long nanos);
// park + 截止时间
public static void parkUntil(long deadline);
// blocker, 用来记录线程被阻塞时被谁阻塞的。用于线程监控和分析工具来定位原因的。
public static void park(Object blocker);
public static void parkNanos(Object blocker, long nanos)
public static void parkUntil(Object blocker, long deadline)

/**
使给定线程的许可证可用(如果它尚不可用)。如果线程在 {@code park} 上被阻塞,那么它将解除阻塞。否则,它对 {@code park} 的下一次调用保证不会阻塞。如果给定线程尚未启动,则不保证此操作有任何效果。
@param thread 要取消驻留的线程,或 {@code null},在这种情况下,此操作无效
*/
public static void unpark(Thread thread);

// 返回提供给尚未解除阻塞的公园方法的最近调用的阻塞器对象,如果未阻塞,则返回 null。返回的值只是一个瞬间快照——线程可能已经解除阻塞或阻塞在不同的阻塞对象上。
public static Object getBlocker(Thread t);
// 内部调用
private static void setBlocker(Thread t, Object arg);

核心方法

// sun.misc.Unsafe

/**
除非许可可用,否则禁用当前线程以进行线程调度。 
如果许可可用,则使用它并立即返回调用;否则当前线程出于线程调度目的而被禁用并处于休眠状态,直到发生以下三种情况之一:
- 其他一些线程以当前线程为目标调用 {@link unpark unpark};或 
- 某个其他线程 {@linkplain Threadinterrupt interrupts} 当前线程;或 
- 经过指定的等待时间(isAbsolute=false, 相对时间, 0表示永远等待不会过期);或 
- 指定的截止日期已过(isAbsolute=true, 绝对时间);或 
- 调用虚假(即无缘无故)返回。 
这个方法确实不报告是哪一个导致方法返回。调用者应该重新检查导致线程首先停止的条件。例如,调用者还可以确定线程在返回时的中断状态。

@param isAbsolute 是否是绝对时间, false:阻塞相对时间长度, true:阻塞到绝对时间点
@param time 时间, 单位纳秒
*/
public native void park(boolean isAbsolute, long time);

/**
使给定线程的许可证可用(如果它尚不可用)。如果线程在 {@code park} 上被阻塞,那么它将解除阻塞。否则,它对 {@code park} 的下一次调用保证不会阻塞。如果给定线程尚未启动,则不保证此操作有任何效果。

多次 unpark 并不会叠加次数, 最多生效1次

@param thread 要取消驻留的线程,或 {@code null},在这种情况下,此操作无效
*/
public native void unpark(Thread thread);

park()和unpark()提供了类似wait()和notify()的机制,但是并不用获得对象的监视器,而是获得许可,park()就是堵塞,挂起,直到有许可可用。unpark()就是发放许可。

unpark()可以在park()之前执行,先发放许可,再消费许可,假如有许可, 则 park() 直接返回,并不堵塞,假如没有许可的话则会阻塞线程,这个许可是直接和线程关联的,并且一个线程只能有一个许可

原理

C++层面上, 每个java线程都有一个Parker实例, 该实例有一个属性 _counter, 用来记录所谓的 “许可”, 只有 0:无, 1:有 两种状态

class Parker : public os::PlatformParker {
    
    
private:
  volatile int _counter ;
  ...
public:
  void park(bool isAbsolute, jlong time);
  void unpark();
  ...
}
class PlatformParker : public CHeapObj<mtInternal> {
    
    
  protected:
    pthread_mutex_t _mutex [1] ;
    pthread_cond_t  _cond  [1] ;
    ...
}

park, 总结来说就是消耗“许可”的过程

  • 当调用park时,先尝试直接能否直接拿到“许可”,即_counter>0时,如果成功,则把_counter设置为0,并返回。
  • 如果不成功,则构造一个ThreadBlockInVM,然后检查_counter是不是>0,如果是,则把_counter设置为0,unlock mutex并返回
  • 否则,再判断等待的时间,然后再调用pthread_cond_wait函数等待,如果等待返回,则把_counter设置为0,unlock mutex并返回

unpark

  • 直接设置_counter为1,再unlock mutext返回。如果_counter之前的值是0,则还要调用pthread_cond_signal唤醒在park中等待的线程

park / unpark 可以随意调整调用的先后顺序

park/unpark与wait/notify对比

  • wait和notify都是Object中的方法, 在调用这两个方法前必须先获得锁对象,但是park不需要获取某个对象的锁就可以锁住线程。
  • notify只能随机选择一个线程唤醒(?, 我记得好像是等待队列的队首线程, 不应该被理解成随机线程),无法唤醒指定的线程,unpark却可以唤醒一个指定的线程。

举例

package jdk.java.util.concurrent.locks;

import com.mrathena.toolkit.ThreadKit;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.locks.LockSupport;

@Slf4j
public class LockSupportTest {
    
    

	public static void main(String[] args) {
    
    
		Thread boy = new Thread(() -> {
    
    
			log.info("想打游戏");
			LockSupport.park();
//			LockSupport.parkNanos(1000000000L);
			log.info("感谢");
			ThreadKit.sleep(5L);
			log.info("打完了");
			ThreadKit.sleep(1L);
			log.info("队友又喊我了, 那我直接上了啊");
			LockSupport.park(Thread.currentThread());
			ThreadKit.sleep(1L);
			log.info("搞定了");
		}, "boy");
		Thread girl = new Thread(() -> {
    
    
			ThreadKit.sleep(2L);
			log.info("同意");
			LockSupport.unpark(boy);
			ThreadKit.sleep(1L);
			log.info("下次想打就打, 不用问我了");
			LockSupport.unpark(boy);
		}, "girl");
		boy.start();
		girl.start();
	}

}
[20220507.103458.599][INFO ][boy                  ] 想打游戏
[20220507.103500.611][INFO ][girl                 ] 同意
[20220507.103500.611][INFO ][boy                  ] 感谢
[20220507.103501.621][INFO ][girl                 ] 下次想打就打, 不用问我了
[20220507.103505.624][INFO ][boy                  ] 打完了
[20220507.103506.632][INFO ][boy                  ] 队友又喊我了, 那我直接上了啊
[20220507.103507.643][INFO ][boy                  ] 搞定了

案例

JDK并发包下的锁和其他同步工具的底层实现中大量使用了LockSupport进行线程的阻塞和唤醒,掌握它的用法和原理可以让我们更好的理解锁和其它同步工具的底层实现。

AQS抽象队列同步器, 以及基于此的并发工具等

猜你喜欢

转载自blog.csdn.net/mrathena/article/details/124625520