「这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战」。
中文文档地址:www.apiref.com/java11-zh/j…
LockSupport 是什么?
解决了线程等待唤醒机制(wait/notify)
核心方法:
LockSupport 中的 park() 和 unpark() 的作用分别是阻塞线程和解除阻塞线程
线程唤醒和等待的方法
三种让线程等待和唤醒的方法
方式1: 使用 Object 中的 wait() 方法让线程等待,使用 Object 中的 notify() 方法唤醒线程。
方式2: 使用 JUC 包中 Condition 的 await() 方法让线程等待,使用 signal() 方法唤醒线程。
方式3: LockSupport 类可以阻塞当前线程以及唤醒指定被阻塞的线程。
方式1: Object 类中的 wait 和 notify 方法实现线程等待和唤醒
试验代码:
public class LockSupportDemo {
static Object objectLock = new Object();
public static void main(String[] args) {
// 1、wait、notify 需要配合 synchronized 使用
new Thread(() -> {
// 2、唤醒线程只能唤醒当前 wait 的线程
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectLock) {
System.out.println(Thread.currentThread().getName() + "\t =======> 进入锁");
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();
}
}
总结:
1、wait、notify 需要配合 synchronized 使用不然会抛出异常
结论:Object 类中的 wait、notify 、notifyAll 用与线程等待和唤醒的方法,都必须要在 synchronized 内部执行(必须要使用 synchronized 关键在)
2、需要先阻塞后唤醒
/**
* 要求:t1 线程等待 3 秒,3 秒后, t2 线程唤醒 t1 线程继续工作
*
* 将 notify 放在 wait 方法之前执行,t1 先 notify 了, 3 秒钟后 t2 线程再执行 wait 方法
* 现象:
* 程序一直无法结束
* 结论:
* 先 wait 后 notify 、notifyAll方法、等待中的线程才会被唤醒,否则无法被唤醒
*/
public class LockSupportDemo {
static Object objectLock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 2、唤醒线程只能唤醒当前 wait 的线程
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (objectLock) {
System.out.println(Thread.currentThread().getName() + "\t =======> 进入锁");
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();
}
}
方式2: Condition 接口中的 await 后 singnal 方法实现线程的等待和唤醒
试验代码:
public class LockSupportConditionDemo {
static Lock lock = new ReentrantLock();
static Condition condition = lock.newCondition();
public static void main(String[] args) {
new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "\t ====== 进入锁");
condition.await();
System.out.println(Thread.currentThread().getName() + "\t ====== 被唤醒");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}, "A").start();
new Thread(() -> {
lock.lock();
try {
condition.signal();
System.out.println(Thread.currentThread().getName() + "\t ====== 通知");
} finally {
lock.unlock();
}
}, "B").start();
}
}
结论:
1、condition 需要配合 Lock 一起使用。
2、需要先阻塞后唤醒
注:方式3 后续单独说明
传统的 synchronized 和 Lock 实现等待唤醒通知的约束
1、线程需要先获得并且持有锁,必须在锁块(synchronized 或 lock)中
2、必须要先等待后唤醒,线程才能够被唤醒
LockSupport 类中的 park 等待和 unpark 唤醒
是什么?
通过 park() 和 unpark() 犯法来实现阻塞和唤醒线程的操作
LockSupport 类使用了一种名为 Permit ( 许可) 的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),
permit 有两个之 1 和 0 , 默认是 0。
可以把许可堪称是一种 (0,1)信号量(Semaphore), 但与 Semaphore 不同的是,许可的累加上限是 1。
主要的方法
核心方法:
核心方法 park()/park(Object blocker)
阻塞当前线程/阻塞传入的具体线程
public static void park() {
UNSAFE.park(false, 0L);
}
permit 默认是 0 ,所以一开始就调用 park() 方法, 当前线程就会阻塞, 知道别的线程将当前线程的 permit 设置为 1 时, park 方法会被唤醒, 然后会将 permit 再次设置为 0 并返回。
核心方法 unpark(Thread thread)
唤醒处于阻塞状态的指定线程
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
代码实践
Thread a = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " \t ======= 进入锁");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + "\t ======== 被唤醒");
}, "A");
a.start();
TimeUnit.SECONDS.sleep(3);
Thread b = new Thread(() -> {
LockSupport.unpark(a);
System.out.println(Thread.currentThread().getName() + "\t ======== 通知了");
}, "A");
b.start();
试验结论:
1、支持无锁的情况调用,执行线程的阻塞;
2、支持先 unpark , 然后 park 操作依然有效。
重点说明
LockSupport 是用来创建锁和其他同步类的基本线程阻塞原语。
LockSupport 是一个线程阻塞工具类, 所有的方法都是静态方法,可以让线程在任意位置阻塞,阻塞之后也有对应的唤醒方法。
归根结底, LockSupport 调用 Unsafe 的 native 代码
LockSupport 提供 park() 和 unpark() 方法实现阻塞吓成和解除线程阻塞的过程。
LockSupport 和每个使用它的线程都有一个许可(permit)关联。permit 相当于 1, 0 的开关,默认是0,
调用一次 unpark 就加 1 变成 1。
调用一次 park 会消费 permit , 也就是将 1 变成 0, 同时 park 立即返回。
如果再次调用 park 就会变成阻塞(因为 permit 为 0 了会阻塞在这里,直到 permit 变为 1),这时候调用 unpark 会把 permit 设置为 1。每个线程都有一个相关的 permit, permit 最多只有一个, 重复调用 unpark 也不会累积凭证。
形象的理解
线程阻塞需要消耗凭证(permit), 这个凭证最多只有 1个
当调用 park 方法时
- 如果有凭证,则会直接消耗掉这个凭证然后正常退出。
- 如果无凭证,就必须阻塞等待凭证可用。
而 unpark 则相反,它会增加一个凭证,但凭证最多只能有 1 个,累加无效。
问题总结
为什么可以先唤醒线程后阻塞线程?
因为 unpark 获取到一个凭证,之后在调用 park 方法,就可以名正言顺的凭证消费,故不会阻塞。
为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?
因为凭证的数量最多为 1 ,连续两次调用 unpark 和调用一次 unpark 效果一样,只会增加一个凭证: 而调用两次 park 却需要消费两个凭证,证不够,不能放行。