LockSupport

LockSupport 用法简介

LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。LockSupport 提供park()和unpark()方法实现阻塞线程和解除线程阻塞,LockSupport和每个使用它的线程都与一个许可(permit)关联。permit相当于1,0的开关,默认是0,调用一次unpark就加1变成1,调用一次park会消费permit, 也就是将1变成0,同时park立即返回。再次调用park会变成block(因为permit为0了,会阻塞在这里,直到permit变为1), 这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累。

park()和unpark()不会有 “Thread.suspend和Thread.resume所可能引发的死锁” 问题,由于许可的存在,调用 park 的线程和另一个试图将其 unpark 的线程之间的竞争将保持活性。

如果调用线程被中断,则park方法会返回。同时park也拥有可以设置超时时间的版本。

三种形式的 park 还各自支持一个 blocker 对象参数。此对象在线程受阻塞时被记录,以允许监视工具和诊断工具确定线程受阻塞的原因。(这样的工具可以使用方法 getBlocker(java.lang.Thread) 访问 blocker。)建议最好使用这些形式,而不是不带此参数的原始形式。在锁实现中提供的作为 blocker 的普通参数是 this。
看下线程dump的结果来理解blocker的作用。

LockSupport 源码解读

  1. LockSupport中主要的两个成员变量:
1
2
3
// Hotspot implementation via intrinsics API
private static final sun.misc.Unsafe UNSAFE;
private static final long parkBlockerOffset;

再来看parkBlockerOffset:
parkBlocker就是第一部分说到的用于记录线程被谁阻塞的,用于线程监控和分析工具来定位原因的,可以通过LockSupport的getBlocker获取到阻塞的对象。

1
2
3
4
5
6
7
8
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> tk = Thread.class;
parkBlockerOffset = UNSAFE.objectFieldOffset
(tk.getDeclaredField("parkBlocker"));
} catch (Exception ex) { throw new Error(ex); }
}

从这个静态语句块可以看的出来,先是通过反射机制获取Thread类的parkBlocker字段对象。然后通过sun.misc.Unsafe对象的objectFieldOffset方法获取到parkBlocker在内存里的偏移量,parkBlockerOffset的值就是这么来的.

JVM的实现可以自由选择如何实现Java对象的“布局”,也就是在内存里Java对象的各个部分放在哪里,包括对象的实例字段和一些元数据之类。 sun.misc.Unsafe里关于对象字段访问的方法把对象布局抽象出来,它提供了objectFieldOffset()方法用于获取某个字段相对 Java对象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之类的方法可以使用前面获取的偏移量来访问某个Java 对象的某个字段。

为什么要用偏移量来获取对象?干吗不要直接写个get,set方法。多简单?
仔细想想就能明白,这个parkBlocker就是在线程处于阻塞的情况下才会被赋值。线程都已经阻塞了,如果不通过这种内存的方法,而是直接调用线程内的方法,线程是不会回应调用的。

2.LockSupport的方法:

img

可以看到,LockSupport中主要是park和unpark方法以及设置和读取parkBlocker方法。

1
2
3
4
private static void setBlocker(Thread t, Object arg) {
// Even though volatile, hotspot doesn't need a write barrier here.
UNSAFE.putObject(t, parkBlockerOffset, arg);
}

对给定线程t的parkBlocker赋值。

1
2
3
4
5
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
}

从线程t中获取它的parkBlocker对象,即返回的是阻塞线程t的Blocker对象。

接下来主查两类方法,一类是阻塞park方法,一类是解除阻塞unpark方法

阻塞线程

  • park()
1
2
3
public static void park() {
UNSAFE.park(false, 0L);
}

调用native方法阻塞当前线程。

  • parkNanos(long nanos)
1
2
3
4
public static void parkNanos(long nanos) {
if (nanos > 0)
UNSAFE.park(false, nanos);
}

阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回。

  • parkUntil(long deadline)
1
2
3
public static void parkUntil(long deadline) {
UNSAFE.park(true, deadline);
}

阻塞当前线程,知道deadline时间(deadline - 毫秒数)。

JDK1.6引入这三个方法对应的拥有Blocker版本。

  • park(Object blocker)
1
2
3
4
5
6
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}

1) 记录当前线程等待的对象(阻塞对象);
2) 阻塞当前线程;
3) 当前线程等待对象置为null。

  • parkNanos(Object blocker, long nanos)
1
2
3
4
5
6
7
8
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);
}
}

阻塞当前线程,最长等待时间不超过nanos毫秒,同样,在阻塞当前线程的时候做了记录当前线程等待的对象操作。

  • parkUntil(Object blocker, long deadline)
1
2
3
4
5
6
public static void parkUntil(Object blocker, long deadline) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(true, deadline);
setBlocker(t, null);
}

阻塞当前线程直到deadline时间,相同的,也做了阻塞前记录当前线程等待对象的操作。

唤醒线程

  • unpark(Thread thread)
1
2
3
4
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}

唤醒处于阻塞状态的线程Thread。

Locksupport 底层

在Linux系统下,是用的Posix线程库pthread中的mutex(互斥量),condition(条件变量)来实现的。
mutex和condition保护了一个_counter的变量,当park时,这个变量被设置为0,当unpark时,这个变量被设置为1。

看看Locksupport的源码中的注释可知,Locksupport是实现别的锁和同步类的基本原语。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Parker : public os::PlatformParker {
private:
volatile int _counter ;
...
public:
void park(bool isAbsolute, jlong time);
void unpark();
...
}
class PlatformParker : public CHeapObj<mtInternal> {
protected:
pthread_mutex_t _mutex [1] ;
pthread_cond_t _cond [1] ;
...
}

可以看到Parker类实际上用Posix的mutex,condition来实现的。
在Parker类里的_counter字段,就是用来记录“许可”的。

  • park 过程

当调用park时,先尝试能否直接拿到“许可”,即_counter>0时,如果成功,则把_counter设置为0,并返回:

1
2
3
4
5
6
7
8
9
10
11
void Parker::park(bool isAbsolute, jlong time) {  

// Ideally we'd do something useful while spinning, such
// as calling unpackTime().

// Optional fast-path check:
// Return immediately if a permit is available.
// We depend on Atomic::xchg() having full barrier semantics
// since we are doing a lock-free update to _counter.

if (Atomic::xchg(0, &_counter) > 0) return;

如果不成功,则构造一个ThreadBlockInVM,然后检查_counter是不是>0,如果是,则把_counter设置为0,unlock mutex并返回:

1
2
3
4
ThreadBlockInVM tbivm(jt);  
if (_counter > 0) { // no wait needed
_counter = 0;
status = pthread_mutex_unlock(_mutex);

否则,再判断等待的时间,然后再调用pthread_cond_wait函数等待,如果等待返回,则把_counter设置为0,unlock mutex并返回:

1
2
3
4
5
6
7
if (time == 0) {  
status = pthread_cond_wait (_cond, _mutex) ;
}
_counter = 0 ;
status = pthread_mutex_unlock(_mutex) ;
assert_status(status == 0, status, "invariant") ;
OrderAccess::fence();
  • unpark 过程

当unpark时,则简单多了,直接设置_counter为1,再unlock mutex返回。如果_counter之前的值是0,则还要调用pthread_cond_signal唤醒在park中等待的线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void Parker::unpark() {  
int s, status ;
status = pthread_mutex_lock(_mutex);
assert (status == 0, "invariant") ;
s = _counter;
_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") ;
}
}

LockSupport的特性

先释放许可,再获取许可

1
2
3
4
5
6
7
public static void main(String[] args)
{
Thread thread = Thread.currentThread();
LockSupport.unpark(thread);//释放许可
LockSupport.park();// 获取许可
System.out.println("b");
}

不可重入

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception
{
Thread thread = Thread.currentThread();

LockSupport.unpark(thread);

System.out.println("a");
LockSupport.park();
System.out.println("b");
LockSupport.park();
System.out.println("c");
}

这段代码打印出a和b,不会打印c,因为第二次调用park的时候,线程无法获取许可出现死锁。

中断响应

LockSupport.part()方法是响应中断地,当线程中断后,会从park方法返回执行后续逻辑,所以,LockSupport中的对中断地响应可以灵活控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* @author joyo
* @date 2018/4/16
*/
public class LockSupportInterruptTest {
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();

Thread thread = new Thread(new Runnable() {
@Override
public void run() {

lock.lock();
try {
LockSupport.park();
System.out.println("come back here");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});

thread.start();
Thread.sleep(2000);
thread.interrupt();
}
}

最终输出结果:come back here,而不是打印异常栈。

而Object.wait()方法并没有这个特性,会直接抛出中断异常。

LockSupport 和 Object的区别

两者区别总结如下:

  1. Object.wait和notify都是针对对象的,notify实际上是不知道唤醒具体哪个线程的,而Locksupport支持指定线程唤醒
  2. 实现原理不同,Locksupport是基于Unsafe.park来实现的。具体可以见参考资料3
  3. Locksupport功能更加强大些: 基于“许可”的同步实现,提供parkBlocker来监视锁的持有等。而Object.wait方法来完成同步,需要依赖监视器锁。
  4. JDK1.6之后针对synchrnized引入了分级的锁,根据后面的代码示例发现两类同步原语的开销是差不多的

两者相同点:

  1. park和wait都会阻塞线程,释放锁
  2. 虽然响应中断行动不同,但是都会更改中断标志位
  3. 功能上其实相近,但是为了易用性和功能妥协,park和unpark基本可以替代Object.wait和notify等

从区别上来看可知,使用Locksupport能更加精细、灵活地控制线程的同步,利于实现各种同步工具和锁。精细体现在针对线程的同步控制,灵活体现在通过“许可”获取的方式来保证活性。

参考

https://segmentfault.com/a/1190000008420938
https://www.jianshu.com/p/e3afe8ab8364
https://blog.csdn.net/u013851082/article/details/70242395
https://kaimingwan.com/post/java/javabing-fa-yu-suo/liao-liao-locksupport