JUC系列之《深入剖析LockSupport:Java并发编程的“交警”》

简介: LockSupport是Java并发编程的底层基石,提供park()和unpark()方法实现线程阻塞与精确唤醒。基于“许可证”机制,无需同步块、调用顺序灵活、可精准控制线程,是ReentrantLock、CountDownLatch等高级同步工具的底层支撑,堪称JUC的“手术刀”。

  • 引言
  • 一、LockSupport是什么?
  • 二、为什么需要LockSupport?
  • 三、核心API:park()与unpark()
  • 四、LockSupport的特性与优势
  • 五、实战应用:手写一个简易锁
  • 六、总结与最佳实践
  • 互动环节

引言

在Java并发编程的世界里,我们已经熟悉了synchronized、ReentrantLock、CountDownLatch等工具。但你是否想过,这些强大的同步工具底层是如何实现线程的阻塞与唤醒的?

答案是:LockSupport。这个看似简单的工具类,却是整个JUC包最底层的基石之一。它就像道路系统中的交警,能够精准地让任何线程"停下"(阻塞)或"放行"(唤醒)。今天,就让我们揭开它的神秘面纱。


一、LockSupport是什么?


java.util.concurrent.locks.LockSupport是一个线程阻塞工具类,所有方法都是静态方法,可以直接调用。它的核心功能就两个:

  1. 阻塞(park):让当前线程等待(阻塞)。
  2. 唤醒(unpark):唤醒一个被阻塞的指定线程。

它最核心的概念是 "许可证(permit)"。你可以把它想象成一个只有一个令牌的令牌桶,但逻辑与常规相反:

  • unpark(thread):如果线程thread还没有许可证,则给它发放一个许可证。最多只能有一个。
  • park():消耗掉当前线程的许可证(如果有的话),并立即返回。如果当前线程没有许可证,那么它就必须等待,直到有其他线程调用unpark给它发放许可证,或者被中断。

二、为什么需要LockSupport?

在LockSupport出现之前,我们主要使用Object.wait()和Object.notify()/notifyAll()来阻塞和唤醒线程。但这种方式有诸多限制:

  1. 必须在synchronized同步块中使用,否则会抛出IllegalMonitorStateException。
  2. wait()和notify()的调用顺序必须严格保证。如果先调用notify()再调用wait(),线程将永远无法被唤醒。
  3. 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的特性与优势

  1. 调用顺序灵活:unpark()可以在park()之前调用。先发许可证,后park()会直接消耗许可证而不会阻塞。这解决了wait()/notify()的顺序死锁问题。
  2. java
  3. public static void main(String[] args) { Thread mainThread = Thread.currentThread(); // 先发放许可证 LockSupport.unpark(mainThread); System.out.println("先调用unpark"); // 再park,此时看到有许可证,直接消耗并返回,不会阻塞 LockSupport.park(); System.out.println("再调用park,也不会阻塞"); }
  4. 精确唤醒:unpark(Thread thread)可以精确指定要唤醒的线程,而不像notify()那样随机。
  5. 无需锁环境:可以在任何地方调用,不需要先获得某个对象的监视器锁。
  6. 可响应中断:线程在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如何作为构建更高级同步器的基石。

六、总结与最佳实践

  1. 底层基石:LockSupport是JUC包中许多高级同步工具(如ReentrantLock, CountDownLatch, Semaphore)的底层阻塞/唤醒机制
  2. 核心机制:基于许可证(permit) 的逻辑,unpark发证,park消费证。
  3. 核心优势
  4. 顺序无关性:unpark先于park调用也不会导致线程永久阻塞。
  5. 精确控制:可以指定要唤醒的线程。
  6. 灵活性:无需在同步块中调用。
  7. 最佳实践
  8. 总是使用park(Object blocker):传入相关的同步对象(如this),这可以在使用jstack等工具诊断线程问题时,清晰地看到线程被哪个对象阻塞,极大提升调试效率。
  9. 在循环中检查条件:和传统的等待机制一样,被park唤醒后,必须重新检查等待条件是否真正满足,因为唤醒可能源于伪唤醒或超时。
  10. 理解中断响应:线程被中断后park会返回,但不会抛异常,记得检查中断状态。

LockSupport是Java并发工具箱中一把小巧而强大的"手术刀"。虽然我们在日常开发中直接使用它的场景不多,但理解其原理,能让我们对JUC包的整体理解上升到一个新的高度,也能在需要构建特定同步原语时得心应手。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一枚后端工程狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值