Loading

LockSupport源码阅读

本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习

本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,否则你会不知道我在说什么

简介

LockSupport用来创建锁和其他同步类的基本线程阻塞原语。相比mutex这个概念来说,LockSupport更像是信号量,不过与信号量最大区别在于LockSupport的"资源计数"最大只有1,叫做 许可证,下面直接介绍两个核心方法park和unpark,其他的方法基本都是基于这两个方法重载或扩展而来。

例子

知其所以然之前先得知其然,先看下文档给出的用LockSupport实现先入先出锁(不可重入)的例子:

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

  public void lock() {
    boolean wasInterrupted = false;      // publish current thread for unparkers
    waiters.add(Thread.currentThread());        // Block while not first in queue or cannot acquire lock
    while (waiters.peek() != Thread.currentThread() || !locked.compareAndSet(false, true)) {
      LockSupport.park(this); // ignore interrupts while waiting
      if (Thread.interrupted())
        wasInterrupted = true;
    }
    waiters.remove();      // ensure correct interrupt status on return
    if (wasInterrupted)
      Thread.currentThread().interrupt();
  }

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

  }

  static {
    // Reduce the risk of "lost unpark" due to classloading
    Class<?> ensureLoaded = LockSupport.class;
  }
}

可以看到lock方法遵循了文档提到的使用规范:

while (!canProceed()) {
  // ensure request to unpark is visible to other threads    ...    
  LockSupport.park(this);  
}

解释下这段代码:canProceed在FIFOMutex这里的含义就是,你必须是队首元素,才能有资格去获取锁。其次即使你是队首线程,如果锁已经被别人拿走了,也没办法拿到锁,只能老老实实park。这样,第一个条件就实现了FIFO的语义,第二个条件实现了Mutex。

释放锁的时候,得先设置共享变量locked为false,表明自己已经释放锁了,再去唤醒队首线程,让他去获取锁。

最后,这个FIFOMutex只是作为一个小例子,其本身是不完善的,比如没有对unlock加以限制,所有线程都能在不lock的情况下unlock,会导致多个线程都获取到锁的局面。

从这个例子中也看到LockSupport的一个好处:通过unpark传入的thread可以精确唤醒线程。

代码分析

LockSupport源码其实没什么好分析的,之前也说了LockSupport提供的是基本线程阻塞原语,因此点进方法源码里基本就是对native方法的简单封装。

park

public static void park() {
    U.park(false, 0L);
}

如果当前LockSupport没有许可证,park将会阻塞线程,否则park不会阻塞直接返回,这就好比信号量为0时想获取信号量的情况。park是通过Unsafe.park实现的,Unsafe.park是一个native方法,传入的参数决定了park是否有限等待,第一个参数指明第二个参数是否是绝对时间戳。如果对被阻塞在park的线程调用interrupt,会导致park立刻返回,此时可以通过线程的isInterrupted来知道park是因为unpark还是因为interrupt返回了。

park还有其他的一些重载方法以及像parkUntil这样的方法,就不说了。值得一提的是park的一个重载:

public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    U.park(false, 0L);
    setBlocker(t, null);
}

里面的setBlocker方法就是将传入的blocker赋值到Thread.parkBlocker成员变量上,等park完之后再将Thread.parkBlocker置空。所以这个blocker有什么用呢?我没找到实际的使用案例,但是根据文档所说:

This object is recorded while the thread is blocked to permit monitoring and diagnostic tools to identify the reasons that threads are blocked.....The normal argument to supply as a blocker within a lock implementation is this.

These methods are designed to be used as tools for creating higher-level synchronization utilities, and are not in themselves useful for most concurrency control applications. The park method is designed for use only in constructions of the form:

while (!canProceed()) {
  // ensure request to unpark is visible to other threads    ...    
  LockSupport.park(this);  
}

大概翻译一下意思是,这个blocker是用来在线程被park阻塞后,被unpark唤醒前,可以通过getBlocker来获取被阻塞线程的具体阻塞原因。至于LockSupport其实不是给应用程序使用的,而是用来实现更高级的同步工具的(JUC的好几个同步工具类都有LockSupport身影)。比如你要用它来实现Lock的话,一般是给park传入this,也就是你的Lock类本身(至于为什么这样做暂时还不清楚)。文档给出了使用park的范式代码:

while (!canProceed()) {
  // ensure request to unpark is visible to other threads    ...    
  LockSupport.park(this);  
}

unpark

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

unpark传入一个线程,意思是将许可证"分发"给这个线程,被unpark过的线程相当于拥有了许可证(但是连续多次unpark最多都只有一个许可证),之后这个线程进行一次park的话就不会阻塞了,但是会消耗掉这个许可证,因此之后再park的话就会重新被阻塞。同样unpark也是借助Unsafe.unpark这个native方法实现。

TODO:最后的static块没看懂,有大佬知道这是哪块知识的话麻烦告知下

TODO:分析LockSupport在JUC中中的使用

posted @ 2023-12-23 01:18  NOSAE  阅读(13)  评论(0编辑  收藏  举报