在上一篇博客中,我们扒开了对象头,看透了 synchronized 这把隐形的锁。在简单的场景下,它确实好用。但在真实的后端高并发开发中,我们经常会遇到一些让人抓狂的需求:

  • “老板,这个线程等锁等了 10 秒了,能不能让它别等了,直接返回失败?”

  • “能不能只唤醒那个负责写数据库的线程,别把所有线程都叫醒?”

面对这些需求,死板的 synchronized 只能摊摊手表示无能为力。今天,我们就来解锁 Java 并发包(JUC)里的高级兵器库,彻底掌控线程的生杀大权!


一、 精准狙击手:LockSupport (park / unpark)

如果你用过原生的 Object.wait()notify(),你肯定体会过那种“盲人摸象”的痛苦:你只能通过 notifyAll() 把所有等待的线程全叫醒,根本无法精准控制。

1. 是什么?(概念与作用)

LockSupport 就像是工厂里的**“精准调度员”**。它不需要配合任何锁(Monitor)使用,而是直接作用于具体的某一个线程。

  • 特点 1:无门槛。不需要像 wait/notify 那样必须写在 synchronized 代码块里。

  • 特点 2:精准打击。你可以明确指名道姓地暂停或唤醒某一个线程(比如 unpark(线程A))。

  • 特点 3:允许“先发车票,再上车”。哪怕你先调用了唤醒(unpark),再去调用暂停(park),线程也不会被阻塞!

2. 怎么用?(代码示例)

Java

import java.util.concurrent.locks.LockSupport;

public class LockSupportDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            try { Thread.sleep(1000); } catch (Exception e) {} // 故意磨蹭1秒
            
            System.out.println("打工人:准备调用 park() 休息...");
            // 因为老板提前发了凭证,这里根本不会休眠,直接放行!
            LockSupport.park(); 
            System.out.println("打工人:手里有凭证,继续干活!");
        });

        worker.start();
        
        // 老板(主线程)动作极快,在员工 park 之前,就提前发了凭证
        System.out.println("老板:提前给打工人发通行证 (unpark)");
        LockSupport.unpark(worker); 
    }
}
/* 输出结果:
老板:提前给打工人发通行证 (unpark)
打工人:准备调用 park() 休息...
打工人:手里有凭证,继续干活!
*/

3. 重点细节:底层的秘密(cond / counter / mutex

为什么 LockSupport 能做到“先发车票后上车”?因为它的底层(C++ 实现)维护了一个包含三个关键属性的机制:

  • _counter(凭证): 这就是那张车票,它的值最多只能是 1,最少是 0。unpark 会把它变成 1,park 会检查它是不是 1,如果是,就消耗它(变成 0)并放行。

  • _mutex(互斥量)& _cond(条件变量):_counter 为 0 时,底层会利用操作系统级别的 mutexcond 让线程真正进入挂起休眠状态。


二、 锁的暗黑陷阱:活跃性问题(死锁 / 活锁 / 饥饿)

锁用得好是保护伞,用不好就是定时炸弹。在并发编程中,有三种让程序“生不如死”的状态。

1. 死锁 (Deadlock):互不相让的僵局

  • 场景: 张三拿着金钥匙,想要银钥匙;李四拿着银钥匙,想要金钥匙。两人互相死等,永远卡住。

  • 排查神技(面试必考): 线上环境发生死锁,千万别盲猜!直接用 JDK 自带的工具:

    1. 敲入 jps 命令,找出当前运行的 Java 进程号(PID)。

    2. 敲入 jstack <PID>,查看线程堆栈信息。如果是死锁,JVM 会在日志最后极其贴心地打印出:"Found one Java-level deadlock!"(找到一个死锁),并列出是哪两个线程在互相卡脖子。或者直接使用图形化界面 jconsole 也能一键检测死锁。

  • 破局方案: 保持顺序加锁。让张三和李四都必须先拿金钥匙,再拿银钥匙,僵局瞬间瓦解。

2. 活锁 (Livelock):谦让过头的尴尬

  • 场景: 两人在狭窄的走廊迎面碰上。为了让路,两人同时往左闪,又同时往右闪,一直在动,但就是谁也过不去。

  • 破局方案: 增加随机的睡眠时间。网络底层协议(如 CSMA/CD)处理冲突就是用的这招:撞车后,各自随机睡 1~5 毫秒再试,完美错开。

3. 饥饿 (Starvation):被遗忘的角落

  • 场景: 某个线程优先级太低,或者其他线程太霸道,导致它永远抢不到 CPU 执行权,活活饿死。


三、 终极兵器:ReentrantLock(可重入锁)

为了解决 synchronized 的死板,JUC 提供了 ReentrantLock。它不仅和 synchronized 一样是可重入的(自己加的锁,自己内部再调用其他加锁方法不会卡死自己),还多了四大超能力!

1. 是什么?(四大超能力)

  1. 可中断(解决死等): 线程在排队等锁的时候,可以被别人用 interrupt() 打断,直接抛出异常走人,不再死等。

  2. 可设置超时(解决死锁): tryLock(1, TimeUnit.SECONDS)。我就等 1 秒钟,拿到就干活,拿不到我就去干别的,绝不一根筋死磕。

  3. 可设置公平锁(解决饥饿): new ReentrantLock(true)。开启公平模式,所有线程必须乖乖排队,先来后到,杜绝有人插队导致老实人饿死。

  4. 多条件变量(解决盲目唤醒): 可以创建多个 Condition(WaitSet 等待室)。比如,你可以把所有等待写数据库的线程关进 ConditionA,读数据库的关进 ConditionB。唤醒时,可以精准只唤醒 A 里的线程(conditionA.signal())。

2. 怎么用?(基本语法与避坑)

⚠️ 绝对铁律: 使用 ReentrantLock必须、一定、千万要在 finally 块中手动释放锁 unlock()!否则一旦业务代码抛出异常,这把锁将永远无法释放,引发灾难!

Java

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    // 实例化一把普通(非公平)的可重入锁
    private static final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            // 超能力演示:尝试获取锁,最多等 2 秒
            boolean isLocked = false;
            try {
                System.out.println("t1:我尝试去拿锁...");
                isLocked = lock.tryLock(2, TimeUnit.SECONDS); 
                
                if (isLocked) {
                    System.out.println("t1:拿到锁了!开始处理核心业务(临界区)...");
                } else {
                    System.out.println("t1:等了 2 秒都没拿到,不等了,去执行备用逻辑!");
                }
            } catch (InterruptedException e) {
                System.out.println("t1:排队期间被打断了!");
            } finally {
                // ⚠️ 重点细节:只有真正拿到了锁,才需要释放锁!
                if (isLocked) {
                    lock.unlock(); 
                    System.out.println("t1:业务处理完毕,释放锁。");
                }
            }
        });

        // 主线程先抢占锁,死死抱住不放
        lock.lock();
        System.out.println("主线程:我先霸占这把锁 5 秒钟!");
        t1.start();
        
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {} finally {
            lock.unlock();
        }
    }
}
/* 输出结果:
主线程:我先霸占这把锁 5 秒钟!
t1:我尝试去拿锁...
t1:等了 2 秒都没拿到,不等了,去执行备用逻辑!
*/

四、 总结与对比

最后,我们用一张表来总结今天的主角 ReentrantLock 与老牌 synchronized 的对决:

特性 synchronized (关键字) ReentrantLock (JUC 类)
底层实现 JVM 层面 (C++ Monitor) API 层面 (Java 代码,基于 AQS)
锁的释放 发生异常或执行完毕,自动释放 必须手动finally 中调用 unlock()
灵活性 只能死等,不可中断 支持超时获取 tryLock、可中断
公平性 绝对的非公平锁 默认非公平,可传 true 开启公平锁
精准唤醒 只有一个 WaitSet,notifyAll 唤醒所有 可配置多个 Condition,精准唤醒

建议: 如果业务场景简单,优先使用 synchronized,因为代码简洁,且 JDK 团队一直在对其底层做优化(如锁升级)。但如果你的业务面临复杂的并发场景(如需要超时防止死锁、需要按条件精准唤醒),别犹豫,直接上 ReentrantLock

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐