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

简介: LockSupport是Java并发编程的底层基石,提供park()和unpark()方法实现线程阻塞与精确唤醒。基于“许可证”机制,无需同步块、调用顺序灵活、可精准控制线程,是ReentrantLock、CountDownLatch等高级同步工具的底层支撑,堪称JUC的“手术刀”。
  • 引言
  • 一、LockSupport是什么?
  • 二、为什么需要LockSupport?
  • 三、核心API:park()与unpark()
  • 四、LockSupport的特性与优势
  • 五、实战应用:手写一个简易锁
  • 六、总结与最佳实践
  • 互动环节

引言

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

答案是: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的出现完美地解决了这些问题,它为构建更高级的同步工具(如ReentrantLockCountDownLatch等)提供了灵活、可靠的底层基础。

三、核心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包的整体理解上升到一个新的高度,也能在需要构建特定同步原语时得心应手。


相关文章
|
5月前
|
Java API 开发者
告别“线程泄露”:《聊聊如何优雅地关闭线程池》
本文深入讲解Java线程池优雅关闭的核心方法与最佳实践,通过shutdown()、awaitTermination()和shutdownNow()的组合使用,确保任务不丢失、线程不泄露,助力构建高可靠并发应用。
|
5月前
|
缓存 负载均衡 并行计算
JUC系列之《ForkJoinPool:分而治之的并发编程艺术 》
本文深入解析Java并发编程利器ForkJoinPool,涵盖分治思想、工作窃取算法、核心架构及实战应用。通过数组求和与文件处理案例,详解任务拆分与合并技巧,并剖析其高性能背后的双端队列与负载均衡机制,助你掌握并行计算最佳实践。
|
5月前
|
存储 算法 安全
《Java集合核心HashMap:深入剖析其原理、陷阱与性能优化》
HashMap是Java中最常用的Map实现,基于哈希表提供近乎O(1)的存取效率。其核心为“数组+链表+红黑树”结构,通过扰动哈希、&运算索引、扩容机制等实现高效操作。但线程不安全,需注意Key的不可变性与合理初始化容量。深入理解其原理,有助于写出高性能代码,避免常见陷阱。
|
5月前
|
算法 安全 Java
深入理解JVM《CMS收集器详解》
CMS收集器以降低停顿时间为目标,通过并发标记-清除实现低延迟,适用于交互式应用。其采用三色标记法与增量更新确保并发安全,利用卡表优化重新标记效率。虽存在浮动垃圾、内存碎片及资源敏感等缺陷,且已被G1、ZGC取代,但其核心思想对现代GC仍具重要参考价值。
|
5月前
|
存储 安全 Java
JUC系列之《深入理解synchronized:Java并发编程的基石 》
本文深入解析Java中synchronized关键字的使用与原理,涵盖其三种用法、底层Monitor机制、锁升级过程及JVM优化,并对比Lock差异,结合volatile应用场景,全面掌握线程安全核心知识。
|
5月前
|
Web App开发 安全 Java
并发编程之《彻底搞懂Java线程》
本文系统讲解Java并发编程核心知识,涵盖线程概念、创建方式、线程安全、JUC工具集(线程池、并发集合、同步辅助类)及原子类原理,帮助开发者构建完整的并发知识体系。
|
5月前
|
消息中间件 监控 Java
《聊聊线程池中线程数量》:不多不少,刚刚好的艺术
本文深入探讨Java线程池的核心参数与线程数配置策略,结合CPU密集型与I/O密集型任务特点,提供理论公式与实战示例,帮助开发者科学设定线程数,提升系统性能。
|
5月前
|
存储 算法 搜索推荐
《数据之美》:Java数据结构与算法精要
本系列深入探讨数据结构与算法的核心原理及Java实现,涵盖线性与非线性结构、常用算法分类、复杂度分析及集合框架应用,助你提升程序效率,掌握编程底层逻辑。
|
前端开发 Java C++
JUC系列之《CompletableFuture:Java异步编程的终极武器》
本文深入解析Java 8引入的CompletableFuture,对比传统Future的局限,详解其非阻塞回调、链式编排、多任务组合及异常处理等核心功能,结合实战示例展示异步编程的最佳实践,助你构建高效、响应式的Java应用。
|
5月前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。

热门文章

最新文章