从零起步学习并发编程 || 第六章:ReentrantLock与synchronized 的辨析及运用

一、ReentrantLock 是什么?

ReentrantLock 是 Java.util.concurrent.locks 包下的可重入独占锁,从字面意思拆解:

  • Reentrant(可重入):同一个线程可以多次获取同一把锁,不会因为自己持有锁而阻塞(比如递归调用中加锁不会死锁)。
  • Lock(锁):它是对 synchronized 关键字的补充和增强,通过编程式的方式实现锁的获取与释放。
1. 核心特性
  • 可重入:和 synchronized 一样,支持同一线程重复加锁,锁会记录 "加锁次数",释放次数需和加锁次数一致才会真正释放。
  • 显式锁 :必须手动调用 lock() 获取锁,手动调用 unlock() 释放锁(通常放在 finally 块中,避免异常导致锁无法释放)。
  • 公平 / 非公平锁
    • 非公平锁(默认):线程获取锁时不按等待顺序,可能 "插队",性能更高(和 synchronized 一致)。
    • 公平锁:线程按等待队列的顺序获取锁,避免 "饥饿",但性能略低(通过构造函数 new ReentrantLock(true) 开启)。
  • 可中断 :支持通过 lockInterruptibly() 响应线程中断,避免线程无限等待锁。
  • 超时获取 :支持 tryLock(long time, TimeUnit unit),在指定时间内获取不到锁则返回 false,不会永久阻塞。
2. 基础使用示例
java 复制代码
import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockDemo {
    // 创建非公平锁(默认),公平锁:new ReentrantLock(true)
    private final ReentrantLock lock = new ReentrantLock();

    public void doSomething() {
        // 1. 获取锁
        lock.lock();
        try {
            // 业务逻辑:临界区代码
            System.out.println(Thread.currentThread().getName() + " 执行临界区代码");
            // 可重入:同一线程再次获取锁
            doInnerThing();
        } finally {
            // 2. 释放锁(必须放finally,避免异常导致锁泄漏)
            lock.unlock();
        }
    }

    private void doInnerThing() {
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " 执行内部临界区代码");
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        ReentrantLockDemo demo = new ReentrantLockDemo();
        new Thread(demo::doSomething, "线程1").start();
        new Thread(demo::doSomething, "线程2").start();
    }
}

执行结果(非公平)

复制代码
线程1 执行临界区代码
线程1 执行内部临界区代码
线程2 执行临界区代码
线程2 执行内部临界区代码

二、ReentrantLock 与 synchronized 的核心区别

我整理了一张核心维度的对比表:

对比维度 ReentrantLock synchronized
锁的类型 显式锁(JDK层面)(手动获取 / 释放) 隐式锁(JVM 自动获取 / 释放)
可重入性 支持(可重入锁) 支持(可重入锁)
公平 / 非公平锁 支持(默认非公平,可手动指定公平) 仅非公平锁
中断性 支持(lockInterruptibly ()) 不支持(等待锁的线程无法被中断)
超时获取锁 支持(tryLock (long time, TimeUnit)) 不支持(要么获取锁,要么一直阻塞)
条件变量(Condition) 支持多个 Condition,可精准唤醒线程 仅支持一个,只能随机 / 全部唤醒线程
锁状态查询 支持(isLocked ()、getHoldCount () 等) 不支持(无法主动查询锁状态)
性能 高并发下性能更优(JDK1.6 后差距缩小) 低并发下更简洁,JVM 优化充分
异常处理 必须手动释放(finally 块),否则锁泄漏 异常时自动释放锁,无需手动处理
关键区别详解
1. 显式 vs 隐式
  • synchronized:写在方法 / 代码块上,JVM 在进入代码块时自动加锁,退出(正常 / 异常)时自动释放锁,无需手动操作。
  • ReentrantLock:必须手动调用 lock() 加锁,且必须在 finally 中调用 unlock() 释放(否则线程异常会导致锁永远无法释放,即 "锁泄漏")。
2. 公平锁支持
  • synchronized:永远是非公平的,线程获取锁时不按等待顺序,可能后到的线程先拿到锁。
  • ReentrantLock:默认非公平,但可以通过构造函数 new ReentrantLock(true) 开启公平锁,保证等待最久的线程先获取锁(适合对顺序要求高的场景)。
3. 中断与超时获取

这是 ReentrantLock 最核心的优势之一:

java 复制代码
// 示例:超时获取锁
public void tryLockWithTimeout() {
    try {
        // 尝试在1秒内获取锁,获取到返回true,否则返回false
        if (lock.tryLock(1, TimeUnit.SECONDS)) {
            try {
                System.out.println(Thread.currentThread().getName() + " 获取到锁");
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println(Thread.currentThread().getName() + " 1秒内未获取到锁,放弃等待");
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt(); // 恢复中断状态
        System.out.println(Thread.currentThread().getName() + " 获取锁时被中断");
    }
}

synchronized 无法实现这种 "超时放弃" 的逻辑,只能一直阻塞。

4. 条件变量(Condition)

ReentrantLock 可以通过 newCondition() 创建多个 Condition,实现精准唤醒线程:

javascript 复制代码
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionDemo {
    private final ReentrantLock lock = new ReentrantLock();
    // 创建两个条件变量:等待读、等待写
    private final Condition readCondition = lock.newCondition();
    private final Condition writeCondition = lock.newCondition();

    public void waitForRead() throws InterruptedException {
        lock.lock();
        try {
            readCondition.await(); // 读线程等待
            System.out.println("读线程被唤醒");
        } finally {
            lock.unlock();
        }
    }

    public void signalRead() {
        lock.lock();
        try {
            readCondition.signal(); // 精准唤醒读线程
        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConditionDemo demo = new ConditionDemo();
        new Thread(demo::waitForRead, "读线程1").start();
        Thread.sleep(1000);
        demo.signalRead(); // 唤醒读线程
    }
}

而 synchronized 只能通过 wait()/notify()/notifyAll() 唤醒,notify() 是随机唤醒一个等待线程,notifyAll() 是唤醒所有,无法精准唤醒。

三、使用场景选择

  • 优先用 synchronized:低并发场景、简单的临界区控制,代码更简洁,JVM 优化(偏向锁、轻量级锁)更充分,不易出错。
  • 选择 ReentrantLock
    1. 需要公平锁、可中断锁、超时获取锁的场景;
    2. 需要精准唤醒线程(多 Condition);
    3. 高并发场景下需要更灵活的锁控制(如手动查询锁状态)。

总结

  1. ReentrantLock 是可重入的显式锁,支持公平 / 非公平、中断、超时获取、多 Condition 等高级特性,需手动加锁 / 释放(务必放 finally);
  2. synchronized 是隐式锁,JVM 自动管理,简洁易用,低并发下更友好,但功能单一;
  3. 核心区别在于灵活性和可控性:ReentrantLock 可控性更强,synchronized 更简洁、容错率更高,实际开发中需根据场景选择。
相关推荐
wenzhangli73 小时前
ooderA2UI BridgeCode 深度解析:从设计原理到 Trae Solo Skill 实践
java·开发语言·人工智能·开源
霖霖总总3 小时前
[小技巧66]当自增主键耗尽:MySQL 主键溢出问题深度解析与雪花算法替代方案
mysql·算法
HalvmånEver3 小时前
Linux:线程互斥
java·linux·运维
rainbow68893 小时前
深入解析C++STL:map与set底层奥秘
java·数据结构·算法
灵感菇_3 小时前
Java 锁机制全面解析
java·开发语言
indexsunny3 小时前
互联网大厂Java面试实战:Spring Boot微服务在电商场景中的应用与挑战
java·spring boot·redis·微服务·kafka·spring security·电商
wdfk_prog3 小时前
[Linux]学习笔记系列 -- [drivers][mmc][mmc_sdio]
linux·笔记·学习
果果燕3 小时前
今日学习笔记:双向链表、循环链表、栈
笔记·学习·链表
娇娇乔木4 小时前
模块十一--接口/抽象方法/多态--尚硅谷Javase笔记总结
java·开发语言