美文网首页
5.LockSupport核心原理分析

5.LockSupport核心原理分析

作者: 致虑 | 来源:发表于2020-09-24 09:40 被阅读0次

LockSupport核心原理分析

在前面分析AQS的时候,经常出现LockSupport.park(this);,LockSupport.unpark(node.thread);这类让线程等待唤醒状态的代码段,这类逻辑与线程的wait/notify功能类似,都是使当前线程等待/唤醒,但使用起来确实要比wait/notify方便的多。

LockSupport本身很简单,无非就是对线程进行等待唤醒,所以下面从一个简单的demo入手,进行分析一下。

public static void main(String[] args) {
  // 定义一个线程C,不干任何事情
  Thread c = new Thread(()->{},"线程C");

  // 定义一个线程B,执行一段输出后,自身进行等待
  Thread b = new Thread(()->{
    // 直接对线程C进行唤醒
    LockSupport.unpark(c);
    System.out.println(Thread.currentThread().getName() + ": 线程执行");
    System.out.println(Thread.currentThread().getName() + ": 阻塞");
    LockSupport.park();
    System.out.println(Thread.currentThread().getName() + ": 继续线程执行");
  },"线程B");

  // 定义一个线程C,自身等待一定时间后,对线程B进行唤醒
  Thread a  =new Thread(()->{
    try {
      System.out.println(Thread.currentThread().getName() + ": 线程执行");
      LockSupport.parkNanos(1*3000*3000*3000);
      System.out.println(Thread.currentThread().getName() + ": 线程继续执行");
      // 对线程B进行唤醒
      LockSupport.unpark(b);
    } catch (Exception e) {}
  },"线程A");

  c.start();
  b.start();
  a.start();
}

看下输出效果

线程B: 线程执行
线程B: 阻塞
线程A: 线程执行
线程A: 线程继续执行
线程B: 继续线程执行

从代码及输出效果来看,这里明显跟wait/notify有较明显的区别(优势)

1)不需要在同步代码块中即可执行park,unpark逻辑

2)park与unpark不用成对出现

整体扫一下方法:

方法 描叙
void park() 阻塞当前线程,调用unpark可以从中断返回
void park(Object blocker) 阻塞当前线程,但是会记录是导致阻塞的对象
void parkNanos(long nanos) 阻塞当前线程nanos纳秒,时间过后会返回
void parkNanos(Object blocker, long nanos) 阻塞当前线程nanos纳秒,但是会记录是导致阻塞的对象
void parkUntil(long deadline) 阻塞当前线程直到deadline
void parkUntil(Object blocker, long deadline) 阻塞当前线程直到deadline,但是会记录是导致阻塞的对象
void unpark(Thread thread) 唤醒指定的线程

以上方法都很简单明了,结合上面的demo,很容易理解。

那么LockSupport具体是如何做到线程的阻塞与唤醒的呢,我们接下来分析一下。

void park(Object blocker)
// 阻塞线程逻辑
public static void park(Object blocker) {
  // 获取当前线程
  Thread t = Thread.currentThread();
  // 设置阻塞对象
  setBlocker(t, blocker);
  // 阻塞线程:这里是线程直接阻塞的位置
  UNSAFE.park(false, 0L);
  // 线程被唤醒时,第一步就是将blocker设置为null
  setBlocker(t, null);
}

// 设置阻塞对象
private static void setBlocker(Thread t, Object arg) {
  UNSAFE.putObject(t, parkBlockerOffset, arg);
}
...
  
// 对象内存偏移量,这个在直接设置对象内存时存储属性有用
private static final long parkBlockerOffset;
static {
  try {
    // 获取Unsafe对象(众所周知,这是一个”本地类“)
    UNSAFE = sun.misc.Unsafe.getUnsafe();
    // 获取线程的Class对象
    Class<?> tk = Thread.class;
    // 获取线程class中parkBlocker内存偏移量
    parkBlockerOffset = UNSAFE.objectFieldOffset
      (tk.getDeclaredField("parkBlocker"));
    ...
  } catch (Exception ex) { throw new Error(ex); }
}
...

在忽略Unsafe本地是如何阻塞的前提下,从上面代码整体上分析就是如下逻辑:

1)获取当前线程

2)设置阻塞当前线程的对象(这会很方便后续的问题排查,哪怕你显式打个日志都行)

3)调用UNSAFE.park进行阻塞

4)如果被唤醒,就清空阻塞当前线程的对象

至于unsafe本地方法如何注册,如何被寻址调用,这个在《认识UnSafe》一章中有详细分析,所以这里就不再降叙其过程

这里贴一下unsafe.cpp中的代码逻辑(至于怎么找到unsafe.cpp,可以回去看《认识UnSafe》这一节内容)

UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time))
  UnsafeWrapper("Unsafe_Park");
  EventThreadPark event;
#ifndef USDT2
  HS_DTRACE_PROBE3(hotspot, thread__park__begin, thread->parker(), (int) isAbsolute, time);
#else /* USDT2 */
   HOTSPOT_THREAD_PARK_BEGIN(
                             (uintptr_t) thread->parker(), (int) isAbsolute, time);
#endif /* USDT2 */
  JavaThreadParkedState jtps(thread, time != 0);

    // 核心逻辑
  thread->parker()->park(isAbsolute != 0, time);
#ifndef USDT2
  HS_DTRACE_PROBE1(hotspot, thread__park__end, thread->parker());
#else /* USDT2 */
  HOTSPOT_THREAD_PARK_END(
                          (uintptr_t) thread->parker());
#endif /* USDT2 */
  if (event.should_commit()) {
    oop obj = thread->current_park_blocker();
    event.set_klass((obj != NULL) ? obj->klass() : NULL);
    event.set_timeout(time);
    event.set_address((obj != NULL) ? (TYPE_ADDRESS) cast_from_oop<uintptr_t>(obj) : 0);
    event.commit();
  }
UNSAFE_END

看到最中间那行代码:

thread->parker()->park(isAbsolute != 0, time);

这里再强调一点就是,为什么java是跨平台的,就是因为对于本地方法在不同的操作系统上有不同的实现机制,因此才有了跨平台一说,这里可以去具体的操作系统上(比如Linux)去找到具体的实现逻辑,就能发现是如何阻塞线程了。这里就不去贴Linux上的代码实现了。

void unpark(Thread thread)

分析完void park(Object blocker)逻辑,在分析unpark那就简要过一下就好了

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

直接将thread作为变量,调用unsafe逻辑进行唤醒

看看unsafe.cpp的代码

UNSAFE_ENTRY(void, Unsafe_Unpark(JNIEnv *env, jobject unsafe, jobject jthread))
  UnsafeWrapper("Unsafe_Unpark");
  Parker* p = NULL;
  if (jthread != NULL) {
    oop java_thread = JNIHandles::resolve_non_null(jthread);
    if (java_thread != NULL) {
      jlong lp = java_lang_Thread::park_event(java_thread);
      if (lp != 0) {
        // This cast is OK even though the jlong might have been read
        // non-atomically on 32bit systems, since there, one word will
        // always be zero anyway and the value set is always the same
        p = (Parker*)addr_from_java(lp);
      } else {
        // Grab lock if apparently null or using older version of library
        MutexLocker mu(Threads_lock);
        java_thread = JNIHandles::resolve_non_null(jthread);
        if (java_thread != NULL) {
          JavaThread* thr = java_lang_Thread::thread(java_thread);
          if (thr != NULL) {
            p = thr->parker();
            if (p != NULL) { // Bind to Java thread for next time.
              java_lang_Thread::set_park_event(java_thread, addr_to_java(p));
            }
          }
        }
      }
    }
  }
  if (p != NULL) {
#ifndef USDT2
    HS_DTRACE_PROBE1(hotspot, thread__unpark, p);
#else /* USDT2 */
    HOTSPOT_THREAD_UNPARK(
                          (uintptr_t) p);
#endif /* USDT2 */
    p->unpark();
  }
UNSAFE_END

看到p->unpark();逻辑就知道去对应的操作系统上调用具体逻辑了,这里也不往下分析了。

总结

整体上<u>LockSupport</u>分析起来是最简单的,整个上层没什么复杂逻辑,无非就是阻塞唤醒(支持阻塞对象的记录,对了,这一点在是在jdk6加上的,jdk5是没有的,为了与wait/notify保持一致吧,为啥wait/notify有呢?因为传统的synchronized一般是针对对象锁的,因此会记录当前的对象信息的)

具体要想继续深入,那就去看看unsafe的整个调用链路及各个操作系统对于park/unpark的具体逻辑就好。

相关文章

网友评论

      本文标题:5.LockSupport核心原理分析

      本文链接:https://www.haomeiwen.com/subject/kcyoyktx.html