目录
一、前言
1.1 简介
1.2 为什么说LockSupport是Java并发的基石?
二、LockSupport的用途
2.1 LockSupport的主要方法
2.2 使用案例
2.3 总结
三、LockSupport 源码分析
3.1 学习原理前的前置知识
3.1.1 Unsafe.park()和Unsafe.unpark()
3.1.2wait和notify/notifyAll
3.1.3 LockSupport灵活性
3.2 LockSupport中的主要成员及其加载时的初始化
3.2.1 parkBlockerOffset
3.2.2 SEED, PROBE, SECONDARY
3.3 构造方法
3.4 park方法
3.5 parkNanos 方法
3.6 parkUntil 方法
3.7 unpark 方法
3.8 LockSupport原理总结
四、中断响应
LockSupport是concurrent包中的一个线程阻塞工具类,所有的方法都是静态方法,不提供构造,可以让线程在任意位置阻塞,当然阻塞之后肯定得有唤醒的方法。
LockSupport用来创建锁和其他同步类的基本线程阻塞原语。简而言之,当调用 LockSupport.park()时,表示当前线程将会等待,直至获得许可,当调用 LockSupport.unpark()时,必须把等待获得许可的线程作为参数进行传递,好让此线程继续运行。
当需要阻塞或唤醒一个线程的时候,JVM都会使用LockSupport工具类来完成相应工作。LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功能,而LockSupport也被称为构建同步组件的基础工具。
Java并发组件和并发工具类如下:
并发组件和并发工具大都是基于AQS来实现的:
队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。
而AQS中的控制线程又是通过LockSupport类来实现的,因此可以说,LockSupport是Java并发基础组件中的基础组件。LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。
接下面我来看看LockSupport有哪些常用的方法。主要有两类方法:park(阻塞线程)和unpark(解除阻塞)。
public static void park(Object blocker); // 暂停当前线程
public static void parkNanos(Object blocker, long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(Object blocker, long deadline); // 暂停当前线程,直到某个时间
public static void park(); // 无期限暂停当前线程
public static void parkNanos(long nanos); // 暂停当前线程,不过有超时时间的限制
public static void parkUntil(long deadline); // 暂停当前线程,直到某个时间
public static void unpark(Thread thread); // 恢复当前线程
public static Object getBlocker(Thread t); // 获取线程的Blocker对象
为什么叫park呢,park英文意思为停车。我们如果把Thread看成一辆车的话,park就是让车停下,unpark就是让车启动然后跑起来。
我们写一个例子来看看这个工具类怎么用的。
public class LockSupportDemo {
public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
static ChangeObjectThread t2 = new ChangeObjectThread("t2");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name) {
super(name);
}
@Override public void run() {
synchronized (u) {
System.out.println("in " + getName());
LockSupport.park();
if (Thread.currentThread().isInterrupted()) {
System.out.println("被中断了");
}
System.out.println("继续执行");
}
}
}
public static void main(String[] args) throws InterruptedException {
t1.start();
Thread.sleep(1000L);
t2.start();
Thread.sleep(3000L);
t1.interrupt();
LockSupport.unpark(t2);
t1.join();
t2.join();
}
}
运行的结果如下:
这儿park和unpark其实实现了wait和notify的功能,不过还是有一些差别的。
我们再来看看Object blocker对象,这是个什么东西呢?这其实就是方便在线程dump的时候看到具体的阻塞对象的信息。
"t1" #10 prio=5 os_prio=31 tid=0x00007f95030cc800 nid=0x4e03 waiting on condition [0x00007000011c9000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
// `下面的这个信息`
at com.wtuoblist.beyond.concurrent.demo.chapter3.LockSupportDemo$ChangeObjectThread.run(LockSupportDemo.java:23) //
- locked <0x0000000795830950> (a java.lang.Object)
blocker对象通过LockSupport.getBlocker方法获得。blocker对象只有在线程阻塞的时候才会被赋值,blocker对象是Thread线程类中的成员属性。
还有一个地方需要注意,相对于线程的stop和resume,park和unpark的先后顺序并不是那么严格。stop和resume如果顺序反了,会出现死锁现象。而park和unpark却不会。这又是为什么呢?还是看一个例子
public class LockSupportDemo {
public static Object u = new Object();
static ChangeObjectThread t1 = new ChangeObjectThread("t1");
public static class ChangeObjectThread extends Thread {
public ChangeObjectThread(String name) {
super(name);
}
@Override public void run() {
synchronized (u) {
System.out.println("in " + getName());
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.park();
if (Thread.currentThread().isInterrupted()) {
System.out.println("被中断了");
}
System.out.println("继续执行");
}
}
}
public static void main(String[] args) {
t1.start();
LockSupport.unpark(t1);
System.out.println("unpark invoked");
}
}
t1内部有休眠1s的操作,所以unpark肯定先于park的调用,但是t1最终仍然可以完结。这是因为park和unpark会对每个线程维持一个许可(boolean值)
意思就是如果新执行unpark,如果发现当前线程还没有执行park呢,那么unpark就会停在那等待,等到真的去执行了park之后,才会继续向下执行unpark。这个功能是通过两个方法共同维护的一个boolean类型的许可变量实现的。
我们再看看jdk的文档描述
在分析 LockSupport函数之前,先引入 sun.misc.Unsafe类中的 park和 unpark函数,因为 LockSupport的核心函数都是基于Unsafe类中定义的 park和 unpark函数,下面给出两个函数的定义:
public native void park(boolean isAbsolute, long time);
public native void unpark(Thread thread);
对两个函数的说明如下:
在看park()和unpark()之前,不妨来看下在没有LockSupport之前,是怎么实现让线程等待/唤醒的。
在没有LockSupport之前,线程的挂起和唤醒都是通过Object的wait和notify/notifyAll方法实现。
写一段例子代码,线程A执行一段业务逻辑后调用wait阻塞住自己。主线程调用notify方法唤醒线程A,线程A然后打印自己执行的结果。
public static void main(String[] args) throws Exception {
final Object obj = new Object();
Thread A = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
try {
obj.wait();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(sum);
});
A.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
obj.notify();
}
执行这段代码,不难发现这个错误:
原因很简单,wait和notify/notifyAll方法只能在同步代码块里用(这个有的面试官也会考察)。所以将代码修改为如下就可正常运行了:
public static void main(String[] args) throws Exception {
final Object obj = new Object();
Thread A = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
try {
synchronized (obj) {
obj.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(sum);
});
A.start();
// 睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
synchronized (obj) {
obj.notify();
}
}
那如果咱们换成LockSupport呢?简单得很,看代码:
public static void main(String[] args) throws Exception {
Thread A = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
LockSupport.park();
System.out.println(sum);
});
A.start();
// 睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
LockSupport.unpark(A);
}
通过上面的例子,我们就能明白LockSupport类就是为了提供与wait和notify/notifyAll方法相同的功能,并且使用起来更加简单方便而创造的工具类。
如果只是LockSupport在使用起来比Object的wait/notify简单,那还真没必要专门讲解下LockSupport。最主要的是灵活性。
上边的例子代码中,主线程调用了Thread.sleep(1000)方法来等待线程A计算完成进入wait状态。如果去掉Thread.sleep()调用:
public static void main(String[] args) throws Exception {
final Object obj = new Object();
Thread A = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
try {
synchronized (obj) {
obj.wait();
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(sum);
});
A.start();
// 睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
//Thread.sleep(1000);
synchronized (obj) {
obj.notify();
}
}
多次执行后,我们会发现:有的时候能够正常打印结果并退出程序,但有的时候线程无法打印结果阻塞住了。原因就在于主线程先调用完notify后,线程A才进入执行wait方法,导致线程A一直阻塞住。由于线程A不是后台线程,所以整个程序无法退出。
那如果换做LockSupport呢?LockSupport就支持主线程先调用unpark后,线程A再调用park而不被阻塞吗?是的,没错。代码如下:
public static void main(String[] args) throws Exception {
Thread A = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
LockSupport.park();
System.out.println(sum);
});
A.start();
// 睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
//Thread.sleep(1000);
LockSupport.unpark(A);
}
不管你执行多少次,这段代码都能正常打印结果并退出。这就是LockSupport最大的灵活所在。
同样的park()和unpark()也不会遇到Thread.suspend 和 Thread.resume所可能引发的死锁问题。
小结一下,LockSupport比Object的wait/notify有两大优势:
public class LockSupport {
// Hotspot implementation via intrinsics API
// UNSAFE字段表示 sun.misc.Unsafe类
// 一般程序中不允许直接调用
private static final sun.misc.Unsafe UNSAFE;
// 而 long型的表示Thread实例对象相应字段在内存中的偏移地址,可以通过该偏移地址获取或者设置该字段的值。
// 表示Thread类中的parkBlocker对象的内存偏移地址
private static final long parkBlockerOffset;
// 表示Thread类中的threadLocalRandomSeed对象的内存偏移地址
private static final long SEED;
// 表示Thread类中的threadLocalRandomProbe对象的内存偏移地址
private static final long PROBE;
// 表示Thread类中的threadLocalRandomSecondarySeed对象的内存偏移地址
private static final long SECONDARY;
// 静态代码块,会在加载时自动执行
static {
try {
// 获取Unsafe实例
UNSAFE = sun.misc.Unsafe.getUnsafe();
// 线程类类型
Class> tk = Thread.class;
// 获取Thread的parkBlocker字段的内存偏移地址
parkBlockerOffset = UNSAFE.objectFieldOffset
(tk.getDeclaredField("parkBlocker"));
// 获取Thread的threadLocalRandomSeed字段的内存偏移地址
SEED = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSeed"));
// 获取Thread的threadLocalRandomProbe字段的内存偏移地址
PROBE = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomProbe"));
// 获取Thread的threadLocalRandomSecondarySeed字段的内存偏移地址
SECONDARY = UNSAFE.objectFieldOffset
(tk.getDeclaredField("threadLocalRandomSecondarySeed"));
} catch (Exception ex) { throw new Error(ex); }
}
}
不难发现,他们在初始化的时候都是通过Unsafe去获得他们的内存地址。
下面讲一下这几个成员属性。
表示Thread类中的parkBlocker对象的内存偏移地址,提供给setBlocker和getBlocker使用。
private static void setBlocker(Thread t, Object arg) {
// Even though volatile, hotspot doesn't need a write barrier here.
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
}
上面方法中的参数t是Thread线程对象,parkBlocker对象就是Thread类的成员属性
// Thread类的成员属性
volatile Object parkBlocker;
上面的setBlocker和getBlocker方法,就是利用偏移地址parkBlockerOffset操作Thread对象中的parkBlocker。
由于Unsafe.putObject是无视Java访问限制,直接修改目标内存地址的值。即使对象被volatile修饰,也是不需要写屏障的。
这边的偏移量就算Thread这个类里面变量parkBlocker在内存中的偏移量:
JVM的实现可以自由选择如何实现Java对象的“布局“,也就是在内存里Java对象的各个部分放在哪里,包括对象的实例字段和一些元数据之类。 sun.misc.Unsafe里关于对象字段访问的方法把对象布局抽象出来,它提供了objectFieldOffset()方法用于获取某个字段相对 Java对象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之类的方法可以使用前面获取的偏移量来访问某个Java 对象的某个字段。
为什么要用偏移量来获取对象?干吗不要直接写个get、set方法?
parkBlocker就是在线程处于阻塞的情况下才被赋值。线程都已经被阻塞了,如果不通过这种内存的方法,而是直接调用线程内的方法,线程是不会回应调用的。
LockSupport中的这三个成员属性,就是下面这三个Thread类中的成员属性相对应Thread对象的偏移地址。
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;
都是Thread类中的内存偏移地址,主要用于ThreadLocalRandom类进行随机数生成,它要比Random性能好很多,可以看jdk源码ThreadLocalRandom.java了解详情,这儿就不贴了。
LockSupport 只有一个私有构造函数,无法被实例化。
// 私有构造函数,无法被实例化
private LockSupport() {}
因为LockSupport中定义的都是static静态方法,所以在使用LockSupport时并不需要实例化出一个对象,直接调用类的静态方法即可。
下面我们分析一下LockSupport最常用的几个方法的源码。
park 函数有两个重载版本,方法摘要如下:
public static void park();
public static void park(Object blocker);
两个函数的区别在于 park()函数有没有 blocker,即没有设置线程的 parkBlocker字段。
park(Object)型函数如下:
public static void park(Object blocker) {
// 获取当前线程
Thread t = Thread.currentThread();
// 设置Blocker
setBlocker(t, blocker);
// 获取许可
UNSAFE.park(false, 0L);
// 重新可运行后再此设置Blocker
setBlocker(t, null);
}
调用 park函数时,首先获取当前线程,然后设置当前线程的 parkBlocker字段,即调用 setBlocker函数,之后调用 Unsafe类的park函数,之后再调用 setBlocker函数。那么问题来了,为什么要在此 park函数中调用两次 setBlocker函数呢?
原因其实很简单,调用 park函数时,当前线程首先设置好 parkBlocker字段,然后再调用 Unsafe的 park函数,此后,当前线程就已经阻塞了,等待该线程的 unpark函数被调用,所以后面的一个 setBlocker函数无法运行,unpark函数被调用,该线程获得许可后,就可以继续运行了,也就运行第二个 setBlocker,把该线程的 parkBlocker字段设置为null,这样就完成了整个 park函数的逻辑。如果没有第二个 setBlocker,那么之后没有调用 park(Object blocker),而直接调用 getBlocker函数,得到的还是前一个 park(Object blocker)设置的 blocker,显然是不符合逻辑的。总之,必须要保证在 park(Object blocker)整个函数执行完后,该线程的parkBlocker字段又恢复为 null。所以,park(Object)型函数里必须要调用 setBlocker函数两次。
setBlocker方法如下:此方法用于设置线程t 的 parkBlocker字段的值为 arg。
private static void setBlocker(Thread t, Object arg) {
// 设置线程t的parkBlocker字段的值为arg
UNSAFE.putObject(t, parkBlockerOffset, arg);
}
另外一个无参重载版本,park()函数如下。
public static void park() {
// 获取许可,设置时间为无限长,直到可以获取许可
UNSAFE.park(false, 0L);
}
调用了park函数后,会禁用当前线程,除非许可可用。在以下三种情况之一发生之前,当前线程都将处于休眠状态,即下列情况发生时,当前线程会获取许可,可以继续运行。
此函数表示在许可可用前禁用当前线程,并最多等待指定的等待时间。具体函数如下。该函数也是调用了两次 setBlocker函数,nanos参数表示相对时间,表示等待多长时间。
public static void park() {
// 获取许可,设置时间为无限长,直到可以获取许可
UNSAFE.park(false, 0L);
}
此函数表示在指定的时限前禁用当前线程,除非许可可用,具体函数如下:该函数也调用了两次 setBlocker函数,deadline参数表示绝对时间,表示指定的时间。
public static void parkUntil(Object blocker, long deadline) {
// 获取当前线程
Thread t = Thread.currentThread();
// 设置Blocker
setBlocker(t, blocker);
UNSAFE.park(true, deadline);
// 设置Blocker为null
setBlocker(t, null);
}
此函数表示如果给定线程的许可尚不可用,则使其可用。如果线程在 park 上受阻塞,则它将解除其阻塞状态。否则,保证下一次调用 park 不会受阻塞。如果给定线程尚未启动,则无法保证此操作有任何效果。具体函数如下:释放许可,指定线程可以继续运行。
public static void unpark(Thread thread) {
if (thread != null) // 线程为不空
UNSAFE.unpark(thread); // 释放该线程许可
}
通过学习上面几个方法的源码,我们就发现LockSupport的底层实现都是基于Unsafe.park()和Unsafe.unpark()。
Unsafe源码也相对简单,看下就行了:
void
sun::misc::Unsafe::unpark (::java::lang::Thread *thread)
{
natThread *nt = (natThread *) thread->data;
nt->park_helper.unpark ();
}
void
sun::misc::Unsafe::park (jboolean isAbsolute, jlong time)
{
using namespace ::java::lang;
Thread *thread = Thread::currentThread();
natThread *nt = (natThread *) thread->data;
nt->park_helper.park (isAbsolute, time);
}
总之使用park和unpark进行线程的阻塞和唤醒操作,LockSuport.park和LockSuport.unpark是基于Unsafe类中的park()和unpark()方法来实现的,而再往底层看,Unsafe又是借助系统层(C语言)方法pthread_cond_wait和pthread_cond_signal来操作pthread_u和pthread_cond实现的,通过pthread_cond_wait函数可以对一个线程进行阻塞操作,在这之前,必须先获取pthread_mutex,通过pthread_cond_signal函数对一个线程进行唤醒操作。
pthread_mutex和pthread_cond使用示例如下:
void *r1(void *arg)
{
pthread_mutex_t* mutex = (pthread_mutex_t *)arg;
static int cnt = 10;
while(cnt--)
{
printf("r1: I am wait.\n");
pthread_mutex_lock(mutex);
/* mutex参数用来保护条件变量的互斥锁,调用pthread_cond_wait前mutex必须加锁 */
pthread_cond_wait(&cond, mutex);
pthread_mutex_unlock(mutex);
}
return "r1 over";
}
void *r2(void *arg)
{
pthread_mutex_t* mutex = (pthread_mutex_t *)arg;
static int cnt = 10;
while(cnt--)
{
pthread_mutex_lock(mutex);
printf("r2: I am send the cond signal.\n");
pthread_cond_signal(&cond);
pthread_mutex_unlock(mutex);
sleep(1);
}
return "r2 over";
}
注意,Linux下使用pthread_cond_signal的时候,会产生“惊群”问题的,但是Java中是不会存在这个“惊群”问题的,那么Java是如何处理的呢?
实际上,Java只会对一个线程调用pthread_cond_signal操作,这样肯定只会唤醒一个线程,也就不存在所谓的惊群问题。Java在语言层面实现了自己的线程管理机制(阻塞、唤醒、排队等),每个Thread实例都有一个独立的pthread_u和pthread_cond(系统层面的/C语言层面),在Java语言层面上对单个线程进行独立唤醒操作。(Java中线程只能在Java线程库的指挥下作战,无法直接获取同一个pthread_mutex或者pthread_cond。Java这种实现线程机制的实现实在太巧妙了,虽然底层都是使用pthread_mutex和pthread_cond这些方法,但是貌似C/C++还没这么强大易用的线程库)
具体LockSuuport.park和LockSuuport.unpark的底层实现可以参考对应JDK源码,下面看一下gdb打印处于LockSuuport.park时的线程状态信息:
由上图可知底层确实是基于pthread_cond函数来实现的。
我们在使用LockSupport过程中,多次调用unpark方法和调用一次unpark方法效果一样,因为都是直接将_counter赋值为1,而不是加1。简单说就是:线程A连续调用两次LockSupport.unpark(B)方法唤醒线程B,然后线程B调用两次LockSupport.park()方法, 线程B依旧会被阻塞。因为两次unpark调用效果跟一次调用一样,只能让线程B的第一次调用park方法不被阻塞,第二次调用依旧会阻塞。
import java.util.concurrent.locks.LockSupport;
class MyThread extends Thread {
private Object object;
public MyThread(Object object) {
this.object = object;
}
public void run() {
System.out.println("before interrupt");
try {
// 休眠3s 为了让主线程先执行park()
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread thread = (Thread) object;
// 3、执行完park之后,执行中断线程
thread.interrupt();
System.out.println("after interrupt");
}
}
public class InterruptDemo {
public static void main(String[] args) {
MyThread myThread = new MyThread(Thread.currentThread());
// 1、执行线程
myThread.start();
System.out.println("before park");
// 2、执行park,获取许可
LockSupport.park("ParkAndUnparkDemo");
// 4、线程被成功唤醒了
System.out.println("after park");
}
}
运行结果:
before park
before interrupt
after interrupt
after park
可以看到,在主线程调用 park阻塞后,在 myThread线程中发出了中断信号,此时主线程会继续运行,也就是说明此时 interrupt起到的作用与 unpark一样。
总之,线程使用LockSupport.park方法被阻塞后,然后被interrupt()方法唤醒之后,该线程就再也不会被 LockSupport.park方法阻塞了,会被直接唤醒。但是被interrupt()方法唤醒之后,该线程仍然可以再次被LockSupport.park方法阻塞。
参考文章:https://www.cnblogs.com/zhengzhaoxiang/p/13973980.html