了解一下 LockSupport
LockSupport
是一个类,位于java.util.concurrent.locks
包中,提供了基本的线程同步机制。
LockSupport
的主要作用是挂起和唤醒线程。它提供了两个主要的静态方法:park()
和unpark()
。
- park():用于挂起当前线程。如果调用
park()
的线程已经被unpark()
,或者线程被中断,那么调用park()
时线程不会阻塞。 - unpark(Thread thread):用于唤醒指定的线程。如果该线程在调用
unpark()
时已经处于挂起状态,那么它会被唤醒。如果该线程还没有进入挂起状态,那么下一次调用park()
时不会阻塞。
LockSupport
就是用来创建锁和其他同步类的基本线程阻塞原语。
三种让线程等待和唤醒的方法
我们知道,使用Object
的wait()
和notify()
方法,可以实现基本的线程等待和唤醒。
并发包(java.util.concurrent
)下Condition
对象的await()
和signal()
方法也可以实现线程等待和唤醒。
但是,wait()
、notify()
必须在同步块或同步方法中调用,否则会抛出IllegalMonitorStateException
。类似的,调用Condition
的await()
和signal()
方法也需要获取相关Lock对象的锁的情况下才能调用,否则会同样会抛出IllegalMonitorStateException
。
另外,如果我们先调用notify()
,然后再调用wait()
。在这种情况下,notify()
被调用时没有线程在等待,所以没有线程会被唤醒,之后当线程调用wait()
时,它会进入等待状态(阻塞了)。
public class WaitNotifyDemo {
static Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 让 Thread A 稍后运行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " 开始");
try {
lock.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + " 被唤醒!");
}, "Thread A").start();
new Thread(() -> {
synchronized (lock) {
lock.notify();
System.out.println(Thread.currentThread().getName() + " 唤醒操作");
}
}, "Thread B").start();
}
}
运行效果:
Thread B 唤醒操作
Thread A 开始
可以看到,线程 A 一直处于阻塞状态,等待其他线程再次调用notify()
。
那如果是用LockSupport
的park()
和unpark()
,就不会有上述问题。
import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo {
// LockSupport 不用必须在同步块或同步方法中调用
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 开始");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " 被唤醒!");
}, "Thread A");
Thread threadB = new Thread(() -> {
LockSupport.unpark(threadA);
System.out.println(Thread.currentThread().getName() + " 唤醒操作");
}, "Thread B");
threadA.start();
threadB.start();
}
}
即使是先唤醒后等待,使用 LockSupport
也没有问题:
import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
// 让 Thread A 稍后运行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 开始");
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " 被唤醒!");
}, "Thread A");
Thread threadB = new Thread(() -> {
LockSupport.unpark(threadA);
System.out.println(Thread.currentThread().getName() + " 唤醒操作");
}, "Thread B");
threadA.start();
threadB.start();
}
}
运行效果:
Thread B 唤醒操作
Thread A 开始
Thread A 被唤醒!
关键点
说白了,LockSupport
提供了静态方法park()
和unpark()
方法来实现阻塞线程和解除线程阻塞的过程。
LockSupport
类使用了一种名为Permit
(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit
),permit
只有两个值1和零,默认是零。permit
相当于1,0的开关。
permit
的默认值为0
。调用一次unpark
就加1,调用一次park
会消费permit
,也就是将1变成0,同时park
立即返回。此时如果再次调用park
会变成阻塞,调用unpark
就又会把permit
置为1。
需要注意的是,每个线程都有一个相关的permit
, permit
最多只有一个,重复调用unpark
也不会积累凭证。
官网是这么写的:
为什么在LockSupport类中,我们可以先唤醒一个线程后再让它阻塞?
这是因为LockSupport
的工作原理基于许可(permit)的概念。
当我们调用unpark
方法时,如果相关线程还没有许可,那么它会获得一个许可。然后,当我们在之后调用park
方法时,如果该线程已经有了许可,那么它会立即消费这个许可并立即返回,而不会阻塞。因此,即使我们先唤醒线程(即先调用unpark
方法),然后再让它阻塞(调用park
方法),线程也不会真正阻塞,因为它已经有了一个许可可以消费。
那为什么唤醒两次后阻塞两次,最终结果还是会阻塞线程?
这是因为LockSupport
的许可(permit)不具备累加性。
无论我们调用多少次unpark
方法,它只会给线程一个许可(将permit置为1)。
当我们连续两次调用park
方法时,第一次调用会消费掉这个许可,然后第二次调用park
方法时,由于没有可用的许可,线程会被阻塞。因此,即使我们先连续两次唤醒线程,然后再连续两次让它阻塞,线程最终还是会被阻塞。
下面的代码演示了使用 LockSupport 类唤醒了两次A线程后阻塞两次,结果A线程会阻塞:
import java.util.concurrent.locks.LockSupport;
public class LockSupportDemo {
public static void main(String[] args) {
Thread threadA = new Thread(() -> {
// 让 Thread A 稍后运行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 开始");
LockSupport.park();
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " 被唤醒!");
}, "Thread A");
Thread threadB = new Thread(() -> {
LockSupport.unpark(threadA);
LockSupport.unpark(threadA);
System.out.println(Thread.currentThread().getName() + " 唤醒操作");
}, "Thread B");
threadA.start();
threadB.start();
}
}