Java同步器之LockSupport源码分析

一、简介

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

LockSupport中的park()unpark(thread)的作用分别是阻塞线程解除阻塞线程,而且park()unpark(thread)不会遇到“Thread.suspendThread.resume所可能引发的死锁”问题。

调用park()的线程和另一个试图将其unpark(thread)的线程之间的竞争将保持活性。

因为park()unpark()有许可(permit)的存在,permit只有两个值10(默认)。permit默认值是0,所以一开始调用park()方法,线程必定会被阻塞。调用unpark(thread)方法后,会自动唤醒thread线程,即park方法立即返回。

  1. 当调用unpark(thread)方法,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)。
  2. 当调用park()方法,如果当前线程的permit1,那么将permit设置为0,并立即返回。如果当前线程的permit0,那么当前线程就会阻塞,直到别的线程将当前线程的permit设置为1park方法会将permit再次设置为0,并返回。

二、案例

下面的“示例1”和“示例2”可以更清晰的了解LockSupport的用法。

2.1 先park再unpark

import java.util.concurrent.locks.LockSupport;

public class LockSupportTest1 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(LocalDateTime.now() + " start...");
            //t1睡眠了一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(LocalDateTime.now() + " park...");
            
            //t1线程一秒后暂停
            LockSupport.park();
            
            System.out.println(LocalDateTime.now() + " resume...");
        }, "t1");
        t1.start();
        
        //主线程睡眠二秒
        Thread.sleep(2000);
        System.out.println(LocalDateTime.now() + " unpark...");
        
        //二秒后由主线程恢复t1线程的运行
        LockSupport.unpark(t1);
    }
}

运行结果

2023-02-08T16:58:30.425 start...
2023-02-08T16:58:31.430 park...
2023-02-08T16:58:32.385 unpark...
2023-02-08T16:58:32.386 resume...

分析

t1线程启动,休眠1秒后,调用park方法,暂停当前线程(也就是t1线程暂停了),不在继续执行。当主线程休眠2秒后,执行unpark方法唤醒t1线程,同时t1线程被唤醒,继续执行。

原理

先调用park分析

  1. 当前线程调用Unsafe.park()方法;
  2. 检查_counter,本情况为0,这时,获得_mutex互斥锁;
  3. 线程进入_cond条件变量阻塞;
  4. 设置_counter=0

再调用unpark分析

  1. 调用Unsafe.unpark(Thread_0)方法,设置_counter1
  2. 唤醒_cond条件变量中的Thread_0
  3. Thread_0恢复运行;
  4. 设置_counter0

2.2 先unpark再park

import java.util.concurrent.locks.LockSupport;

public class LockSupportTest2 {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println(LocalDateTime.now() + " start...");
            try {
                //t1睡眠了两秒
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(LocalDateTime.now() + " park...");
            
            //t1线程两秒后暂停
            LockSupport.park();
            
            System.out.println(LocalDateTime.now() + " resume...");
        }, "t1");
        t1.start();

        //主线程睡眠一秒
        Thread.sleep(1000);
        
        System.out.println(LocalDateTime.now() + " unpark...");
        
        //一秒后由主线程恢复t1线程的运行
        LockSupport.unpark(t1);
    }
}

运行结果

2023-02-08T16:59:57.452 start...
2023-02-08T16:59:58.423 unpark...
2023-02-08T16:59:59.457 park...
2023-02-08T16:59:59.457 resume...

分析

t1线程启动,休眠2秒。主线程只休眠1秒(此时t1线程还在睡眠的过程中),主线程醒来后执行了unpark方法,我们看到t1线程没有报错,也没有抛异常。当t1线程睡够2秒后执行park方法,理应暂停线程,但是并没有停下来,几乎同时向下继续执行。

原理

  1. 调用Unsafe.unpark(Thread_0)方法,设置_counter1
  2. 当前线程调用Unsafe.park()方法;
  3. 检查_counter,本情况为1,这时线程无需阻塞,继续运行;
  4. 设置_counter0

三、源码

3.1 构造方法

LockSupport类只提供了一个被private修饰的构造方法,意味着LockSupport不能在任何地方被实例化,但所有方法都是静态方法,可以在任意地方被调用。

/**
 * 私有构造
 */
private LockSupport() {}

3.2 属性

/**
 * 这个方法是由于多线程随机数生成器ThreadLocalRandom的package访问权限限制不能被这个包下的类使用,
 * 复制了一份实现出来,在StampedLock中被使用
 */
static final int nextSecondarySeed() {
    int r;
    Thread t = Thread.currentThread();
    if ((r = UNSAFE.getInt(t, SECONDARY)) != 0) {
        r ^= r << 13;   // xorshift
        r ^= r >>> 17;
        r ^= r << 5;
    }
    else if ((r = java.util.concurrent.ThreadLocalRandom.current().nextInt()) == 0)
        r = 1; // avoid zero
    UNSAFE.putInt(t, SECONDARY, r);
    return r;
}

/**
 * /**
 *  * 三种情况停止阻塞:
 *  * 1. 调用unpark
 *  * 2. 线程被中断
 *  * 3. 设置时间到了
 *  * @Param isAbsolute 是否绝对时间
 *  * @Param time 时间 为0时代表无线等待
 *  *
 * Unsafe.park(boolean isAbsolute,long time);
 *
 * /**
 *  * 释放某线程,需要保证释放时线程存活
 *  *
 * Unsafe.unpark(Thread thread);
 */
private static final sun.misc.Unsafe UNSAFE;

/**
 * 获取每个字段的偏移地址
 */
private static final long parkBlockerOffset;
private static final long SEED;
private static final long PROBE;
private static final long SECONDARY;
static {
    try {
    	/** 获取一个Unsafe实例 后续利用该实例获取偏移量 */
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        /** 创建一个Thread的class对象 后续利用反射获取字段 */
        Class<?> tk = Thread.class;
        parkBlockerOffset = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("parkBlocker"));
        SEED = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSeed"));
        PROBE = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomProbe"));
        SECONDARY = UNSAFE.objectFieldOffset
            (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
    } catch (Exception ex) { throw new Error(ex); }
}

3.3 blocker

3.3.1 blocker的作用

在分析LockSupport源码前,什么是blockerblocker的作用是什么?

Thread类的源码中,可以找到这样一个被volatile修饰的变量,LockSupport类中所有blocker的相关变量以及方法都是为这个parkBlocker变量服务的。

volatile Object parkBlocker;

当线程被阻塞时,如果该线程的parkBlocker变量不为空,则在打印堆栈异常时,控制台会打印输出具体阻塞对象的信息,方便错误排查。

3.3.2 blocker相关方法

首先关注blocker相关方法:

/**
 * 设置blocker
 * 将线程t的parkBlocker字段的值更新为arg
 */
private static void setBlocker(Thread t, Object arg) {
    UNSAFE.putObject(t, parkBlockerOffset, arg);
}

/**
 * 获取blocker
 */
public static Object getBlocker(Thread t) {
    if (t == null)
        throw new NullPointerException();
    return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
}

源码中可以清晰的看到方法中使用了parkBlockerOffset,即在类初始化时获取的parkBlocker在内存中的偏移量,putObjectgetObjectVolatile方法采用地址加偏移量的方式从内存直接设置或获取parkBlocker(Unsafe包下的方法可以直接操作内存,因此该类被命名为Unsafe),这样做的原因是因为线程被阻塞时无法被赋值或取值。

3.4 park

LockSupport为阻塞操作提供了两组三类方法,一组是不设置blocker的方法,另一组是设置blocker方法的,JDK推荐为Thread设置blocker方便调试。

/** 
 * 基础阻塞方法,无时限 
 */
public static void park(Object blocker) {
    /** 获取当前线程 */
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    /** 阻塞核心方法 直到被唤醒前不会执行下一语句 内部逻辑后文展开讨论 */
    UNSAFE.park(false, 0L);
    /** 线程被唤醒后parkBlocker重新置null */
    setBlocker(t, null);
}

/** 
 * 逻辑与park基本相同
 * 阻塞nanos纳秒 超时后线程被自动唤醒 
 */
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);
    }
}

/** 
 * 逻辑与park基本相同
 * 阻塞到日期deadline为止 超过deadline日期后自动被唤醒  
 */
public static void parkUntil(Object blocker, long deadline) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(true, deadline);
    setBlocker(t, null);
}

/**
 * 无限等待其它线程将该线程unpark或中断该线程
 */
public static void park() {
    UNSAFE.park(false, 0L);
}

/**
 * 指定等待时间
 */
public static void parkNanos(long nanos) {
    if (nanos > 0)
        UNSAFE.park(false, nanos);
}

/**
 * 阻塞到指定时刻
 */
public static void parkUntil(long deadline) {
    UNSAFE.park(true, deadline);
}

3.5 unpark

unpark方法如下,UNSAFE.unpark为唤醒线程的核心方法。

/**
 * 释放许可,使线程继续运行。
 * 如果线程没被阻塞,则下次park不会阻塞该线程?
 */
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

说明LockSupport是通过调用Unsafe函数中的接口实现阻塞和解除阻塞的。

3.6 底层实现

Unsafe的源码中可见,parkunpark都是native方法(意味着由C++底层实现)。

void Parker::park(bool isAbsolute, jlong time) {
  // 先原子的将_counter的值设为0,并返回_counter的原值,如果原值>0说明有通行证,直接返回
  // 首先尝试能否获取许可
  // 利用原子操作设_counter(许可)为0,同时返回_counter原值
  // 原值为1时获取许可成功 直接return;
  if (Atomic::xchg(0, &_counter) > 0) return;

  Thread* thread = Thread::current();
  assert(thread->is_Java_thread(), "Must be JavaThread");
  JavaThread *jt = (JavaThread *)thread;
  //如果线程被中断 直接返回
  if (Thread::is_interrupted(thread, false)) {
    return;
  }

  timespec absTime;
  // 如果出现time小于0 或
  // 调用了parkUntil方法(只用调用parkUntil时isAbsolute为true)且time为0的情况
  // 意味着不需要尝试获取许可 直接返回
  if (time < 0 || (isAbsolute && time == 0) ) {
    return;
  }
  // 只有在java种调用了parkNanos或parkUntil方法才会进入此分支
  if (time > 0) {
    // 定时唤醒
    unpackTime(&absTime, isAbsolute, time);
  }

  ThreadBlockInVM tbivm(jt);

  // 如果线程被中断,直接返回
  // 如果没有被中断,且获取互斥锁失败,直接返回
  if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
    return;
  }

  int status;
  // 如果_counter > 0, 不需要等待,这里再次检查_counter的值
  if (_counter > 0)  {
    _counter = 0;
    status = pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant") ;
    // 插入写屏障
    OrderAccess::fence();
    return;
  }

  OSThreadWaitState osts(thread->osthread(), false);
  // 暂停Java线程
  jt->set_suspend_equivalent();

  assert(_cur_index == -1, "invariant");
  if (time == 0) {
    _cur_index = REL_INDEX; // arbitrary choice when not timed
    // 线程进入阻塞状态 并等待_cond[_cur_index]信号
    status = pthread_cond_wait (&_cond[_cur_index], _mutex) ;
  } else {
    _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
    // 线程进入限时阻塞状态
    status = os::Linux::safe_cond_timedwait (&_cond[_cur_index], _mutex, &absTime) ;
    if (status != 0 && WorkAroundNPTLTimedWaitHang) {
      pthread_cond_destroy (&_cond[_cur_index]) ;
      pthread_cond_init    (&_cond[_cur_index], isAbsolute ? NULL : os::Linux::condAttr());
    }
  }
  _cur_index = -1;
  _counter = 0 ;
  // 互斥锁释放
  status = pthread_mutex_unlock(_mutex) ;
  assert_status(status == 0, status, "invariant") ;
  OrderAccess::fence();
}

void Parker::unpark() {
  int s, status ;
  status = pthread_mutex_lock(_mutex);
  assert (status == 0, "invariant") ; 
  //保存原始许可值 用于后续判断
  s = _counter; 
  //许可置1
  _counter = 1;
  if (s < 1) {
     if (WorkAroundNPTLTimedWaitHang) {
        //唤醒等待线程
        status = pthread_cond_signal (_cond) ;
        assert (status == 0, "invariant") ;
        //释放锁操作
        status = pthread_mutex_unlock(_mutex);
        assert (status == 0, "invariant") ;
     } else {
        status = pthread_mutex_unlock(_mutex);
        assert (status == 0, "invariant") ;
        status = pthread_cond_signal (_cond) ;
        assert (status == 0, "invariant") ;
     }
  } else {
    //释放锁
    pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant") ;
  }
}

mutexcondition保护了一个_counter的变量,当park时,这个变量置为了0,当unpark时,这个变量置为1

值得注意的是在park函数里,调用pthread_cond_wait时,并没有用while来判断,所以posix condition里的"Spurious wakeup"一样会传递到上层Java的代码里。

四、总结

LockSupport也是实现线程间通信的一种有效的方式。unparkpark之间不需要有严格的顺序。可以先执行unpark,之后再执行park。此外,LockSupport全部是采用UnSafe类来实现的。这个类通过使用park/unpark以及相关cas操作,就实现了javaJUC的各种复杂的数据结构和容器,而且效率非常高。

  1. LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个permit
  2. permit只有两个值10,默认是0
  3. 可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1
  4. permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park()方法会被唤醒,然后会将permit再次设置为0并返回。

五、拓展

5.1 park/unpark与wait/notify的区别

  1. 先唤醒再阻塞操作,由LockSupport实现,即先调用unpark再调用park,则该线程不会被阻塞;由Object实现,即先调用notify再调用wait,则该线程会被阻塞。
  2. LockSupport允许在任意地方阻塞唤醒线程,Objectwait/notify必须在synchronized同步代码块内调用。因为park/unpark依赖许可量,wait/notify依赖锁。
  3. LockSupport允许唤醒指定线程,notify只能唤醒随机线程,notifyAll唤醒全部阻塞线程。

5.2 为什么可以先唤醒线程后阻塞线程

因为unpark获取了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,故不会阻塞。

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次park却需要消费两个凭证,证不够,不能放行。

posted @ 2022-05-08 18:39  夏尔_717  阅读(174)  评论(0编辑  收藏  举报