park()和unpark()方法,你知道是什么吗?

418 阅读6分钟

背景

并发编程学习中,看到了使用park()和unpark()方法,自己之前没见过,于是搜索了下,这两个方法的作用,先将搜索结果,整理如下:

并发编程中的park和unpark方法详解

unpark() 是 Java 中用于线程调度的一个重要方法,它与 park() 方法一起使用,提供了一种轻量级的线程阻塞和唤醒机制。unpark()park() 是 Java 线程库中底层的同步原语,通常由更高层次的同步工具(如 LockConditionSemaphore 等)内部使用,但也可以在自定义的同步逻辑中直接调用。

1. unpark()park() 的基本概念

  • unpark(Thread thread):该方法用于唤醒一个被 park() 阻塞的线程。它会将指定的线程从阻塞状态中移除,使其恢复执行。如果该线程尚未调用 park(),则 unpark() 会在后续的 park() 调用中立即生效。

  • park():该方法用于阻塞当前线程,直到发生以下情况之一:

    • 另一个线程调用了 unpark() 来唤醒当前线程。
    • 指定了超时时间,且超时时间已到。
    • 线程被中断。

park()unpark() 是成对使用的,类似于 wait()notify(),但它们提供了更灵活的控制方式,并且可以在任意线程之间进行阻塞和唤醒操作,而不仅仅是持有锁的线程。

2. unpark() 的工作原理

unpark() 的核心功能是为指定的线程设置一个“许可”(permit),当该线程调用 park() 时,它会检查是否有可用的许可。如果有许可,则线程不会被阻塞,而是继续执行;如果没有许可,则线程会被阻塞,直到另一个线程调用 unpark() 为其提供许可。

许可的工作流程:

  1. 初始状态:每个线程都有一个许可计数器,初始值为 0。
  2. unpark() 调用:当 unpark() 被调用时,它会为指定的线程增加一个许可(将许可计数器加 1)。如果该线程已经处于阻塞状态(即调用了 park()),则它会立即被唤醒并继续执行。
  3. park() 调用:当线程调用 park() 时,它会检查是否有可用的许可。如果有许可,则线程消耗一个许可(将许可计数器减 1)并继续执行;如果没有许可,则线程会被阻塞,直到另一个线程调用 unpark() 为其提供许可。

许可的特性:

  • 许可是不可重入的:即使多次调用 unpark(),线程在调用 park() 时也只会消耗一个许可。也就是说,unpark() 的调用次数并不会累积,每次 park() 只会消耗一个许可。
  • 许可可以提前设置unpark() 可以在 park() 之前调用,这样当线程后续调用 park() 时,它会立即获得许可并继续执行,而不会被阻塞。

3. unpark()park() 的使用场景

unpark()park() 主要用于实现自定义的同步机制,尤其是在需要精确控制线程阻塞和唤醒的情况下。它们比传统的 wait()notify() 更加灵活,因为它们不需要依赖对象锁,并且可以在任意线程之间进行阻塞和唤醒操作。

典型使用场景:

  • 线程池:Java 的线程池(如 ThreadPoolExecutor)内部使用了 park()unpark() 来管理任务的提交和执行。当没有任务时,工作线程会调用 park() 进入阻塞状态,等待新的任务到来。当有新任务提交时,线程池会调用 unpark() 唤醒一个阻塞的工作线程来处理任务。

  • 条件变量LockCondition 类(如 ReentrantLockCondition)内部使用了 park()unpark() 来实现线程的等待和通知。await() 方法会调用 park() 来阻塞当前线程,而 signal() 方法会调用 unpark() 来唤醒一个等待的线程。

  • 自定义同步器:如果你需要实现自己的同步器或锁机制,park()unpark() 提供了底层的阻塞和唤醒功能,可以帮助你构建更复杂的同步逻辑。

4. unpark()park() 的 API

unpark()park() 是通过 java.util.concurrent.locks.LockSupport 类提供的静态方法。以下是这两个方法的签名:

public class LockSupport {
    // 唤醒指定的线程
    public static void unpark(Thread thread);

    // 阻塞当前线程,直到被唤醒或超时
    public static void park();

    // 阻塞当前线程,直到被唤醒或超时到达
    public static void parkNanos(long nanos);

    // 阻塞当前线程,直到被唤醒或指定的时间到达
    public static void parkUntil(long deadline);
}
  • unpark(Thread thread):唤醒指定的线程。如果该线程尚未调用 park(),则许可会被保存,直到后续的 park() 调用时生效。

  • park():阻塞当前线程,直到被 unpark() 唤醒或线程被中断。

  • parkNanos(long nanos):阻塞当前线程,直到被 unpark() 唤醒或指定的纳秒时间过去。

  • parkUntil(long deadline):阻塞当前线程,直到被 unpark() 唤醒或指定的时间戳到达。

5. 示例代码

以下是一个简单的示例,展示了如何使用 unpark()park() 来实现线程的阻塞和唤醒:

import java.util.concurrent.locks.LockSupport;

public class ParkUnparkExample {

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("Thread T1 is about to park...");
            LockSupport.park();  // 阻塞当前线程
            System.out.println("Thread T1 has been unparked!");
        });

        t1.start();

        // 让主线程稍微等待一下,确保 T1 已经调用了 park()
        Thread.sleep(1000);

        // 唤醒 T1 线程
        System.out.println("Main thread is going to unpark T1...");
        LockSupport.unpark(t1);

        // 等待 T1 线程结束
        t1.join();
    }
}

输出结果:

Thread T1 is about to park...
Main thread is going to unpark T1...
Thread T1 has been unparked!

在这个示例中:

  • T1 线程调用 LockSupport.park() 后进入阻塞状态,等待被唤醒。
  • 主线程调用 LockSupport.unpark(t1) 唤醒了 T1 线程,T1 线程随后继续执行并打印出消息。

6. unpark()park() 的优点

  • 灵活性unpark()park() 不依赖于对象锁,因此可以在任意线程之间进行阻塞和唤醒操作,提供了更大的灵活性。
  • 性能:相比于 wait()notify()park()unpark() 的开销较小,适合高性能的并发编程。
  • 精确控制park()unpark() 提供了更细粒度的控制,允许你在特定条件下阻塞和唤醒线程,而不必依赖锁的状态。

7. 注意事项

  • 不要滥用:虽然 unpark()park() 提供了强大的线程控制能力,但在大多数情况下,你应该优先使用更高层次的同步工具(如 LockConditionSemaphore 等),除非你确实需要自定义的同步逻辑。
  • 许可的非重入性unpark() 提供的许可是不可重入的,这意味着即使多次调用 unpark(),线程在调用 park() 时也只会消耗一个许可。因此,你不应该依赖多次 unpark() 来实现多次 park() 的唤醒。
  • 线程安全unpark()park() 是线程安全的,但你需要确保在多线程环境中正确使用它们,以避免死锁或其他并发问题。

8. 总结

unpark() 是 Java 中用于唤醒被 park() 阻塞的线程的方法。它与 park() 一起构成了一个轻量级的线程阻塞和唤醒机制,广泛应用于 Java 的并发库中,尤其是在线程池、锁和条件变量的实现中。通过 unpark()park(),你可以实现更灵活和高效的线程调度,但需要注意它们的使用场景和潜在的风险。