LockSupport源码阅读

发布时间 2023-12-23 01:19:39作者: NOSAE

LockSupport源码阅读

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

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

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

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

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方法实现。

最后,文档给了一个用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,会导致多个线程都获取到锁的局面。

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

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