从零起步学习并发编程 || 第六章: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. 基础使用示例

csharp 复制代码
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 最核心的优势之一:

csharp 复制代码
// 示例:超时获取锁
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,实现精准唤醒线程:

java 复制代码
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 更简洁、容错率更高,实际开发中需根据场景选择。

相关推荐
普马萨特1 分钟前
Wi-Fi (802.11) 协议演进
运维·服务器·网络
砍材农夫3 分钟前
python环境|pip|uv|venv|Conda区别
后端·python·conda·pip·uv
袁小皮皮不皮9 分钟前
2.HCIP OSPF路由基础(优化版)
运维·服务器·网络·网络协议·智能路由器
牛油果子哥q10 分钟前
二叉树(Binary Tree)零基础精讲,树基础概念、树形分类、核心性质、递归/层序遍历、完整代码与面试考点全解
c++·面试·数据挖掘
Csvn11 分钟前
Linux 网络配置与排查命令实战
后端
vsropy16 分钟前
Ubuntu20 ping: www.baidu.com: 域名解析暂时失败的解决办法
运维·服务器
IT_陈寒16 分钟前
Redis主从切换把我坑惨了,这份血泪史你最好看看
前端·人工智能·后端
张忠琳26 分钟前
【Go 1.26.4】Golang Slice 深度解析
开发语言·后端·golang
不吃土豆的马铃薯33 分钟前
C++ 正则表达式入门详解
linux·服务器·网络·数据库·c++·正则表达式
starsky762381 小时前
NIO与BIO的区别
java·服务器·nio