一,前言
LockSupprot是线程的阻塞原语,用来阻塞线程和唤醒线程。每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,并且可在线程中使用,则调用park()将会立即返回,否则可能阻塞。如果许可尚不可用,则可以调用 unpark 使其可用。但是注意许可不可重入,也就是说只能调用一次park()方法,否则会一直阻塞。
我们可以使用它来阻塞和唤醒线程,功能和 wait,notify 有些相似,但是 LockSupport 比起 wait,notify 功能更强大,也好用的多。
在java并发包下各种同步组件的底层实现中,LockSupport 的身影处处可见。例如 ReentrantLock,ReentReadWriteLocks,已经在介绍线程间等待/通知机制使用的Condition时都会调用 LockSupport.park() 方法和 LockSupport.unpark() 方法。
二,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 的阻塞对象的重载方法。
调用 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 致使线程阻塞,线程会进入到 BLOCKED 状态,而调用 LockSupprt 方法阻塞线程会致使线程进入到WAITING 状态。
三,LockSupport 简单使用
import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "进入阻塞");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "被唤醒");
});
t1.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "准备唤醒");
//有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()
LockSupport.unpark(t1);
// t1.interrupt();
}
}
t1 线程调用 LockSupport.park() 致使thread阻塞,当 mian 线程睡眠1秒结束后通过 LockSupport.unpark(thread) 方法唤醒 thread 线程,thread 线程被唤醒执行后续操作。有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。
另外,还有一点值得关注的是,LockSupport.unpark(thread) 可以指定线程对象唤醒指定的线程。
执行结果:
Thread-0进入阻塞
main准备唤醒
Thread-0被唤醒
LockSupport 和 wait、notify 区别
LockSupport 不需要获取某对象的锁;
因为中断的时候park不会抛出InterruptedException异常,所以需要在park之后自行判断中断状态,然后做额外的处理
四,park 和 unpark 执行顺序不会引发死锁
相对于线程的stop和resume,park和unpark的先后顺序并不是那么严格。stop和resume如果顺序反了,会出现死锁现象。而park和unpark却不会。
import java.util.concurrent.locks.LockSupport;
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();
// 唤醒线程,此案例中先于 park() 方法执行
LockSupport.unpark(t1);
System.out.println("unpark invoked");
}
}
执行结果:
unpark invoked
in t1
继续执行
t1内部有休眠1s的操作,所以unpark肯定先于park的调用,但是t1最终仍然可以完结。这是因为park和unpark会对每个线程维持一个许可(boolean值)
1,unpark调用时,如果当前线程还未进入park,则许可为true
2,park调用时,判断许可是否为true,如果是true,则继续往下执行,并将许可重置为false;如果是false,则等待,直到许可为true。
jdk 的文档对 park() 和 unpark() 的描述:
五,LockSupport阻塞和唤醒线程原理浅析
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] ;
...
}
LockSupport就是通过控制变量 _counter
来对线程阻塞唤醒进行控制的。原理有点类似于信号量机制
。
1,当调用 park() 方法时,会将 counter 置为0,同时判断前值,等于 1 说明前面被 unpark 过,则直接退出,否则将使该线程阻塞。
2,当调用 unpark() 方法时,会将 _counter 置为 1,同时判断前值,小于1会进行线程唤醒,否则直接退出。
形象的理解,线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。当调用 park 方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;但是如果没有凭证,就必须阻塞等待凭证可用;而 unpark 则相反,它会增加一个凭证,但凭证最多只能有1个
。
3,为什么可以先唤醒线程后阻塞线程?
因为 unpark 获得了一个凭证,之后调用park因为有凭证消费,故不会阻塞。
4,为什么唤醒 park() 两次后阻塞 unpark() 两次会阻塞线程?
因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证;而调用两次 park 却需要消费两个凭证
。
/**
* // 发放唤醒凭证
* Makes available the permit for the given thread,
* // 没凭证,此时调用park,会阻塞
* if it was not already available. If the thread was blocked on {@code park} then it will unblock.
* // 有凭证,此时调用park,不会阻塞
* Otherwise, `its next call to {@code park} is guaranteed not to block. `
* This operation is not guaranteed to have any effect at all if the given thread has not been started.
*
* @param thread the thread to unpark, or {@code null}, in which case
* this operation has no effect
*/
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
总结
LockSupport 是 JDK中 用来实现线程阻塞和唤醒的工具。使用它可以在任何场合使线程阻塞,可以指定任何线程进行唤醒,并且不用担心阻塞和唤醒操作的顺序,但要注意连续多次唤醒的效果和一次唤醒是一样的。
JDK 并发包下的锁和其他同步工具的底层实现中大量使用了 LockSupport 进行线程的阻塞和唤醒,掌握它的用法和原理可以让我们更好的理解锁和其它同步工具的底层实现。
评论区