本节学会后的主要应用:
- 互斥:使用
synchronized
或Lock
达到共享资源互斥效果 - 同步:使用
wait/notify
或Lock
的条件变量达到线程间的通信效果
管程:Monitor(本节原理)
一、共享带来的问题
\quad
当多个线程访问一个共同的资源时,会发生线程安全问题。假设有共享变量i=0
,有两个任务,任务1对该变量进行+1操作,任务2对该变量进行-1操作。假设线程t1对i进行+1操作,但还未来得及保存,此时i=0,时间片就切换给线程2,此时线程2又对i进行-1操作并保存,此时i=-1,t2线程结束,时间片给t1,t1将上次未保存的结果1给i,最终输出i=1。按理说i最后等于0,但是由于多个线程操作共享资源,导致了线程安全问题。例如下面程序,返回结果就可能是:[-10000,10000]中任意一个值
static int i = 0;
@Test
public void test3() throws InterruptedException {
Thread t1 = new Thread(()->{
for(int j = 0; j < 10000; j ++ ){
i ++ ;
}
}, "t1");
Thread t2 = new Thread(()->{
for(int j = 0; j < 10000; j ++ ){
i -- ;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", i);
}
临界区
- 多个线程访问共享资源,如果只涉及到读操作,不会有安全问题;如果对共享资源进行修改时发生指令交错,就会出现问题
- 临界区:一段代码块内如果存在对共享内存的多线程读写操作,称这段代码块为临界区
竞态条件
- 多个线程在临界区执行,由于代码对应的指令执行顺序不同而导致结果无法预测,称为发生了竞态条件
如何避免临界区竞态条件
- 阻塞式:
synchronized,Lock
- 非阻塞式:原子变量
二、synchronized
对象锁
\quad
synchronized
俗称对象锁,采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其他线程想要获得这个对象锁时就会被阻塞住。这样就能保证拥有锁的线程可以安全地执行临界区中代码,不用担心上下文问题。语法如下:
synchronized(任意对象)
{
临界区
}
\quad
对第一节的示例代码中临界区(也就是i++,i–)处加锁,加锁需要随便创建一个对象,我们使用private Object lock = new Object();
作为锁对象,用synchronized
把临界区代码包起来,这样就不会发生线程安全问题了,如下:
private int i = 0;
private Object lock = new Object();
@Test
public void test3() throws InterruptedException {
Thread t1 = new Thread(()->{
for(int j = 0; j < 10000; j ++ ){
synchronized (lock) {
i++;
}
}
}, "t1");
Thread t2 = new Thread(()->{
for(int j = 0; j < 10000; j ++ ){
synchronized (lock) {
i--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", i);
}
\quad 可以将临界区的代码封装在一个类中,这样封装性更好,例如上述例子,可以改造为如下:
@Test
public void test3() throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(()->{
for(int j = 0; j < 10000; j ++ ){
room.increment();
}
}, "t1");
Thread t2 = new Thread(()->{
for(int j = 0; j < 10000; j ++ ){
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.getCounter());
}
}
class Room{
private int counter = 0;
public void increment(){
synchronized (this){
counter ++ ;
}
}
public void decrement(){
synchronized (this){
counter -- ;
}
}
public int getCounter(){
synchronized (this){
return counter;
}
}
可以将synchronized
锁在方法上,一般可以把临界区代码剥离成为一个函数,直接锁在该方法上
synchronized method()
{
临界区
}
等价于
method(){
synchronized(this){
临界区
}
}
上面例子中给出的Room也可以这样写:
class Room{
private int counter = 0;
public synchronized void increment(){
counter ++ ;
}
public synchronized void decrement(){
counter -- ;
}
public synchronized int getCounter(){
return counter;
}
}
如果加在静态方法上等于锁住了类对象。
class Test{
public synchronized static void method(){
}
}
等价于
class Test{
public static void method(){
synchronized(Test.class){
}
}
}
总结:
synchronized
实际上是用对象锁保证了临界区内代码的原子性,临界区内代码不可分割,不会被线程切换所打断
三、常见的线程安全类
String
Integer
等包装类StringBuffer
Vector
Hashtable
:与HashMap底层实现一样,只是在方法上加上了锁,如下:
java.util.concurrent
,简称juc
\quad
注:只能保证上锁的方法是线程安全的,如果是多个方法的组合,就不是线程安全的了,毕竟上锁只是给其中方法上了锁,方法内的代码具有原子性。如下示例:
三、synchronized
优化
\quad
synchronized
代码块是由一对 monitorenter/moniterexit 字节码指令实现, monitor 是其同步实现的基础, Java SE1.6 为了改善性能, 使得 JVM 会根据竞争情况, 使用如下 3 种不同的锁机制:
- 偏向锁
- 轻量级锁
- 重量级锁
\quad 上述这三种机制的切换是根据竞争激烈程度进行的, 在几乎无竞争的条件下, 会使用偏向锁, 在轻度竞争的条件下, 会由偏向锁升级为轻量级锁, 在重度竞争的情况下, 会升级到重量级锁。为了更好的理解这些锁,我们先学习几个前置知识:
前置知识1:java对象头
- 对象头包含了多种不同的信息, 其中就包含对象锁相关的信息。
- 无锁时对应标志位为01、轻量级锁位00,重量级锁位10
前置知识2:Monitor
- Monitor也称为监视器或者管程
- Monitor对象就相当于锁,每个Java对象都可以关联一个Monitor对象,不同对象关联不同的Monitor对象。如果使用
synchronized
给对象上锁后,该对象头的mark word就被设置为指向Monitor对象的指针
- 刚开始Monitor中owner为null
- 当Thread-2执行
synchronized(obj)
时就会把Monitor的所有者owner置为Thread-2,Monitor只能有一个owner - 在Thread-2上锁过程中,如果Thread-3等也来执行
synchronized(obj)
,就会进入EntryList - Thread-2执行完同步代码块中的内容,然后再唤醒EntryList中的线程来竞争锁
synchronized
必须进入同一个对象的monitor才有上述效果,不加synchronized
的对象不会关联监视器
前置知识3:cas操作
- CAS (Compare And Swap) 指令是一个CPU层级的原子性操作指令。 在 Intel 处理器中, 其汇编指令为 cmpxchg。
- 该指令概念上存在 3 个参数, 第一个参数【目标地址】, 第二个参数【值1】, 第三个参数【值2】, 指令会比较【目标地址存储的内容】和 【值1】 是否一致, 如果一致, 则将【值 2】 填写到【目标地址】, 其语义可以用如下的伪代码表示。
- 注意: 该指令是是原子性的, 也就是说 CPU 执行该指令时, 是不会被中断执行其他指令的
1、轻量级锁
\quad
如果一个对象虽然有多线程访问,但多线程访问的时间是错开的(没有竞争),可以使用轻量级锁来优化。轻量级锁对使用者透明,语法仍然是synchronized
,没有新的语法,由JVM根据竞争激烈成都来进行决定。假设有两个方法同步块,利用同一个对象加锁:
static final Obejct obj = new Object()
public static void method1(){
synchronized(obj){
// 同步块A
method2()
}
}
public static void method2(){
synchronized(obj){
// 同步块B
}
}
\quad 以上代码块的加锁原理如下:
- 1、在线程栈帧中创建锁记录对象(Lock Record),每个线程的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word,如下图所示:
- 2、让锁记录中Object reference指向要锁的对象Object(Java对象有很多字段,其中就有锁字段,具体信息请看上述关于Java对象头的讲解),并尝试用cas替换Object的Mark Word,讲Mark Word的值存入锁记录:
- 如果cas替换成功,对象头存储了锁记录地址和状态00,表示由该线程给对象加锁,如下:
- 如果cas失败,有两种情况:
- 如果是其它线程已经持有了该Object的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了
synchronized
锁重入,那么再添加一条Lock Record作为重入的计数,这叫synchronized
锁重入,加了几次锁就可以看Lock record数目
- 当退出
synchronized
代码块(解锁时)如果有取值null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一
- 当退出
synchronized
代码块解锁时锁记录不为null,这时cas将Mark Word的值恢复给对象头- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
2、锁膨胀
\quad 如果在尝试加轻量级锁的过程中,cas操作无法成功,这时一种情况就是有其他线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
- 当Thread1进行轻量级加锁时,Thread0已经对该队象加了轻量级锁
- 这时Thread1加轻量级锁失败,进入锁膨胀流程:为Object对象申请Monitor锁,让Object指向重量级锁地址,然后自己进入Monitor的EntryList
- Thread0退出同步解锁时,使用cas将Mark Word的值恢复给对象头,失败。这时会进入重量级锁流程,即根据Monitor地址找到Monitor对象,设置owner为努力了,唤醒EntryList中Thread1线程
3、自旋优化
\quad
重量级锁竞争的时候,可以使用自旋来进行优化,如果当前线程自旋成功,即这时持锁线程退出了同步块,释放了锁,这时当前线程就可以避免阻塞。因为阻塞需要线程上下文切换,耗费资源。
- Java6以后自旋锁是自适应的,自旋次数智能绝顶
- 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势
- 对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
4、偏向锁
\quad
轻量级锁在没有竞争,就自己这个线程,进行锁重入时仍然需要执行CAS操作。Java6引入偏向锁进一步优化:只有第一次使用CAS将线程ID设置到对象的Mark Word头,之后发现这个线程ID是自己的就表示没有竞争,不用重新CAS。以后只要不发生竞争,这个对象就归该线程所有。
\quad
一个对象开启时,默认开启偏向锁。
四、wait/notify
- 当owner对应的线程不满足条件,无法运行,也不能一直占着资源,这时候调用wait方法进入WaitSet
- Waiting状态是已经获得了锁,但是又放弃了锁进入wait的状态
- waiting状态线程会在owner对应线程调用notify或notifyAll时唤醒,唤醒后并不意味着立即获得锁,需要进入EntryList重新竞争
obj.wait()
:让进入Object锁的线程到WaitSet等待obj.wait(long n)
:会等待最多n毫秒,如果这个时间没有线程唤醒,则会自己唤醒obj.notify()
:让Object上正在WaitSet等待的线程中挑一个唤醒obj.notifyAll()
:让Object上正在WaitSet等待的全部线程唤醒- NOTE:这些方法都需要线程称为owner后才能调用
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final Object obj = new Object();
@Test
public void test() {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t2").start();
// 主线程两秒后执行
sleep(0.5);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
// obj.notify(); // 唤醒obj上一个线程
obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}
16:05:09.512 c.TestWaitNotify [t1] - 执行....
16:05:09.513 c.TestWaitNotify [t2] - 执行....
16:05:10.012 c.TestWaitNotify [main] - 唤醒 obj 上其它线程
16:05:10.012 c.TestWaitNotify [t1] - 其它代码....
16:05:10.012 c.TestWaitNotify [t2] - 其它代码....
sleep和wait的取别:
- sleep是Thread方法,而wait是Object的方法
- sleep不需要强制和
synchronized
配合使用,但wait需要和synchronized
一起使用 - sleep在睡眠时不会释放锁,wait在等待时需要释放锁
Park &Unpark
\quad 用于暂停当前线程和恢复某个线程的运行。
LockSupport.park();
LockSupport.unpark(需要恢复运行的线程)
public class TestParkUnpark {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
对于上述例子,线程t1先park,1s后主线程对其进行unpark,t1恢复运行,可以打印出resume…。假设我们在t1 park前sleep 2s,这样,就是主线程先对t1进行unpark,1s后t1 park,相当于先喝解药再喝毒药,这样能解毒吗?
public class TestParkUnpark {
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
log.debug("start...");
log.debug("park...");
sleep(2);
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
sleep(1);
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
\quad 答案是可以的。相当于先喝解药再喝毒药,也是可以解毒的。这是怎么实现的呢?
wait/notify 和 park/unpark 区别
- wait¬ify 必须要 Object Monitor 一起使用,而 park 与 unpark 不需要
- park&unpark是以确定的单个线程【堵塞】和【唤醒】线程的,而notify是随机唤醒同一把锁对象的线程的
- park不会释放锁资源,wait会释放锁资源
- park&unpark可以先unpark(park后直接就唤醒了),而wait¬ify不能提前notify
五、线程状态切换
NEW
:新建线程RUNNABLE
:运行状态、可运行状态和操作系统层面的阻塞状态都称为RUNNABLE,因此调用t1.start()后线程就进入运行状态BLOCKED
:java中三种阻塞状态之一,后续讲完锁后会详解WATING
:java中三种阻塞状态之一,后续讲完锁后会详解TIMED_WAITING
:java中三种阻塞状态之一,t1.sleep()会让线程进入该状态RERMINATED
1、RUNNABLE <—> WAITING
\quad
线程t用synchronized(obj)
获取了对象锁后,
- 调用
obj.wait()
方法,t线程从RUNNABLE --> WAITING
- 调用
obj.notify(), obj.notifyAll(), t.interrupt()
时:- 竞争锁成功,t线程从
WAITING --> RUNNABLE
- 竞争锁失败,t线程从
WAITING --> BLOCKED
- 竞争锁成功,t线程从
\quad
当前线程调用t.join()
方法,当前线程从 RUNNABLE --> WAITING
。当t线程运行结束或者被interrput
时,当前线程从WAITING --> RUNNABLE
。
\quad
当前线程从LockSupport.park()
方法会让当前线程从RUNNABLE --> WAITING
,当调用LockSupport.unpark(该线程)
或者被interrput
时,当前线程从WAITING --> RUNNABLE
。
2、RUNNABLE <—>TIME_WAITING
\quad
线程t用synchronized(obj)
获取了对象锁后,
- 调用
obj.wait(long n)
方法,t线程从RUNNABLE --> TIMED_WAITING
- t线程等待了n毫秒,或者调用
obj.notify(), obj.notifyAll(), t.interrupt()
时:- 竞争锁成功,t线程从
WAITING --> RUNNABLE
- 竞争锁失败,t线程从
WAITING --> BLOCKED
- 竞争锁成功,t线程从
\quad
当前线程调用t.join(long n)
方法,当前线程从 RUNNABLE -->TIMED_WAITING
。当前线程等待事件超过了n毫秒,或当t线程运行结束或者被interrput
时,当前线程从TIMED_WAITING --> RUNNABLE
。
\quad
当前线程从LockSupport.parkNanos(long nanos) 或者 LockSupport.parkUtil(long millis)
方法会让当前线程从RUNNABLE --> TIMED_WAITING
,当等待超时,或调用LockSupport.unpark(该线程)
或者被interrput
时,当前线程从TIMED_WAITING--> RUNNABLE
。
\quad
当前线程调用Thread.sleep(long n)
,当前线程从RUNNABLE -->TIMED_WAITING
;当前线程等待事件超过n毫秒,则当前线程从TIMED_WAITING--> RUNNABLE
。
3、RUNNABLE <—> BLOCKED
\quad
t线程用sychronized(obj)
获取了对象锁时如果竞争失败,从RUNNABLE-->BLOCKED
。当持有obj锁线程的同步代码块执行完毕,会唤醒该对象上所有BLOCKED
的线程重新竞争,如果t线程竞争成功,则从BLOCKED-->RUNNABLE
,其他失败的线程仍然BLOCKED
。
4、RUNNABLE --> TERMINATED
六、多把锁
public class TestMultiLock {
public static void main(String[] args) {
BigRoom bigRoom = new BigRoom();
new Thread(() -> {
bigRoom.study();
},"t1").start();
new Thread(() -> {
bigRoom.sleep();
},"t2").start();
}
}
class BigRoom {
public void sleep() {
synchronized (this) {
log.debug("sleeping 2 h");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (this) {
log.debug("study 1 h");
Sleeper.sleep(1);
}
}
}
19:36:07.027 c.BigRoom [t1] - study 1 h
19:36:08.031 c.BigRoom [t2] - sleeping 2 h
sleep和study都只有一个对象锁,两个任务不能同时进行,因此并发低。解决方法是准备多个对象锁,对Room类做如下改进:
class BigRoom {
private final Object studyRoom = new Object();
private final Object sleepRoom = new Object();
public void sleep() {
synchronized (sleepRoom) {
log.debug("sleeping 2 h");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (studyRoom) {
log.debug("study 1 h");
Sleeper.sleep(1);
}
}
}
19:39:19.206 c.BigRoom [t2] - sleeping 2 h
19:39:19.206 c.BigRoom [t1] - study 1 h
\quad 使用多把锁将锁的粒度细分,好处是可以增强并发度;坏处时如果一个线程需要同时获得多把锁,容易发生死锁。
1、死锁
\quad 多把锁的情况下可能会出现死锁,如下示例:
private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
19:44:47.059 c.TestDeadLock [t1] - lock A
19:44:47.077 c.TestDeadLock [t2] - lock B
2、活锁
\quad 两个线程互相改变对方结束条件,导致最后都无法结束。
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
3、饥饿
\quad 一个线程由于优先级太低,始终得不到cpu的调度执行,也不能结束。
七、ReentrantLock
\quad
reentrant
表示可重入,ReentrantLock
表示可重入锁,该锁与synchronized
有相似之处,但也有如下不同的特点:
- 可中断,意思是这个锁可以被打断,防止某个线程无限等待,是解决死锁的一种方式
- 可以设置超时时间,这样避免了一个线程永久霸占一个锁,可以避免死锁问题
- 可以设置为公平锁,防止线程饥饿的情况
- 支持多个条件变量,可以让正在阻塞中等待获取该锁的线程按照不同条件进入不同的entryList,可以按照不同条件唤醒不同entryList中的线程
- 与
synchronized
一样,都支持可重入
1、可重入
\quad 可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。意思是某个线程可以对一个对象反复加锁。
2、基本语法
reentrantLock.lock();
try{
// 临界区
}finally{
// 释放锁
reentrantLock.unlock();
}
fially保证即使出现异常也能释放锁。
public class TestReentrant {
private static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args){
lock.lock();
try {
log.debug("enter main");
m1(); // 测试是否可重入
}finally {
lock.unlock();
}
}
public static void m1(){
lock.lock();
try {
log.debug("enter m1");
}finally {
lock.unlock();
}
}
}
20:21:22.070 c.TestReentrant [main] - enter main
20:21:22.072 c.TestReentrant [main] - enter m1
3、可打断
\quad
在等待锁的过程中,其他线程可以用interrupt
终止该线程的等待。
public static void test1() {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
// 如果没有竞争则获得 lock 对象锁
// 如果有竞争则进入阻塞队列,可以被其他线程用 interrupt 打断
log.debug("try to get lock");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("do not get lock");
return;
}
try {
log.debug("get lock");
} finally {
lock.unlock();
}
}, "t1");
t1.start();
lock.lock(); // 主线程获得了锁,t1线程无法得到锁
}
主线程打断线程t1等待锁的过程,t1无法获得锁,直接return:
public static void test1() {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
// 如果没有竞争则获得 lock 对象锁
// 如果有竞争则进入阻塞队列,可以被其他线程用 interrupt 打断
log.debug("try to get lock");
lock.lockInterruptibly();
} catch (InterruptedException e) {
e.printStackTrace();
log.debug("do not get lock");
return;
}
try {
log.debug("get lock");
} finally {
lock.unlock();
}
}, "t1");
t1.start();
lock.lock(); // 主线程获得了锁,t1线程无法得到锁
log.debug("interrupt lock");
t1.interrupt();
}
4、锁超时
\quad 与可打断不同,锁超时指某个线程在等待获取锁的过程中最多等待指定时间,超时则自动放弃等待,也可以解决死锁问题。可打断是由其他线程来完成对某个线程终止无限等待的操作,是被动的。锁超时是自己超时主动放弃锁,是主动的。使用lock.tryLock()来实现,相当于自旋:
private static void test1() {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
log.debug("启动...");
try {
if (!lock.tryLock(1, TimeUnit.SECONDS)) {
log.debug("获取等待 1s 后失败,返回");
return;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
}
5、公平锁
其实使用tryLock的方式放弃锁也相当于实现了公平。公平锁其实会降低并发度,一般不用。
6、条件变量
八、控制线程执行顺序
\quad 如果需要线程2先执行,线程1后执行,可以使用wait/notify实现:
static final Object lock = new Object();
// 表示 t2 是否运行过
static boolean t2runned = false;
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (!t2runned) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("1");
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (lock) {
log.debug("2");
t2runned = true;
lock.notify();
}
}, "t2");
t1.start();
t2.start();
}
这样写对cpu压力很大,t1会一直自旋。也可以使用park/unpark实现:
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
LockSupport.park();
log.debug("1");
}, "t1");
t1.start();
new Thread(() -> {
log.debug("2");
LockSupport.unpark(t1);
},"t2").start();
}