LockSupport:线程阻塞与唤醒的精妙控制
大家好!今天我们来深入探讨Java并发编程中的一个强大工具:LockSupport
。它提供了一种比传统的wait/notify
机制更灵活、更底层的线程阻塞和唤醒机制。我们将从LockSupport
的基本概念、原理、使用方法,以及与wait/notify
的比较等方面进行详细讲解,并通过具体的代码示例来展示其强大的功能。
1. LockSupport
概述
LockSupport
是Java并发包java.util.concurrent
中的一个工具类,它提供了一组静态方法,用于阻塞和唤醒线程。它的核心功能是park
和unpark
。
-
park()
方法: 阻塞当前线程,除非获得许可证(permit)。如果调用park()
时,线程已经持有许可证,则park()
立即返回,并清除许可证。否则,线程将被阻塞,直到以下情况发生:- 其他线程调用
unpark(Thread)
方法,将当前线程作为参数,并给予其许可证。 - 当前线程被中断。
- 发生“虚假唤醒”(spurious wakeup)。
- 其他线程调用
unpark(Thread)
方法: 给予指定线程一个许可证。如果指定线程之前因为调用park()
方法而被阻塞,那么它会被唤醒,并获得许可证,park()
方法返回。如果指定线程尚未调用park()
方法,那么许可证会被记录下来,当下一次该线程调用park()
方法时,它会立即返回,而不会被阻塞。
LockSupport
的关键特性在于它允许在park()
之前调用unpark(Thread)
,这与wait/notify
机制有很大的不同。wait/notify
必须在synchronized块中使用,而且必须先调用wait()
,才能调用notify()
或notifyAll()
。
2. LockSupport
的原理
LockSupport
的底层实现依赖于操作系统的线程阻塞和唤醒机制。在Linux系统中,它通常使用pthread_cond_wait
和pthread_cond_signal
等函数。
LockSupport
内部维护了一个与每个线程关联的许可证(permit)。许可证可以看作是一个计数器,最多只能是1。
park()
操作: 如果许可证可用(值为1),则将其设置为0,并立即返回。如果许可证不可用(值为0),则线程进入阻塞状态,等待其他线程调用unpark(Thread)
方法来给予其许可证。unpark(Thread)
操作: 如果指定线程的许可证当前不可用(值为0),则将其设置为1,并唤醒该线程。如果指定线程的许可证已经可用(值为1),则不做任何操作。
LockSupport
并不强制要求在park()
和unpark(Thread)
之间建立任何形式的同步关系,这使得它比wait/notify
更加灵活。
3. LockSupport
的使用方法
LockSupport
提供了一系列静态方法,最常用的包括:
LockSupport.park()
:阻塞当前线程。LockSupport.park(Object blocker)
:阻塞当前线程,并记录阻塞对象(blocker)。这个blocker可以用来诊断死锁问题,在线程dump中可以看到。LockSupport.parkNanos(long nanos)
:阻塞当前线程指定的时间(纳秒)。LockSupport.parkNanos(Object blocker, long nanos)
:阻塞当前线程指定的时间(纳秒),并记录阻塞对象。LockSupport.parkUntil(long deadline)
:阻塞当前线程直到指定的截止时间(毫秒)。LockSupport.parkUntil(Object blocker, long deadline)
:阻塞当前线程直到指定的截止时间(毫秒),并记录阻塞对象。LockSupport.unpark(Thread thread)
:唤醒指定的线程。
下面是一些简单的示例:
示例1:基本的阻塞和唤醒
import java.util.concurrent.locks.LockSupport;
public class LockSupportExample {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("Thread is starting to park...");
LockSupport.park(); // 阻塞当前线程
System.out.println("Thread is unparked!");
});
t.start();
Thread.sleep(2000); // 等待线程启动
System.out.println("Main thread is unparking the thread...");
LockSupport.unpark(t); // 唤醒线程
}
}
在这个例子中,线程t
首先打印一条消息,然后调用LockSupport.park()
方法进入阻塞状态。主线程等待2秒后,调用LockSupport.unpark(t)
方法唤醒线程t
。线程t
被唤醒后,打印另一条消息并结束。
示例2:先unpark
后park
import java.util.concurrent.locks.LockSupport;
public class LockSupportExample2 {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
System.out.println("Thread is starting...");
LockSupport.park(); // 阻塞当前线程,但由于之前已经unpark,所以立即返回
System.out.println("Thread is running after park.");
});
System.out.println("Main thread is unparking the thread before it starts...");
LockSupport.unpark(t); // 唤醒线程,但线程尚未park
t.start();
t.join(); //等待线程执行完成
}
}
在这个例子中,主线程在线程t
启动之前就调用了LockSupport.unpark(t)
方法。当线程t
启动后,调用LockSupport.park()
方法时,由于它已经持有一个许可证,所以park()
方法立即返回,线程不会被阻塞。
示例3:使用blocker
诊断死锁
import java.util.concurrent.locks.LockSupport;
public class LockSupportExample3 {
private static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
synchronized (LOCK) {
System.out.println("Thread is starting to park...");
LockSupport.park(LOCK); // 阻塞当前线程,并记录阻塞对象LOCK
System.out.println("Thread is unparked!");
}
});
t.start();
Thread.sleep(2000);
System.out.println("Main thread is unparking the thread...");
LockSupport.unpark(t);
}
}
在这个例子中,线程t
在synchronized块中调用LockSupport.park(LOCK)
方法。如果线程t
因为某种原因无法被唤醒(例如,死锁),那么在线程dump中可以看到线程t
被阻塞在LockSupport.park(LOCK)
方法上,并且阻塞对象是LOCK
。这有助于诊断死锁问题。
4. LockSupport
与wait/notify
的比较
LockSupport
和wait/notify
都可以用于线程的阻塞和唤醒,但它们之间存在一些关键的区别:
特性 | wait/notify |
LockSupport |
---|---|---|
同步要求 | 必须在synchronized 块中使用。 |
不需要显式的同步。 |
许可证 | 没有许可证的概念。 | 每个线程关联一个许可证。 |
调用顺序 | 必须先调用wait() ,才能调用notify() /notifyAll() 。 |
可以先调用unpark(Thread) ,后调用park() 。 |
灵活性 | 较低,依赖于synchronized 的锁机制。 |
较高,可以更灵活地控制线程的阻塞和唤醒。 |
错误使用风险 | 容易出现notify() /notifyAll() 丢失信号的问题。 |
相对较低,因为unpark(Thread) 可以先于park() 调用。 |
虚假唤醒 | 需要处理虚假唤醒。 | 也会发生虚假唤醒,需要处理。 |
LockSupport
的优点:
- 更灵活:
LockSupport
不需要显式的同步,并且允许在park()
之前调用unpark(Thread)
,这使得它可以更灵活地控制线程的阻塞和唤醒。 - 更安全:
LockSupport
不容易出现notify()
/notifyAll()
丢失信号的问题,因为unpark(Thread)
可以先于park()
调用。 - 更底层:
LockSupport
提供了一种更底层的线程阻塞和唤醒机制,可以用于构建更高级的并发工具。
LockSupport
的缺点:
- 需要手动管理线程:
LockSupport
需要手动管理线程,例如,需要显式地调用unpark(Thread)
方法来唤醒线程。 - 可能出现虚假唤醒:
LockSupport
也会发生虚假唤醒,因此在使用时需要注意处理。
5. LockSupport
的应用场景
LockSupport
广泛应用于Java并发包中的各种并发工具,例如:
ReentrantLock
:ReentrantLock
的实现依赖于LockSupport
来阻塞和唤醒线程。Condition
:Condition
的实现也依赖于LockSupport
来阻塞和唤醒线程。ForkJoinPool
:ForkJoinPool
的实现使用LockSupport
来管理工作线程的阻塞和唤醒。CountDownLatch
、CyclicBarrier
、Semaphore
: 这些并发工具的底层实现也可能使用LockSupport
。
除了Java并发包中的并发工具之外,LockSupport
还可以用于构建自定义的并发工具,例如:
- 自定义的阻塞队列: 可以使用
LockSupport
来实现一个自定义的阻塞队列,该队列可以在队列为空时阻塞消费者线程,并在队列有数据时唤醒消费者线程。 - 自定义的锁: 可以使用
LockSupport
来实现一个自定义的锁,该锁可以提供比ReentrantLock
更灵活的锁定和解锁机制。
总而言之,LockSupport
是一个非常强大的工具,可以用于解决各种并发编程问题。
6. 注意事项和最佳实践
-
处理虚假唤醒: 尽管
LockSupport
不像wait/notify
那么容易出现信号丢失问题,但仍然可能发生虚假唤醒。因此,在使用LockSupport
时,应该在一个循环中调用park()
方法,并在循环中检查唤醒条件是否满足。while (!condition) { LockSupport.park(); }
-
避免死锁: 在使用
LockSupport
时,需要注意避免死锁。可以使用LockSupport.park(Object blocker)
方法来记录阻塞对象,以便在线程dump中诊断死锁问题。 -
选择合适的阻塞方式:
LockSupport
提供了多种阻塞方式,例如park()
、parkNanos()
、parkUntil()
。应该根据实际需求选择合适的阻塞方式。 -
避免过度使用: 虽然
LockSupport
非常灵活,但也不应该过度使用。在可以使用更高级的并发工具(例如ReentrantLock
、Condition
、Semaphore
)的情况下,应该优先使用这些工具。
7. 案例分析:使用LockSupport
实现简单的阻塞队列
下面我们通过一个具体的例子来展示如何使用LockSupport
实现一个简单的阻塞队列。
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.LockSupport;
public class BlockingQueue<T> {
private Queue<T> queue = new LinkedList<>();
private int capacity;
private Thread consumerWaiter;
private Thread producerWaiter;
public BlockingQueue(int capacity) {
this.capacity = capacity;
}
public synchronized void put(T element) throws InterruptedException {
while (queue.size() == capacity) {
producerWaiter = Thread.currentThread();
LockSupport.park(this); // 队列已满,阻塞生产者线程
}
queue.offer(element);
if (consumerWaiter != null) {
LockSupport.unpark(consumerWaiter); // 唤醒消费者线程
}
}
public synchronized T take() throws InterruptedException {
while (queue.isEmpty()) {
consumerWaiter = Thread.currentThread();
LockSupport.park(this); // 队列为空,阻塞消费者线程
}
T element = queue.poll();
if (producerWaiter != null) {
LockSupport.unpark(producerWaiter); // 唤醒生产者线程
}
return element;
}
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> queue = new BlockingQueue<>(10);
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
queue.put(i);
System.out.println("Produced: " + i);
Thread.sleep(100); // 模拟生产时间
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
Integer element = queue.take();
System.out.println("Consumed: " + element);
Thread.sleep(200); // 模拟消费时间
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producer.start();
consumer.start();
producer.join();
consumer.join();
}
}
在这个例子中,我们使用LockSupport
实现了put()
和take()
方法,用于阻塞和唤醒生产者和消费者线程。当队列已满时,生产者线程会被阻塞,直到队列有空闲空间。当队列为空时,消费者线程会被阻塞,直到队列有数据。
LockSupport
的灵活应用
LockSupport
相较于传统的wait/notify
提供了更灵活的线程控制机制。通过理解其底层原理和合理应用,可以构建更高效、更可靠的并发程序。需要注意的是,在使用时要充分考虑线程安全和死锁问题,并根据实际需求选择合适的阻塞和唤醒策略。