LockSupport.park()/unpark():实现比Object.wait/notify更灵活的线程阻塞与唤醒

LockSupport.park()/unpark():实现比Object.wait/notify更灵活的线程阻塞与唤醒

大家好,今天我们来深入探讨Java并发编程中一个非常重要的工具:LockSupport。它提供了一种比传统的Object.wait()/notify()机制更加灵活和强大的线程阻塞与唤醒机制。我们将从Object.wait()/notify()的局限性出发,逐步深入理解LockSupport的工作原理、使用方法以及它带来的优势。

Object.wait()/notify()的局限性

Object.wait()/notify()是Java早期提供的线程同步机制,它允许线程在某个条件不满足时进入等待状态,并在条件满足时被其他线程唤醒。 然而,这种机制存在一些固有的局限性:

  1. 必须持有锁: wait()notify()/notifyAll()方法必须在synchronized块或方法中调用,这意味着线程必须先获得对象的锁才能进行等待或唤醒操作。这限制了它们的应用场景,并可能导致不必要的锁竞争。

  2. 容易出现虚假唤醒: 即使没有其他线程调用notify()wait()方法也可能意外地返回,这就是所谓的“虚假唤醒”(spurious wakeup)。因此,使用wait()时通常需要在一个循环中检查等待条件,以确保线程在满足条件时才继续执行。

  3. 先唤醒后等待问题: 如果一个线程在另一个线程调用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的优势在于:

  1. 无需持有锁: park()unpark()方法不需要线程持有任何锁。 这使得它们可以用于更广泛的并发场景,并且避免了不必要的锁竞争。

  2. 避免虚假唤醒: park()方法不会出现虚假唤醒的情况。 线程只有在被显式地unpark()或者被中断时才会返回。

  3. 支持先唤醒后等待: 如果一个线程在另一个线程调用park()之前调用了unpark(),那么后续的park()调用会立即返回,而不会阻塞线程。 这解决了Object.wait()/notify()中可能出现的信号丢失问题。

  4. 更精细的控制: 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是许多并发工具(例如ReentrantLockSemaphoreCountDownLatch等)的基础。 AQS使用LockSupport来实现线程的阻塞和唤醒,以及管理等待队列。

AQS的核心思想是使用一个volatile int state变量来表示同步状态,并使用一个FIFO队列来管理等待获取锁的线程。 当一个线程尝试获取锁时,如果state变量表示锁已经被占用,那么该线程会被添加到等待队列中,并调用LockSupport.park()进入阻塞状态。 当持有锁的线程释放锁时,它会从等待队列中唤醒一个或多个线程,使它们有机会再次尝试获取锁。

AQS使用LockSupport的优点在于:

  1. 非阻塞的等待队列管理: AQS的等待队列是基于CAS(Compare-and-Swap)操作实现的,不需要使用锁来保护队列的访问。 这避免了锁竞争,提高了并发性能。

  2. 灵活的线程唤醒策略: AQS可以根据不同的同步器类型选择不同的线程唤醒策略。 例如,ReentrantLock通常只唤醒等待队列中的第一个线程,而Semaphore可以唤醒多个线程。

  3. 支持中断: 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,例如ReentrantLockSemaphore等。
  • 在自定义并发工具类时,可以利用LockSupport实现线程的阻塞和唤醒,提高代码的灵活性和性能。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注