LockSupport类的字段和方法
总体描述
通过上面图片可以看到,LockSupport具有的字段:parkBlockerOffset、UNSAFE、SEED、PROBE、SECONDARY ;具有的方法:getBlocker(Thread t)、park()、park(Object blocker)、parkNanos(long nanos)、parkNanos(Object blocker, long nanos)、parkUntil(long deadline)、parkUntil(Object blocker, long deadline)、setBlocker(Thread t, Object arg)、unpark(Thread thread);此外,还有一块静态代码块。
字段
它的字段主要是一个引用类型UNSAFE对象,这个类前面提到过;而其它4个字段其实是Thread类中四个属性的内存偏移地址。可以看到在静态代码块中获取了Thread对象的四个属性的偏移地址,那就是说LockSupport类中需要用到这四个偏移地址。以parkBlocker字段为例,示意图如下:
方法
getBlocker和setBlocker
可以看到这两个方法其实就是分别给上图的Thread对象的blocker字段取值和赋值操作,并且这种操作是通过unsafe这个类操作的,也就是直接操作的内存,与这个Thread类对象是否处于运行状态没有关系。
park类型的方法
首先是park()方法,其实可以看到它调用的是unsafe类的park方法,而这个方法是一个native方法,首先需要先了解unsafe的park和unpark方法:
而unsafe类的park和unpark底层是cpp实现,具体怎么实现可以参考:
https://juejin.cn/post/6844903703623761933#heading-3
此外,park方法还有一个重载方法:
可以其实就是设置线程对象的blocker字段,而这个blocker字段是用来标识当前线程等待的对象(简称为阻塞对象),该对象主要用于问题排查和系统监控。
由于在Java 5之前,当线程阻塞时(使用synchronized关键字)在一个对象上时,通过线程dump能够查看到该线程的阻塞对象。方便问题定位,而Java 5退出的Lock等并发工具却遗漏了这一点,致使在线程dump时无法提供阻塞对象的信息。因此,在Java 6中,LockSupport新增了含有阻塞对象的park方法。用以替代原有的park方法。
可见调用了两次setBlocker方法,这个方法是直接操作内存的。当执行到“UNSAFE.park”之后,当前线程就阻塞了,无法执行到下一个setBlocker,而当线程t被唤醒(比如调用unpark)就会接着执行第二个setBlocker方法,并把阻塞对象设置为null,线程t重新进入到运行状态。如下代码所示:
class LockSupportDemo {
public static void main(String[] args) throws InterruptedException {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
LockSupport.park("线程a的blocker数据");
System.out.println("我是被线程b唤醒后的操作");
}
});
a.start();
//让当前主线程睡眠1秒,保证线程a在线程b之前执行
Thread.sleep(1000);
Thread b = new Thread(new Runnable() {
@Override
public void run() {
String before = (String) LockSupport.getBlocker(a);
System.out.println("阻塞时从线程a中获取的blocker------>" + before);
LockSupport.unpark(a);
//这里睡眠是,保证线程a已经被唤醒了
try {
Thread.sleep(1000);
String after = (String) LockSupport.getBlocker(a);
System.out.println("唤醒时从线程a中获取的blocker------>" + after);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
b.start();
}
}
而上述例子,完全知道了blocker可以在线程阻塞的时候获取数据,也就证明了当我们对线程进行问题排查和系统监控的时候blocker的有着非常重要的作用。
unpark方法
LockSupport类的unpark方法其实调用的是unsafe类的unpark方法,只需要一个需要unpark的线程对象即可。
LockSupport的优点
这里说的LockSupport的优点主要是对比Object类的wait/notify的优点。
简单
相比wait/notify,parl/unpark其实更加简单易用,因为wait/notify必须在同步代码块中使用,比如下面的代码是错误的,会抛出异常:
public class TestObjWait {
public static void main(String[] args) throws Exception {
final Object obj = new Object();
Thread t = 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);
});
t.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
obj.notify();
}
}
至于为什么会抛出异常,其实就是wait/notify机制的实现原理,可以参考:https://www.yuque.com/docs/share/4bddd325-236c-491e-b2eb-012a2422afaf?# 《wait-notify机制》
修改为下面这样才是正确的:
public class TestObjWait {
public static void main(String[] args) throws Exception {
final Object obj = new Object();
Thread t = 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);
});
t.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
synchronized (obj) {
obj.notify();
}
}
}
而park/unpark其实就没有这个限制了,如下代码所示:
public class TestObjWait {
public static void main(String[] args) throws Exception {
Thread t = new Thread(() -> {
int sum = 0;
for (int i = 0; i < 10; i++) {
sum += i;
}
LockSupport.park();
System.out.println(sum);
});
t.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
Thread.sleep(1000);
LockSupport.unpark(t);
}
}
灵活
相比wait/notify机制,park/unpark更加灵活。因为如果先调用notify,然后才调用wait。如下代码:
public class TestObjWait {
public static void main(String[] args) throws Exception {
final Object obj = new Object();
Thread A = new Thread(new Runnable() {
@Override
public void run() {
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后,线程t才进入wait方法,导致线程t一直阻塞住,从而导致整个程序无法退出。
而这个其实就是之前所说的“通知遗漏”,程序不停止的原因就是线程t一直在wait:
但是,使用park/unpark却不会出现通知遗漏,因为先unpark后park不会导致线程一直处于等待状态(具体实现还得是底层的c++),如:
public class TestObjWait {
public static void main(String[] args) throws Exception {
final Object obj = new Object();
Thread t = new Thread(() -> {
int sum = 0;
for(int i=0;i<10;i++){
sum+=i;
}
LockSupport.park();
System.out.println(sum);
});
t.start();
//睡眠一秒钟,保证线程A已经计算完成,阻塞在wait方法
//Thread.sleep(1000);
LockSupport.unpark(t);
}
}
几个方法的对比
Thread.sleep()和Object.wait()的区别
Thread.sleep()和Condition.await()的区别
Thread.sleep()和LockSupport.park()的区别
这里特别注意LockSupport.park()是不会释放锁资源的。