Java-并发-锁-LockSupport

19 篇文章 0 订阅
11 篇文章 0 订阅

Java-并发-锁-LockSupport

摘要

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语,他的两个主要方法park()unpark()的作用分别是阻塞线程和解除阻塞线程。本文会介绍其基本概念,然后从java和cpp两个维度分析其源码,最后会对LockSupport和wait/notify做出对比,并给出LockSupport典型使用例子

0x01 基本概念

  • LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
  • LockSupport中有一个许可的概念
    • 当调用park()方法时,如果拥有许可就立刻返回;否则也许会阻塞
    • 调用unpark()会使得本来不可用的许可变为可用状态,解除线程阻塞
    • parkNanos可指定超时时长、parkUntil可指定截止时间戳
  • java.util.concurrent.Semaphore中许可的概念不同,LockSupport的许可每个线程最多能拥有1个
  • LockSupport的线程park可因中断、timeout或unpark甚至是毫无理由的返回,所以一般是通过循环检查附加条件是否满足
  • LockSupport的park行为可被中断,但不会抛出InterruptedException。此时可通过interrupted(会清除中断标记位)或isInterrupted方法判断是否发生中断
  • LockSupport是通过调用Unsafe函数中的UNSAFE.parkUNSAFE.unpark实现阻塞和解除阻塞的。

0x02 源码解析

2.1 类定义和构造方法

// 该类很耿直,就是个独立的 没有什么乱七八糟的继承关系
public class LockSupport {
    // 私有构造方法,不能实例化LockSupport
    private LockSupport() {}
}

2.2 初始化

// Hotspot implementation via intrinsics API
// UNSAFE实例
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 {
        // 只有根BootStrapClassLoader加载的类才能使用这个方式初始化UNSAFE,
        // 否则会报异常Exception in thread "main" java.lang.SecurityException: Unsafe
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class<?> tk = Thread.class;
        // 获取parkBlocker这个field在Thread类中的偏移位置
        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); }
}

2.3 重要方法

2.3.1 unpark
/**
 * 如果指定的线程处于park状态,就给他个许可让他不再阻塞
 * 否则,该线程的下一次park调用会保证不会被阻塞,但是多次调用就没用,只能保证一次
 * 当然,如果线程没开始运行,就没有任何保证
 *
 * @param thread 需要被unpark的线程对象,传null没有任何作用
 */
public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}
2.3.2 park

park的参数Blocker可用来记录阻塞时的关联对象,便于监视和排查线程阻塞情况。

/**
 * 禁用当前线程已达到不可调度运行的目的,直到有许可 可用
 * 如果此方法时就有许可可用(在此之前已经用unpark给了本线程一个许可),那就会立刻消费这个许可并立刻返回;
 * 否则当前线程会被禁止线程调度,保持睡眠直到以下三种情况发生:
 * 1.某个其他的线程调用了unpark,并以当前线程为参数,给一个许可
 * 2.某个其他的线程对当前线程调用了interrupt方法,发了中断(此时不会抛出InterruptedException)
 * 3.The call spuriously (that is, for no reason) returns莫名奇妙的返回了?
 *
 * 因为该方法返回空,所以调用者不会知道是啥原因导致的返回,所以需要检查下原因,
 * 比如查下线程中断状态
 *
 * @param blocker 负责park此线程的同步对象
 * @since 1.6
 */
public static void park(Object blocker) {
    Thread t = Thread.currentThread();
    setBlocker(t, blocker);
    UNSAFE.park(false, 0L);
    setBlocker(t, null);
}

/**
 * 禁用当前线程已达到不可调度运行的目的,直到有许可 可用
 * 如果此方法时就有许可可用(在此之前已经用unpark给了本线程一个许可),那就会立刻消费这个许可并立刻返回;
 * 否则当前线程会被禁止线程调度,保持睡眠直到以下四种情况发生:
 * 1.某个其他的线程调用了unpark,并以当前线程为参数,给一个许可
 * 2.某个其他的线程对当前线程调用了interrupt方法,发了中断(此时不会抛出InterruptedException)
 * 3.The call spuriously (that is, for no reason) returns莫名奇妙的返回了?
 * 4.指定的等待时间消耗完毕
 *
 * 因为该方法返回空,所以调用者不会知道是啥原因导致的返回,所以需要检查下原因,
 * 比如查下线程中断状态
 *
 * @param blocker 负责park此线程的同步对象
 * @param nanos 等待时间阈值,纳秒单位
 * @since 1.6
 */
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);
    }
}
// 这个方法与parkNanos类似,只不过传入的是毫秒级的绝对时间即时间戳
public static void parkUntil(long deadline) {
    UNSAFE.park(true, deadline);
}
    
/**
 * 禁用当前线程已达到不可调度运行的目的,直到有许可 可用
 * 
 * 如果此方法时就有许可可用(在此之前已经用unpark给了本线程一个许可),那就会立刻消费这个许可并立刻返回;
 * 否则当前线程会被禁止线程调度,保持睡眠直到以下三种情况发生:
 * 1.某个其他的线程调用了unpark,并以当前线程为参数,给一个许可
 * 2.某个其他的线程对当前线程调用了interrupt方法,发了中断(此时不会抛出InterruptedException)
 * 3.The call spuriously (that is, for no reason) returns莫名奇妙的返回了?
 * 
 * 因为该方法返回空,所以调用者不会知道是啥原因导致的返回,所以需要检查下原因,
 * 比如查下线程中断状态
 */
public static void park() {
    UNSAFE.park(false, 0L);
}

2.4 辅助方法

  • setBlocker
private static void setBlocker(Thread t, Object arg) {
    // 就算是volatile,hotspot vm 也不需要在这里使用读屏障
    // 将当前线程的parkBlocker设为arg
    UNSAFE.putObject(t, parkBlockerOffset, arg);
}
  • getBlocker
/**
 * 返回关联最近一次调用park方法,且还未解除阻塞时的对象
 * 如果已经解除阻塞了,就返回null
 * 注意1.该方法只是返回当时的内存快照,也就是说返回来以后可能使用其他对象示例又调用了park方法
 * 注意2.这个方法是用的`UNSAFE.getObjectVolatile`而不是直接用`Thread.parkBlocker`。原因待补充
 *
 * @param t the thread
 * @return the blocker
 * @throws NullPointerException if argument is null
 * @since 1.6
 */
public static Object getBlocker(Thread t) {
    if (t == null)
        throw new NullPointerException();
    return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
}

0x03 底层实现原理

看完第二章Java层级代码实现后,有兴趣可以继续看在cpp层级的实现原理:

3.1 park.hpp

该代码在jdk8/hotspot/src/share/vm/runtime/park.hpp

可以看到定义了Parker类:

class Parker : public os::PlatformParker {
private:
  // 记录许可数
  volatile int _counter ;
  Parker * FreeNext ;
  JavaThread * AssociatedWith ; // Current association
  
public:
  Parker() : PlatformParker() {
    // 初始,_counter为0 代表无许可
    _counter       = 0 ;
    FreeNext       = NULL ;
    AssociatedWith = NULL ;
  }
public:
  // park方法,isAbsolute为true时代表time为时间戳
  // isAbsolute为false时,time为超时时长
  void park(bool isAbsolute, jlong time);
  void unpark();

3.2 Parker::park(bool isAbsolute, jlong time)

该方法代码位于jdk8/hotspot/src/os/linux/vm/os_linux.cpp,部分核心代码如下:

void Parker::park(bool isAbsolute, jlong time) {
  // _counter大于0 就直接返回,表示许可可用
  if (Atomic::xchg(0, &_counter) > 0) return;

  Thread* thread = Thread::current();
  // 线程中断,就返回
  if (Thread::is_interrupted(thread, false)) {
    return;
  }
  ThreadBlockInVM tbivm(jt);
  
  // Don't wait if cannot get lock since interference arises from
  // unblocking.  Also. check interrupt before trying wait
  if (Thread::is_interrupted(thread, false) || pthread_mutex_trylock(_mutex) != 0) {
  // 再次检查线程中断
  // 无中断就尝试获取_mutex
    return;
  }
  
  if (_counter > 0)  { 
  // 此时许可可用
    // _counter重置为0,代表消费了这个许可
    _counter = 0;
    // 解除互斥锁
    status = pthread_mutex_unlock(_mutex);
    // Paranoia to ensure our locked and lock-free paths interact
    // correctly with each other and Java-level accesses.
    OrderAccess::fence();
    // 直接返回
    return;
  }
  
  // 执行到这里,说明无许可,需要阻塞线程
  
  if (time == 0) {
    _cur_index = REL_INDEX; // arbitrary choice when not timed
    // 时间未0就调用条件等待,无时间
    status = pthread_cond_wait (&_cond[_cur_index], _mutex) ;
  } else {
    _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
    // 否则按时间调用safe_cond_timedwait 按时长等待
    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());
    }
  }

3.3 Parker::unpark

核心代码摘录如下:

void Parker::unpark() {
  int s, status ;
  // 获取互斥锁
  status = pthread_mutex_lock(_mutex);
  // 临时存counter数
  s = _counter;
  // 将_counter重置为1,代表有许可
  _counter = 1;
  if (s < 1) {
  // 代表无许可
    // 此时线程也许被挂起了
    if (_cur_index != -1) {
    // 确认线程被挂起了
    // 调用pthread_cond_signal唤醒线程
    // 调用pthread_mutex_unlock释放_mutex
      if (WorkAroundNPTLTimedWaitHang) {
        status = pthread_cond_signal (&_cond[_cur_index]);
        status = pthread_mutex_unlock(_mutex);
      } else {
        status = pthread_mutex_unlock(_mutex);
        status = pthread_cond_signal (&_cond[_cur_index]);
      }
    } else {
    // 线程没有被挂起,只需释放_mutex
      pthread_mutex_unlock(_mutex);
    }
  } else {
  // 有许可,只需释放_mutex
    pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant") ;
  }
}

0x04 使用示例

4.1 普通使用例子

import java.util.concurrent.locks.LockSupport;

public class LockParkDemo1 {
    private static Thread mainThread;
    public static void main(String[] args) {
        InnerThread it =  new LockParkDemo1().new InnerThread();
        Thread td = new Thread(it);
        mainThread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + " start it");
        td.start();
        System.out.println(Thread.currentThread().getName() + " block");
//        LockSupport.park(Thread.currentThread());
        LockSupport.park();
        System.out.println(Thread.currentThread().getName() + " continue");

    }
    class InnerThread implements Runnable{
        @Override
        public void run() {
            int count = 5;
            while(count>0){
                System.out.println("count=" + count);
                count--;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Thread.currentThread().getName()+" wakup others");
            LockSupport.unpark(mainThread);
        }
    }
}

程序输出结果如下:

main start it
main block
Thread-0 wakup others
main continue

4.2 Blocker及调试例子

  1. 代码很简单,如下:
import java.util.concurrent.locks.LockSupport;

/**
 * Created by chengc on 2018/12/15.
 */
public class BlockerTest
{
    public static void main(String[] args)
    {
        Thread.currentThread().setName("Messi");
        LockSupport.park("YangGuang");
    }
}
  1. jps查看该进程pid:
$ jps
73900 BlockerTest
  1. jstack:
$ jstack -l 73900
"Messi" #1 prio=5 os_prio=31 tid=0x00007fe34c822000 nid=0x1b03 waiting on condition [0x0000700006470000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x000000076ac8fcc0> (a java.lang.String)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at demos.concurrent.lock.park.BlockerTest.main(BlockerTest.java:13)

   Locked ownable synchronizers:
	- None

可以看到我们的主线程Messi处于WAITING状态,而且原因是parkingBlocker对象时个java.lang.String

0x05 LockSupport和wait/notify区别

  • 阻塞和唤醒是对于线程来说的,LockSupportpark/unpark更符合这个语义,以“线程”作为方法的参数, 语义更清晰,使用起来也更方便;

  • wait/notify的实现使得“线程”的阻塞/唤醒对线程本身来说是被动的,要准确的控制哪个线程、什么时候阻塞/唤醒很困难, 要不随机唤醒一个线程(notify)要不唤醒所有的(notifyAll)。

  • 关于wait/notify的实现原理请看这篇文章:

0x06 应用

AQS(AbstractQueuedSynchronizer)就是利用了LockSupport的相关方法来控制线程阻塞或者唤醒。

public final void awaitUninterruptibly() {
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            boolean interrupted = false;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if (Thread.interrupted())
                    interrupted = true;
            }
            if (acquireQueued(node, savedState) || interrupted)
                selfInterrupt();
        }

0x07 总结

LockSupport.park底层就是用的Unsafe.park,而JDK中很多地方使用了Unsafe。这两个锁偏底层,建议用基于他们或AQS的高级锁如ReentrantLockCountDownLatchCyclicBarrier等。

0xFF 参考文档

Unsafe类park,unpark详解

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值