- 引言
- 一、LockSupport是什么?
- 二、为什么需要LockSupport?
- 三、核心API:park()与unpark()
- 四、LockSupport的特性与优势
- 五、实战应用:手写一个简易锁
- 六、总结与最佳实践
- 互动环节
引言
在Java并发编程的世界里,我们已经熟悉了synchronized、ReentrantLock、CountDownLatch等工具。但你是否想过,这些强大的同步工具底层是如何实现线程的阻塞与唤醒的?
答案是:LockSupport。这个看似简单的工具类,却是整个JUC包最底层的基石之一。它就像道路系统中的交警,能够精准地让任何线程"停下"(阻塞)或"放行"(唤醒)。今天,就让我们揭开它的神秘面纱。
一、LockSupport是什么?
java.util.concurrent.locks.LockSupport是一个线程阻塞工具类,所有方法都是静态方法,可以直接调用。它的核心功能就两个:
- 阻塞(park):让当前线程等待(阻塞)。
- 唤醒(unpark):唤醒一个被阻塞的指定线程。
它最核心的概念是 "许可证(permit)"。你可以把它想象成一个只有一个令牌的令牌桶,但逻辑与常规相反:
- unpark(thread):如果线程thread还没有许可证,则给它发放一个许可证。最多只能有一个。
- park():消耗掉当前线程的许可证(如果有的话),并立即返回。如果当前线程没有许可证,那么它就必须等待,直到有其他线程调用unpark给它发放许可证,或者被中断。
二、为什么需要LockSupport?
在LockSupport出现之前,我们主要使用Object.wait()和Object.notify()/notifyAll()来阻塞和唤醒线程。但这种方式有诸多限制:
- 必须在synchronized同步块中使用,否则会抛出IllegalMonitorStateException。
- wait()和notify()的调用顺序必须严格保证。如果先调用notify()再调用wait(),线程将永远无法被唤醒。
- notify()是随机唤醒,不能精确唤醒某个指定的线程。
LockSupport的出现完美地解决了这些问题,它为构建更高级的同步工具(如ReentrantLock、CountDownLatch等)提供了灵活、可靠的底层基础。
三、核心API:park()与unpark()
LockSupport的API非常简洁,最核心的就是以下几个方法:
import java.util.concurrent.locks.LockSupport; public class LockSupportDemo { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { System.out.println(Thread.currentThread().getName() + ": 开始执行,即将被阻塞..."); // 1. park() - 阻塞当前线程 // 如果此时有许可证,则消耗掉许可证并继续运行;如果没有,则等待。 LockSupport.park(); System.out.println(Thread.currentThread().getName() + ": 被唤醒了,继续执行!"); }, "示例线程"); thread.start(); // 主线程睡眠2秒,确保子线程先执行并先调用park() Thread.sleep(2000); System.out.println(Thread.currentThread().getName() + ": 准备唤醒子线程"); // 2. unpark(Thread thread) - 唤醒指定的线程 // 给`thread`线程发放一个许可证(如果它还没有的话)。 LockSupport.unpark(thread); } }
输出:
示例线程: 开始执行,即将被阻塞... main: 准备唤醒子线程 示例线程: 被唤醒了,继续执行!
其他常用方法:
- parkNanos(long nanos):阻塞当前线程,最长不超过指定的纳秒时间。超时后自动唤醒。
- parkUntil(long deadline):阻塞当前线程,直到某个绝对的截止时间(从1970年开始的毫秒数)。
- park(Object blocker):与park()功能相同,但允许传入一个blocker对象,用于记录线程被阻塞的原因,方便问题排查和监控(强烈推荐使用这种方式)。
四、LockSupport的特性与优势
- 调用顺序灵活:unpark()可以在park()之前调用。先发许可证,后park()会直接消耗许可证而不会阻塞。这解决了wait()/notify()的顺序死锁问题。
- java
- public static void main(String[] args) { Thread mainThread = Thread.currentThread(); // 先发放许可证 LockSupport.unpark(mainThread); System.out.println("先调用unpark"); // 再park,此时看到有许可证,直接消耗并返回,不会阻塞 LockSupport.park(); System.out.println("再调用park,也不会阻塞"); }
- 精确唤醒:unpark(Thread thread)可以精确指定要唤醒的线程,而不像notify()那样随机。
- 无需锁环境:可以在任何地方调用,不需要先获得某个对象的监视器锁。
- 可响应中断:线程在park()阻塞时,如果被其他线程中断(interrupt()),它会立即返回,但不会抛出InterruptedException。可以通过Thread.interrupted()方法检查中断标志。
五、实战应用:手写一个简易锁
理解了LockSupport,我们其实可以模仿AQS(
AbstractQueuedSynchronizer)的思路,实现一个非常简单的不可重入锁。
import java.util.concurrent.locks.LockSupport; import java.util.concurrent.atomic.AtomicReference; /** * 一个基于LockSupport的简易互斥锁(不可重入) */ public class MiniLock { // 使用原子引用,记录当前持有锁的线程 private final AtomicReference<Thread> owner = new AtomicReference<>(); // 等待队列,这里简单使用链表结构。AQS中是一个真正的CLH队列 private volatile Node waiters; private static class Node { final Thread thread; volatile Node next; Node(Thread thread) { this.thread = thread; } } public void lock() { Thread current = Thread.currentThread(); // 尝试通过CAS获取锁 while (!owner.compareAndSet(null, current)) { // 获取失败,将自己加入等待队列 Node node = new Node(current); node.next = waiters; waiters = node; // 然后park自己 LockSupport.park(this); // 传入this作为blocker // 被唤醒后,并不代表立刻拿到锁了,需要重新进入循环尝试CAS } // 成功获取锁,退出方法 } public void unlock() { Thread current = Thread.currentThread(); if (owner.compareAndSet(current, null)) { // 释放锁成功,需要唤醒等待队列中的一个线程 if (waiters != null) { // 这里简单唤醒队列中的第一个线程(非公平锁策略) Node first = waiters; if (first != null) { waiters = first.next; // 从队列中移除 LockSupport.unpark(first.thread); // 唤醒它 } } } } // 测试我们的MiniLock private static int count = 0; private static final MiniLock lock = new MiniLock(); public static void main(String[] args) throws InterruptedException { Runnable task = () -> { for (int i = 0; i < 10000; i++) { lock.lock(); try { count++; } finally { lock.unlock(); } } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Final count: " + count); // 正确输出 20000 } }
这个例子极大地简化了AQS的实现,但它清晰地展示了LockSupport如何作为构建更高级同步器的基石。
六、总结与最佳实践
- 底层基石:LockSupport是JUC包中许多高级同步工具(如ReentrantLock, CountDownLatch, Semaphore)的底层阻塞/唤醒机制。
- 核心机制:基于许可证(permit) 的逻辑,unpark发证,park消费证。
- 核心优势:
- 顺序无关性:unpark先于park调用也不会导致线程永久阻塞。
- 精确控制:可以指定要唤醒的线程。
- 灵活性:无需在同步块中调用。
- 最佳实践:
- 总是使用park(Object blocker):传入相关的同步对象(如this),这可以在使用jstack等工具诊断线程问题时,清晰地看到线程被哪个对象阻塞,极大提升调试效率。
- 在循环中检查条件:和传统的等待机制一样,被park唤醒后,必须重新检查等待条件是否真正满足,因为唤醒可能源于伪唤醒或超时。
- 理解中断响应:线程被中断后park会返回,但不会抛异常,记得检查中断状态。
LockSupport是Java并发工具箱中一把小巧而强大的"手术刀"。虽然我们在日常开发中直接使用它的场景不多,但理解其原理,能让我们对JUC包的整体理解上升到一个新的高度,也能在需要构建特定同步原语时得心应手。