LockSupport.park()/unpark():实现比Object.wait/notify更灵活的线程阻塞与唤醒
大家好,今天我们来深入探讨Java并发编程中一个非常重要的工具:LockSupport。它提供了一种比传统的Object.wait()/notify()机制更加灵活和强大的线程阻塞与唤醒机制。我们将从Object.wait()/notify()的局限性出发,逐步深入理解LockSupport的工作原理、使用方法以及它带来的优势。
Object.wait()/notify()的局限性
Object.wait()/notify()是Java早期提供的线程同步机制,它允许线程在某个条件不满足时进入等待状态,并在条件满足时被其他线程唤醒。 然而,这种机制存在一些固有的局限性:
-
必须持有锁:
wait()和notify()/notifyAll()方法必须在synchronized块或方法中调用,这意味着线程必须先获得对象的锁才能进行等待或唤醒操作。这限制了它们的应用场景,并可能导致不必要的锁竞争。 -
容易出现虚假唤醒: 即使没有其他线程调用
notify(),wait()方法也可能意外地返回,这就是所谓的“虚假唤醒”(spurious wakeup)。因此,使用wait()时通常需要在一个循环中检查等待条件,以确保线程在满足条件时才继续执行。 -
先唤醒后等待问题: 如果一个线程在另一个线程调用
wait()之前调用了notify(),那么notify()信号将会丢失,导致等待线程永远无法被唤醒。
为了更清晰地展示这些问题,我们来看一个经典的生产者-消费者模型例子,并使用Object.wait()/notify()来实现:
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerWithWaitNotify {
private final Queue<Integer> buffer = new LinkedList<>();
private final int capacity = 10;
public synchronized void produce(int value) throws InterruptedException {
while (buffer.size() == capacity) {
System.out.println("Producer waiting, buffer is full.");
wait(); // 必须在synchronized块中调用
}
buffer.offer(value);
System.out.println("Produced: " + value);
notifyAll(); // 必须在synchronized块中调用
}
public synchronized void consume() throws InterruptedException {
while (buffer.isEmpty()) {
System.out.println("Consumer waiting, buffer is empty.");
wait(); // 必须在synchronized块中调用
}
int value = buffer.poll();
System.out.println("Consumed: " + value);
notifyAll(); // 必须在synchronized块中调用
}
public static void main(String[] args) {
ProducerConsumerWithWaitNotify pc = new ProducerConsumerWithWaitNotify();
new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
pc.produce(i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
pc.consume();
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();
}
}
在这个例子中,生产者和消费者都需要获得pc对象的锁才能进行生产或消费操作。 如果缓冲区已满或为空,线程会调用wait()进入等待状态,并在其他线程调用notifyAll()时被唤醒。可以看到,锁的竞争以及可能发生的虚假唤醒,都让代码变得复杂。
LockSupport的优势
LockSupport是Java并发包(java.util.concurrent)中提供的一个工具类,它提供了一组用于阻塞和唤醒线程的静态方法,而无需像Object.wait()/notify()那样依赖于对象的锁。 LockSupport的核心方法是park()和unpark()。
LockSupport的优势在于:
-
无需持有锁:
park()和unpark()方法不需要线程持有任何锁。 这使得它们可以用于更广泛的并发场景,并且避免了不必要的锁竞争。 -
避免虚假唤醒:
park()方法不会出现虚假唤醒的情况。 线程只有在被显式地unpark()或者被中断时才会返回。 -
支持先唤醒后等待: 如果一个线程在另一个线程调用
park()之前调用了unpark(),那么后续的park()调用会立即返回,而不会阻塞线程。 这解决了Object.wait()/notify()中可能出现的信号丢失问题。 -
更精细的控制:
LockSupport允许你针对特定的线程进行阻塞和唤醒,而不是像notifyAll()那样唤醒所有等待的线程,从而实现更精细的控制。
LockSupport的基本用法
LockSupport提供了以下主要方法:
| 方法 | 描述 |
|---|---|
park() |
阻塞当前线程。 |
park(Object blocker) |
阻塞当前线程,并记录阻塞对象。阻塞对象可以用于诊断和监控。 |
parkNanos(long nanos) |
阻塞当前线程,最多阻塞指定的纳秒数。 |
parkNanos(Object blocker, long nanos) |
阻塞当前线程,最多阻塞指定的纳秒数,并记录阻塞对象。 |
parkUntil(long deadline) |
阻塞当前线程,直到指定的绝对时间(毫秒)为止。 |
parkUntil(Object blocker, long deadline) |
阻塞当前线程,直到指定的绝对时间(毫秒)为止,并记录阻塞对象。 |
unpark(Thread thread) |
唤醒指定的线程。如果该线程之前调用了park()方法进入阻塞状态,那么它会被唤醒并继续执行。 |
getBlocker(Thread thread) |
获取指定线程的阻塞对象。 如果线程没有被阻塞,或者阻塞对象没有被设置,则返回null。 |
-
park()方法会阻塞当前线程,直到以下情况之一发生:- 当前线程被
unpark()。 - 当前线程被中断。
park()方法“无缘无故”地返回(这种情况很少见,属于伪唤醒,但通常可以忽略)。
- 当前线程被
-
unpark(Thread thread)方法会唤醒指定的线程。如果该线程之前调用了park()方法进入阻塞状态,那么它会被唤醒并继续执行。如果该线程还没有调用park()方法,那么下次调用park()方法时会立即返回,而不会阻塞线程。
下面是一个简单的例子,演示了LockSupport的基本用法:
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 starting");
LockSupport.park("Waiting for unpark"); // 阻塞线程,并设置阻塞对象
System.out.println("Thread resumed");
});
t.start();
Thread.sleep(2000); // 等待线程启动并进入阻塞状态
System.out.println("Main thread unparking the thread");
LockSupport.unpark(t); // 唤醒线程
t.join(); // 等待线程结束
System.out.println("Main thread exiting");
}
}
在这个例子中,线程t首先打印"Thread starting",然后调用LockSupport.park()进入阻塞状态。 主线程在等待2秒后,调用LockSupport.unpark(t)唤醒线程t。 线程t被唤醒后,打印"Thread resumed",然后结束。
使用LockSupport改进生产者-消费者模型
现在,我们使用LockSupport来改进之前的生产者-消费者模型:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.LockSupport;
public class ProducerConsumerWithLockSupport {
private final Queue<Integer> buffer = new LinkedList<>();
private final int capacity = 10;
private final Thread producerThread;
private final Thread consumerThread;
public ProducerConsumerWithLockSupport() {
producerThread = Thread.currentThread();
consumerThread = null;
}
public void produce(int value) throws InterruptedException {
synchronized (buffer) {
while (buffer.size() == capacity) {
System.out.println("Producer waiting, buffer is full.");
LockSupport.park(producerThread); // 阻塞生产者线程
}
buffer.offer(value);
System.out.println("Produced: " + value);
LockSupport.unpark(consumerThread); // 唤醒消费者线程
}
}
public void consume() throws InterruptedException {
synchronized (buffer) {
while (buffer.isEmpty()) {
System.out.println("Consumer waiting, buffer is empty.");
LockSupport.park(consumerThread); // 阻塞消费者线程
}
int value = buffer.poll();
System.out.println("Consumed: " + value);
LockSupport.unpark(producerThread); // 唤醒生产者线程
}
}
public static void main(String[] args) {
ProducerConsumerWithLockSupport pc = new ProducerConsumerWithLockSupport();
Thread producer = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
pc.produce(i);
Thread.sleep(100);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 20; i++) {
pc.consume();
Thread.sleep(200);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 设置线程引用
pc.setProducerThread(producer);
pc.setConsumerThread(consumer);
producer.start();
consumer.start();
}
// Setter方法设置线程引用
public void setProducerThread(Thread producerThread) {
this.producerThread = producerThread;
}
public void setConsumerThread(Thread consumerThread) {
this.consumerThread = consumerThread;
}
}
在这个改进后的例子中,我们仍然使用synchronized块来保护共享的缓冲区,但是使用LockSupport.park()和LockSupport.unpark()来阻塞和唤醒生产者和消费者线程。 这样做的好处是,我们可以精确地控制哪个线程被唤醒,避免了notifyAll()可能导致的无谓的线程竞争。
需要注意的是,由于LockSupport不与任何锁关联,因此我们需要使用额外的机制(例如synchronized块)来保证对共享资源的互斥访问。
LockSupport在AQS中的应用
LockSupport在Java并发包中的一个重要应用是在AbstractQueuedSynchronizer(AQS)框架中。 AQS是许多并发工具(例如ReentrantLock、Semaphore、CountDownLatch等)的基础。 AQS使用LockSupport来实现线程的阻塞和唤醒,以及管理等待队列。
AQS的核心思想是使用一个volatile int state变量来表示同步状态,并使用一个FIFO队列来管理等待获取锁的线程。 当一个线程尝试获取锁时,如果state变量表示锁已经被占用,那么该线程会被添加到等待队列中,并调用LockSupport.park()进入阻塞状态。 当持有锁的线程释放锁时,它会从等待队列中唤醒一个或多个线程,使它们有机会再次尝试获取锁。
AQS使用LockSupport的优点在于:
-
非阻塞的等待队列管理: AQS的等待队列是基于CAS(Compare-and-Swap)操作实现的,不需要使用锁来保护队列的访问。 这避免了锁竞争,提高了并发性能。
-
灵活的线程唤醒策略: AQS可以根据不同的同步器类型选择不同的线程唤醒策略。 例如,
ReentrantLock通常只唤醒等待队列中的第一个线程,而Semaphore可以唤醒多个线程。 -
支持中断: AQS支持线程在等待获取锁的过程中被中断。 当一个线程被中断时,AQS会将其从等待队列中移除,并抛出一个
InterruptedException。
总结LockSupport
LockSupport提供了一种比Object.wait()/notify()更加灵活和强大的线程阻塞与唤醒机制。它无需持有锁,避免了虚假唤醒,支持先唤醒后等待,并且可以实现更精细的线程控制。LockSupport广泛应用于Java并发包中的各种并发工具,例如AQS框架。 理解LockSupport的工作原理和使用方法,对于编写高性能的并发程序至关重要。
关于 park(Object blocker)
park(Object blocker) 的 blocker 参数的主要作用是方便诊断线程被阻塞的原因。 JVM 提供了工具和 API,允许你在线程被阻塞时,查看其 blocker 对象。 这对于分析死锁、长时间阻塞等问题非常有帮助。 在实际应用中,你可以将 blocker 设置为与阻塞原因相关的对象,例如锁对象、IO 通道等。
LockSupport的优势和应用场景
LockSupport相比Object.wait()/notify(),提供了更细粒度的线程控制,避免了不必要的锁竞争。- AQS框架的实现大量依赖于
LockSupport,例如ReentrantLock、Semaphore等。 - 在自定义并发工具类时,可以利用
LockSupport实现线程的阻塞和唤醒,提高代码的灵活性和性能。