LockSupport
是什么?
LockSupport是一个工具类,提供了基本的线程阻塞和唤醒功能,它是创建锁和其他同步组件的基础工具,内部是使用sun.misc.Unsafe类实现的。
LockSupport和使用它的线程都会关联一个许可,park方法表示消耗一个许可,调用park方法时,如果许可可用则park方法返回,如果没有许可则一直阻塞直到许可可用。unpark方法表示增加一个许可,多次调用并不会积累许可,因为许可数最大值为1。
-
LockSupport是用来创建锁和其他同步类的基本线程阻塞原语。
-
LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和0,默认是0。可以把许可看成是一种(0,1)信号量(Semaphore),但与Semaphore不同的是,许可的累加上限是1。
LockSupport底层是UNSAFE。
-
permit默认是0,所以一开始调用park()方法,当前线程就会阻塞,直到别的线程将当前线程的permit设置为1时,park方法会被唤醒,然后会将permit再次设置为0并返回。
-
调用unpark(thread)方法后,就会将thread线程的许可permit设置成1(注意多次调用unpark方法,不会累加,permit值还是1)会自动唤醒thread线程,即之前阻塞中的LockSupport.park()方法会立即返回。
-
LockSupport和每个使用它的线程都有一个许可(permit)关联。permit相当于1,0的开关,默认是0,
调用一次unpark就将0变成1,调用一次park会消费permit,也就是将1变成o,同时park立即返回。如再次调用park会变成阻塞(因为permit为零了会阻塞在这里,一直到permit变为1),这时调用unpark会把permit置为1。每个线程都有一个相关的permit, permit最多只有一个,重复调用unpark也不会积累凭证。 -
形象的理解
线程阻塞需要消耗凭证(permit),这个凭证最多只有1个。-
当调用park方法时,如果有凭证,则会直接消耗掉这个凭证然后正常退出;如果无凭证,就必须阻塞等待凭证可用;
-
而unpark则相反,它会增加一个凭证,但凭证最多只能有1个,累加无效。
-
类图
主要方法介绍
-
park(): 阻塞当前线程,直到unpark方法被调用或当前线程被中断,park方法才会返回。
-
park(Object blocker): 同park()方法,多了一个阻塞对象blocker参数。
-
parkNanos(long nanos): 同park方法,nanos表示最长阻塞超时时间,超时后park方法将自动返回。
-
parkNanos(Object blocker, long nanos): 同parkNanos(long nanos)方法,多了一个阻塞对象blocker参数。
-
parkUntil(long deadline): 同park()方法,deadline参数表示最长阻塞到某一个时间点,当到达这个时间点,park方法将自动返回。(该时间为从1970年到现在某一个时间点的毫秒数)
-
parkUntil(Object blocker, long deadline): 同parkUntil(long deadline)方法,多了一个阻塞对象blocker参数。
-
unpark(Thread thread): 唤醒处于阻塞状态的线程thread。
源码分析
类的构造函数
// 私有构造函数,无法被实例化
private LockSupport() {}
类的属性
public class LockSupport {
// Hotspot implementation via intrinsics API
private static final sun.misc.Unsafe UNSAFE;
// 表示内存偏移地址
private static final long parkBlockerOffset;
// 表示内存偏移地址
private static final long SEED;
// 表示内存偏移地址
private static final long PROBE;
// 表示内存偏移地址
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); }
}
}
类的主要方法
static void park() //挂起当前线程,等待一个许可
static void unpark(Thread thread) //为某个线程提供一个许可,唤醒某个指定线程
在分析LockSupport函数之前,先引入sun.misc.Unsafe类中的park和unpark函数,因为LockSupport的核心函数都是基于Unsafe类中定义的park和unpark函数,
下面给出Unsafe类中定义的park和unpark函数的定义:
public native void park(boolean isAbsolute, long time);
public native void unpark(Thread thread);
说明: 对两个函数的说明如下:
-
park函数,阻塞线程
- 并且该线程在下列情况发生之前都会被阻塞:
- 调用unpark函数,释放该线程的许可。
- 该线程被中断。
- 设置的时间到了。
- 当time为绝对时间时,isAbsolute为true,否则,isAbsolute为false。
- 当time为0时,表示无限等待,直到unpark发生。
- 并且该线程在下列情况发生之前都会被阻塞:
-
unpark函数,释放线程的许可,即激活调用park后阻塞的线程。这个函数不是安全的,调用这个函数时要确保线程依旧存活。
park()函数
public static void park() {
// 获取许可,设置时间为无限长,直到可以获取许可
UNSAFE.park(false, 0L);
}
调用了park函数后,会禁用当前线程,除非许可可用。在以下三种情况之一发生之前,当前线程都将处于休眠状态,即下列情况发生时,当前线程会获取许可,可以继续运行。
- 其他某个线程将当前线程作为目标调用 unpark。
- 其他某个线程中断当前线程。
- 该调用不合逻辑地(即毫无理由地)返回。
unpark函数
此函数表示如果给定线程的许可尚不可用,则使其可用。如果线程在 park 上受阻塞,则它将解除其阻塞状态。否则,保证下一次调用 park 不会受阻塞。如果给定线程尚未启动,则无法保证此操作有任何效果。具体函数如下:
public static void unpark(Thread thread) {
if (thread != null) // 线程为不空
UNSAFE.unpark(thread); // 释放该线程许可
}
3种让线程等待和唤醒的方法
-
方式1: 使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程
-
方式2: 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
-
方式3: LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
方式1: 使用Object中的wait()方法让线程等待, 使用Object中的notify()方法唤醒线程
class MyThread extends Thread {
public void run() {
synchronized (this) {
System.out.println("before notify");
notifyAll();
System.out.println("after notify");
}
}
}
public class WaitAndNotifyDemo {
public static void main(String[] args) throws InterruptedException {
MyThread t1 = new MyThread();
// 主线程在第一次启动的时候肯定会获取到锁
synchronized (t1) {
try {
//t1线程启动,
t1.start();
System.out.println("before wait");
// 主线程睡眠3s
TimeUnit.SECONDS.sleep(3);
// 阻塞主线程
t1.wait();
System.out.println("after wait");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
结果
before wait
before notify
after notify
after wait
wait():作用是使当前线程从调用处中断并且释放锁转入等待队列,直到收到notify或者notifyAll的通知才能从等待队列转入锁池队列,没有收到停止会一直死等。
正常情况下
public class LockSupportDemo1 {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
}
},"A").start();
new Thread(() -> {
synchronized (objectLock)
{
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
}
},"B").start();
}
}
结果:
A ------come in
B ------通知
A ------被唤醒
Process finished with exit code 0
异常情况1
去掉同步代码块
public class LockSupportDemo1 {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
// }
},"A").start();
new Thread(() -> {
// synchronized (objectLock){
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
// }
},"B").start();
}
}
结果:
A ------come in
Exception in thread "A" Exception in thread "B" java.lang.IllegalMonitorStateException
at java.lang.Object.wait(Native Method)
at java.lang.Object.wait(Object.java:502)
at com.youth.guiguthirdquarter.AQS.LockSupportDemo1.lambda$main$0(LockSupportDemo1.java:16)
at java.lang.Thread.run(Thread.java:748)
java.lang.IllegalMonitorStateException
at java.lang.Object.notify(Native Method)
at com.youth.guiguthirdquarter.AQS.LockSupportDemo1.lambda$main$1(LockSupportDemo1.java:26)
at java.lang.Thread.run(Thread.java:748)
Process finished with exit code 0
报错。
异常情况2
先唤醒,再等待。
public class LockSupportDemo1 {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectLock){
System.out.println(Thread.currentThread().getName()+"\t"+"------come in");
try {
objectLock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t"+"------被唤醒");
}
},"A").start();
new Thread(() -> {
synchronized (objectLock)
{
objectLock.notify();
System.out.println(Thread.currentThread().getName()+"\t"+"------通知");
}
},"B").start();
}
}
结果:
B ------通知
A ------come in
Process finished with exit code -1
死循环,A无法被唤醒了。
这两点我们之前也说过,Object类提供的wait和notify
-
只能在synchronized同步代码块里使用
-
只能先等待(wait),再唤醒(notify)。顺序一旦错了,那个等待线程就无法被唤醒了。
方式2: 使用JUC包中Condition的await()方法让线程等待,使用signal()方法唤醒线程
缺点和Object类里的wait,notify一样。
- 只能在lock同步代码块里使用,不然就报错
- 只能先等待(await),再唤醒(signal)。顺序一旦错了,那个等待线程就无法被唤醒了。
但相对于wait,notify改进的一点是,可以绑定lock进行定向唤醒。
方式3: LockSupport类可以阻塞当前线程以及唤醒指定被阻塞的线程
有的时候我不需要进入同步代码块,只是需要让线程阻塞,这个时候LockSupport就发挥作用了。并且还解决了之前的第二个问题,也就是等待必须在唤醒的前面。
static void park() //挂起当前线程,等待一个许可
static void unpark(Thread thread) //为某个线程提供一个许可,唤醒某个指定线程
异常情况1
无同步代码块
public class LockSupportDemo3 {
public static void main(String[] args) {
/**
LockSupport:俗称 锁中断
LockSupport它的解决的痛点
1。LockSupport不用持有锁块,不用加锁,程序性能好,
2。不需要等待和唤醒的先后顺序,不容易导致卡死
*/
Thread t1 = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t ----begin-时间:" + System.currentTimeMillis());
LockSupport.park();//阻塞当前线程
System.out.println(Thread.currentThread().getName() + "\t ----被唤醒-时间:" + System.currentTimeMillis());
}, "t1");
t1.start();
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + "\t 通知t1...");
}
}
结果:
t1 ----begin-时间:1603376148147
t1 ----被唤醒-时间:1603376148147
main 通知t1...
Process finished with exit code 0
没有问题
异常情况2
先唤醒,再阻塞(等待)
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "\t ----begin-时间:" + System.currentTimeMillis());
LockSupport.park();//阻塞当前线程
System.out.println(Thread.currentThread().getName() + "\t ----被唤醒-时间:" + System.currentTimeMillis());
}, "t1");
t1.start();
LockSupport.unpark(t1);
System.out.println(Thread.currentThread().getName() + "\t 通知t1...");
}
结果:
main 通知t1...
t1 ----begin-时间:1603376257183
t1 ----被唤醒-时间:1603376257183
Process finished with exit code 0
可以看到,如果你先唤醒了。那么后面的LockSupport.park();就相当于瞬间被唤醒了,不会和之前一样程序卡死。为什么呢?结合之前分析的流程
-
先执行unpark,将许可证由0变为1
-
然后park来了发现许可证此时为0(也就是有许可证),那么他就不会阻塞,马上就往后执行。同时消耗许可证(也就是将1又变为0)。