java并发编程之:LockSupport
在介绍之前,先抛几个问题。
- Thread.interrupt()方法和InterruptedException异常的关系?是由interrupt触发产生了InterruptedException异常?
- Thread.interrupt()会中断线程什么状态的工作? RUNNING or BLOCKING?
- 一般Thread编程需要关注interrupt中断不?一般怎么处理?可以用来做什么?
- LockSupport.park()和unpark(),与object.wait()和notify()的区别?
- LockSupport.park(Object blocker)传递的blocker对象做什么用?
- LockSupport能响应Thread.interrupt()事件不?会抛出InterruptedException异常?
- Thread.interrupt()处理是否有对应的回调函数?类似于钩子调用?
如果你都都能很明确的答上来了,说明你已经完全懂Thread.interrupt,可以不用往下看那了。
那如果不清楚的,带着这几个问题,一起来梳理下。
Thread的interrupt处理的几个方法:
- public void interrupt() : 执行线程interrupt事件
- public boolean isInterrupted() : 检查当前线程是否处于interrupt
- public static boolean interrupted() : check当前线程是否处于interrupt,并重置interrupt信息。类似于resetAndGet()
理解:
1. 每个线程都有一个interrupt status标志位,用于表明当前线程是否处于中断状态
2. 一般调用Thread.interrupt()会有两种处理方式
- 遇到一个低优先级的block状态时,比如object.wait(),object.sleep(),object.join()。它会立马触发一个unblock解除阻塞,并throw一个InterruptedException。
- 其他情况,Thread.interrupt()仅仅只是更新了status标志位。然后你的工作线程通过Thread.isInterrrupted()进行检查,可以做相应的处理,比如也throw InterruptedException或者是清理状态,取消task等。
解答一下之前的几个问题:
问题1: Thread.interrupt()方法和InterruptedException异常的关系?是由interrupt触发产生了InterruptedException异常?
答: Thread.interrupt()只是在Object.wait() .Object.join(), Object.sleep()几个方法会主动抛出InterruptedException异常。而在其他的的block常见,只是通过设置了Thread的一个标志位信息,需要程序自我进行处理。
问题2:Thread.interrupt()会中断线程什么状态的工作? RUNNING or BLOCKING?
答:Thread.interrupt设计的目的主要是用于处理线程处于block状态,比如wait(),sleep()状态就是个例子。但可以在程序设计时为支持task cancel,同样可以支持RUNNING状态。比如Object.join()和一些支持interrupt的一些nio channel设计。
问题3: 一般Thread编程需要关注interrupt中断不?一般怎么处理?可以用来做什么?
答: interrupt用途: unBlock操作,支持任务cancel, 数据清理等。
问题4: LockSupport.park()和unpark(),与object.wait()和notify()的区别?
1. 面向的主体不一样。LockSuport主要是针对Thread进进行阻塞处理,可以指定阻塞队列的目标对象,每次可以指定具体的线程唤醒。Object.wait()是以对象为纬度,阻塞当前的线程和唤醒单个(随机)或者所有线程。
2. 实现机制不同。虽然LockSuport可以指定monitor的object对象,但和object.wait(),两者的阻塞队列并不交叉。可以看下测试例子。object.notifyAll()不能唤醒LockSupport的阻塞Thread.
问题5: LockSupport.park(Object blocker)传递的blocker对象做什么用?
答: 对应的blcoker会记录在Thread的一个parkBlocker属性中,通过jstack命令可以非常方便的监控具体的阻塞对象.
问题6: LockSupport能响应Thread.interrupt()事件不?会抛出InterruptedException异常?
答:能响应interrupt事件,但不会抛出InterruptedException异常。
LockSupport 和 CAS 是Java并发包中很多并发工具控制机制的基础,它们底层其实都是依赖Unsafe实现。
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 方法还可以在其他任何时间“毫无理由”地返回,因此通常必须在重新检查返回条件的循环里调用此方法。从这个意义上说,park 是“忙碌等待”的一种优化,它不会浪费这么多的时间进行自旋,但是必须将它与 unpark 配对使用才更高效。
三种形式的 park 还各自支持一个 blocker 对象参数。此对象在线程受阻塞时被记录,以允许监视工具和诊断工具确定线程受阻塞的原因。(这样的工具可以使用方法 getBlocker(java.lang.Thread) 访问 blocker。)建议最好使用这些形式,而不是不带此参数的原始形式。在锁实现中提供的作为 blocker 的普通参数是 this。
看下线程dump的结果来理解blocker的作用。
从线程dump结果可以看出:
有blocker的可以传递给开发人员更多的现场信息,可以查看到当前线程的阻塞对象,方便定位问题。所以java6新增加带blocker入参的系列park方法,替代原有的park方法。
LockSupport 源码解读
LockSupport中主要的两个成员变量:
// Hotspot implementation via intrinsics API
private static final sun.misc.Unsafe UNSAFE;
private static final long parkBlockerOffset;
unsafe:全名sun.misc.Unsafe可以直接操控内存,被JDK广泛用于自己的包中,如java.nio和java.util.concurrent。但是不建议在生产环境中使用这个类。因为这个API十分不安全、不轻便、而且不稳定。LockSupport的方法底层都是调用Unsafe的方法实现。
再来看parkBlockerOffset:
parkBlocker就是第一部分说到的用于记录线程被谁阻塞的,用于线程监控和分析工具来定位原因的,可以通过LockSupport的getBlocker获取到阻塞的对象。
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的方法:
可以看到,LockSupport中主要是park和unpark方法以及设置和读取parkBlocker方法。
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赋值。
public static Object getBlocker(Thread t) {
if (t == null)
throw new NullPointerException();
return UNSAFE.getObjectVolatile(t, parkBlockerOffset);
}
从线程t中获取它的parkBlocker对象,即返回的是阻塞线程t的Blocker对象。
接下来主查两类方法,一类是阻塞park方法,一类是解除阻塞unpark方法
阻塞线程
park()
public static void park() {
UNSAFE.park(false, 0L);
}
调用native方法阻塞当前线程。
parkNanos(long nanos)
public static void parkNanos(long nanos) {
if (nanos > 0)
UNSAFE.park(false, nanos);
}
阻塞当前线程,最长不超过nanos纳秒,返回条件在park()的基础上增加了超时返回。
parkUntil(long deadline)
public static void parkUntil(long deadline) {
UNSAFE.park(true, deadline);
}
阻塞当前线程,知道deadline时间(deadline - 毫秒数)。
JDK1.6引入这三个方法对应的拥有Blocker(许可)版本。
park(Object blocker)
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)
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)
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)
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
唤醒处于阻塞状态的线程Thread。
LockSupport位于JUC.locks包下,LockSupport是线程的阻塞原语,用来阻塞线程和唤醒线程,每个使用LockSupport的线程都会与一个许可关联,如果该需课可用,并且可在线程中使用,则调用park()将立即返回,否则可能会阻塞,如果许可上不可用,则可以调用unpark使其可用。但是需要注意许可不可重入。也就是说只能调用一次park()方法,否则会一直阻塞。
LockSupport方法介绍:
阻塞线程:
- void park():阻塞当前线程,如果调用unpark方法或者当前线程被中断,从能从park()方法中返回
- void park(Object blocker):功能同方法1,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;
- void parkNanos(long nanos):阻塞当前线程,最长不超过nanos纳秒,增加了超时返回的特性;
- void parkNanos(Object blocker, long nanos):功能同方法3,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;
- void parkUntil(long deadline):阻塞当前线程,知道deadline;
- void parkUntil(Object blocker, long deadline):功能同方法5,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查;
唤醒线程
void unpark(Thread thread):唤醒处于阻塞状态的指定线程
实际上LockSupport阻塞和唤醒线程的功能是依赖于sun.misc.Unsafe,这是一个很底层的类,有兴趣的可以去查阅资料,比如park()方法的功能实现则是靠unsafe.park()方法。另外在阻塞线程这一系列方法中还有一个很有意思的现象就是,每个方法都会新增一个带有Object的阻塞对象的重载方法。那么增加了一个Object对象的入参会有什么不同的地方了?示例代码很简单就不说了,直接看dump线程的信息。
调用park()方法dump线程:
"main" #1 prio=5 os_prio=0 tid=0x02cdcc00 nid=0x2b48 waiting on condition [0x00d6f000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304)
at learn.LockSupportDemo.main(LockSupportDemo.java:7)
调用park(Object blocker)方法dump线程
"main" #1 prio=5 os_prio=0 tid=0x0069cc00 nid=0x6c0 waiting on condition [0x00dcf000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x048c2d18> (a java.lang.String)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at learn.LockSupportDemo.main(LockSupportDemo.java:7)
通过分别调用这两个方法然后dump线程信息可以看出,带Object的park方法相较于无参的park方法会增加 parking to wait for <0x048c2d18> (a java.lang.String)的信息,这种信息就类似于记录“案发现场”,有助于工程人员能够迅速发现问题解决问题。有个有意思的事情是,我们都知道如果使用synchronzed阻塞了线程dump线程时都会有阻塞对象的描述,在java 5推出LockSupport时遗漏了这一点,在java 6时进行了补充。还有一点需要需要的是:synchronzed致使线程阻塞,线程会进入到BLOCKED状态,而调用LockSupprt方法阻塞线程会致使线程进入到WAITING状态。
3. 一个例子
用一个很简单的例子说说这些方法怎么用。
public class LockSupportDemo {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "被唤醒");
});
thread.start();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LockSupport.unpark(thread);
}
}
thread线程调用LockSupport.park()致使thread阻塞,当mian线程睡眠3秒结束后通过LockSupport.unpark(thread)方法唤醒thread线程,thread线程被唤醒执行后续操作。另外,还有一点值得关注的是,LockSupport.unpark(thread)可以指定线程对象唤醒指定的线程。
LockSupport模拟实现ThreadPoolExecutor.submit() 返回 Future方法:
import java.util.concurrent.Callable;
import java.util.concurrent.locks.LockSupport;
/**
* Created with IntelliJ IDEA.
* Description:
* User: zhubo
* Date: 2018-04-02
* Time: 10:11
*/
public class FutureTask <V> implements Runnable{
private boolean isDone;
private V returnVal;
Thread currentT ;
Callable<V> callable;
public FutureTask(Callable<V> callable,Thread t) {
if(null == callable){
throw new NullPointerException("parameter callable can not be null");
}
currentT = t;
this.callable = callable;
isDone = false;
}
@Override
public void run() {
try{
returnVal = callable.call();
runFinished();
}catch (Exception e){
e.printStackTrace();
}
}
public void runFinished(){
isDone = true;
LockSupport.unpark(currentT);
}
public boolean isDone() {
return isDone;
}
public V getVal(){
if(isDone){
return returnVal;
}
LockSupport.park();
return returnVal;
}
}
import java.util.concurrent.Callable;
/**
* Created with IntelliJ IDEA.
* Description:
* User: zhubo
* Date: 2018-04-02
* Time: 10:49
*/
public class FutureTaskTest {
public static void main(String[] args) {
Callable<Integer> callable = new Callable<Integer>(){
@Override
public Integer call() throws Exception {
Thread.sleep(2000);
return 12;
}
};
FutureTask<Integer> future = new FutureTask<>(callable, Thread.currentThread());
Thread t = new Thread(future);
t.start();
System.out.println("=============================");
Integer val = future.getVal();
System.out.println(val);
}
}
参考:
https://blog.csdn.net/aitangyong/article/details/38373137
https://www.jianshu.com/p/9677a754cf60