【Java并发编程】锁机制:Lock体系:ReentrantLock、ReentrantReadWriteLock、Lock vs synchronized 区别(附《思维导图》+《面试高频考点清单》)

简介: 本文系统梳理Java Lock体系核心知识:涵盖ReentrantLock(可重入、公平/非公平、AQS实现)、ReentrantReadWriteLock(读写分离、锁降级、state拆分)及StampedLock(乐观读、缓解写饥饿),深度对比synchronized与Lock在实现、特性、性能及场景上的八大区别,助力高并发编程与面试通关。

思维导图

Java并发编程:Lock体系 系统性知识总结

一、Lock体系整体概述

1.1 产生背景

synchronized是Java内置的锁机制,但存在以下局限性:

  • 不可中断:获取锁的线程会一直阻塞,无法响应中断
  • 非公平:默认非公平,无法实现公平锁
  • 单一条件:只能关联一个条件变量
  • 无法尝试获取锁:不能设置超时时间,获取失败会无限阻塞

1.2 Lock接口定义

java.util.concurrent.locks.Lock是所有锁的顶级接口,定义了锁的基本操作:

public interface Lock {
   
    void lock(); // 阻塞获取锁
    void lockInterruptibly() throws InterruptedException; // 可中断获取锁
    boolean tryLock(); // 非阻塞尝试获取锁,立即返回
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 超时尝试获取锁
    void unlock(); // 释放锁
    Condition newCondition(); // 创建条件变量
}

1.3 Lock体系核心类图

Lock
├─ ReentrantLock (可重入锁)
├─ ReadWriteLock (读写锁接口)
│  └─ ReentrantReadWriteLock (可重入读写锁)
└─ StampedLock (JDK8新增,乐观读写锁)

二、ReentrantLock详解

2.1 基本特性

  • 可重入性:同一个线程可以多次获取同一把锁,内部维护了一个计数器
  • 支持公平/非公平模式:构造方法可指定,默认非公平
  • 可中断:支持lockInterruptibly()方法
  • 超时获取:支持tryLock(long, TimeUnit)方法
  • 多条件变量:支持创建多个Condition对象

2.2 实现原理:基于AQS

ReentrantLock内部通过AbstractQueuedSynchronizer(AQS)实现,AQS是Java并发包的基础框架:

  • 同步状态private volatile int state,0表示未锁定,>0表示已锁定
  • CLH队列:双向链表结构,用于存放等待锁的线程
  • 独占模式:ReentrantLock使用AQS的独占模式

2.3 公平锁 vs 非公平锁

特性 公平锁 非公平锁
获取顺序 严格按照线程等待顺序 先尝试直接获取锁,失败再排队
性能 较低(上下文切换多) 较高(减少上下文切换)
饥饿问题 可能出现(线程一直抢不到锁)
实现方式 new ReentrantLock(true) new ReentrantLock(false)(默认)

非公平锁获取流程

  1. 尝试CAS将state从0改为1,成功则直接获取锁
  2. 失败则调用acquire(1)进入AQS队列
  3. 队列中的线程按顺序获取锁

公平锁获取流程

  1. 直接调用acquire(1)
  2. 先检查队列是否有等待线程,有则排队
  3. 没有等待线程才尝试CAS获取锁

2.4 可重入性实现

当线程再次获取锁时:

  1. 检查当前线程是否是持有锁的线程
  2. 如果是,将state加1
  3. 释放锁时,state减1,直到state为0才真正释放锁

2.5 Condition条件变量

  • 替代了Object的wait()/notify()/notifyAll()方法
  • 一个Lock可以创建多个Condition,实现更精细的线程通信
  • 常用方法:await()signal()signalAll()

使用示例

Lock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

// 生产者
lock.lock();
try {
   
    while (queue.size() == capacity) {
   
        notFull.await(); // 队列满,等待
    }
    queue.add(item);
    notEmpty.signal(); // 唤醒消费者
} finally {
   
    lock.unlock();
}

// 消费者
lock.lock();
try {
   
    while (queue.isEmpty()) {
   
        notEmpty.await(); // 队列空,等待
    }
    Object item = queue.remove();
    notFull.signal(); // 唤醒生产者
    return item;
} finally {
   
    lock.unlock();
}

三、ReentrantReadWriteLock详解

3.1 设计思想

  • 读写分离:读操作可以并发执行,写操作必须独占执行
  • 适用于读多写少的场景,能显著提高并发性能
  • 包含两个锁:读锁(共享锁)和写锁(独占锁)

3.2 基本特性

  • 可重入性:读锁和写锁都支持可重入
  • 锁降级:写锁可以降级为读锁,但读锁不能升级为写锁
  • 公平/非公平模式:构造方法可指定,默认非公平
  • 写锁排他:写锁与任何锁都互斥(读锁、写锁)
  • 读锁共享:多个线程可以同时持有读锁

3.3 实现原理

ReentrantReadWriteLock内部也基于AQS实现,将32位的state变量拆分为两部分:

  • 高16位:表示读锁的持有次数(共享锁计数)
  • 低16位:表示写锁的持有次数(独占锁计数)
state = 0x00000000
高16位:读锁计数  低16位:写锁计数

3.4 锁降级机制

定义:持有写锁的线程,可以先获取读锁,再释放写锁,从而将写锁降级为读锁。

目的:保证数据的可见性,避免其他线程在写操作完成后、读操作开始前修改数据。

正确示例

rwl.writeLock().lock();
try {
   
    // 执行写操作
    data = updateData();

    // 获取读锁(锁降级)
    rwl.readLock().lock();
} finally {
   
    rwl.writeLock().unlock(); // 释放写锁,此时持有读锁
}

try {
   
    // 执行读操作
    processData(data);
} finally {
   
    rwl.readLock().unlock(); // 释放读锁
}

注意:不支持锁升级(持有读锁时获取写锁会导致死锁)。

3.5 使用场景与注意事项

  • 适用场景:读多写少的缓存系统、配置管理、数据查询服务
  • 注意事项
    1. 读锁持有期间不能获取写锁(会导致死锁)
    2. 写锁持有期间可以获取读锁(锁降级)
    3. 读锁和写锁都必须在finally块中释放
    4. 写线程过多时可能导致读线程饥饿(可使用公平模式缓解)

四、Lock vs synchronized 全面对比

4.1 核心特性对比

特性 synchronized Lock
实现方式 JVM内置,字节码层面实现 JDK层面实现,纯Java代码
锁释放 自动释放(代码块结束或异常) 手动释放(必须在finally中调用unlock())
可重入性 支持 支持(ReentrantLock)
公平性 不支持(默认非公平) 支持(可指定公平/非公平)
可中断性 不支持 支持(lockInterruptibly())
超时获取 不支持 支持(tryLock(long, TimeUnit))
条件变量 只能关联一个(Object的wait/notify) 支持多个(newCondition())
锁类型 独占锁 支持独占锁、共享锁(读写锁)
性能 低并发下性能较好 高并发下性能更优

4.2 实现原理对比

synchronized实现原理

  • 基于对象头的Mark Word和监视器锁(Monitor)
  • 锁升级过程:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
  • 重量级锁依赖操作系统的互斥量(Mutex)实现

Lock实现原理

  • 基于AQS框架和CAS操作
  • 内部维护一个CLH队列存放等待线程
  • 不依赖操作系统,纯Java实现,避免了用户态与内核态的切换

4.3 使用场景对比

优先使用synchronized的场景

  • 简单的同步场景,代码量少
  • 不需要Lock的高级特性(可中断、超时、多条件)
  • 低并发环境,synchronized的性能已经足够
  • 对代码简洁性要求较高

优先使用Lock的场景

  • 需要公平锁的场景
  • 需要可中断获取锁的场景
  • 需要超时获取锁的场景
  • 需要多个条件变量的场景
  • 读多写少的场景(使用ReentrantReadWriteLock)
  • 高并发环境,需要更好的性能

五、最佳实践与常见陷阱

5.1 Lock使用最佳实践

  1. 必须在finally块中释放锁:防止异常导致锁无法释放

    Lock lock = new ReentrantLock();
    lock.lock();
    try {
         
        // 业务逻辑
    } finally {
         
        lock.unlock();
    }
    
  2. 优先使用非公平锁:除非有特殊的公平性要求,否则非公平锁性能更好

  3. 合理使用tryLock():避免线程无限阻塞

    if (lock.tryLock(1, TimeUnit.SECONDS)) {
         
        try {
         
            // 业务逻辑
        } finally {
         
            lock.unlock();
        }
    } else {
         
        // 获取锁失败的处理逻辑
    }
    
  4. 读写锁的正确使用:读操作使用读锁,写操作使用写锁

5.2 常见陷阱

  1. 忘记释放锁:导致死锁,必须在finally中调用unlock()
  2. 锁重入次数不匹配:获取多少次锁,就要释放多少次锁
  3. 在Condition.await()前没有持有锁:会抛出IllegalMonitorStateException
  4. 读写锁使用错误:读操作使用写锁,导致并发性能下降
  5. 锁升级:持有读锁时获取写锁,会导致死锁
  6. 多个锁获取顺序不一致:导致死锁(遵循固定的获取顺序)

六、面试核心考点清单

  1. ReentrantLock的实现原理:基于AQS,state变量,CLH队列
  2. 公平锁与非公平锁的区别:获取流程、性能、饥饿问题
  3. 可重入性的实现:state计数器,线程持有判断
  4. Condition的作用与实现:替代wait/notify,多条件变量
  5. ReentrantReadWriteLock的实现原理:state拆分,读写锁互斥规则
  6. 锁降级机制:定义、目的、正确使用方式
  7. Lock与synchronized的区别:从实现、特性、性能、使用场景等方面
  8. AQS的核心思想:同步状态、CLH队列、独占/共享模式
  9. 死锁的产生条件与避免方法:互斥、持有并等待、不可剥夺、循环等待
  10. 各种锁的使用场景:根据业务特点选择合适的锁

一、可直接背诵的面试版核心考点(精简版)

1. Lock体系基础

  • 产生背景:解决synchronized的4大缺陷:不可中断、非公平、单一条件、无法超时获取
  • Lock接口核心方法lock()(阻塞)、lockInterruptibly()(可中断)、tryLock()(非阻塞)、tryLock(long, TimeUnit)(超时)、unlock()(必须finally)、newCondition()(多条件)
  • 核心实现类ReentrantLock(独占可重入)、ReentrantReadWriteLock(读写分离)、StampedLock(JDK8乐观读写)

2. ReentrantLock 必背考点

  • 核心特性:可重入、公平/非公平双模式、可中断、超时获取、多条件变量
  • 实现原理:基于AQS(抽象队列同步器),volatile int state记录锁状态,CLH双向队列存放等待线程
  • 公平vs非公平
    • 非公平(默认):先CAS抢锁,失败再排队,性能高,可能饥饿
    • 公平:直接排队,严格FIFO,性能低,无饥饿
  • 可重入实现:同一线程多次获取时state++,释放时state--state=0才真正释放
  • Condition:替代wait/notify,一个Lock可创建多个Condition,实现精准唤醒(如生产者消费者的notFull/notEmpty)

3. ReentrantReadWriteLock 必背考点

  • 设计思想:读写分离,读共享、写独占,适用于读多写少场景
  • state拆分:32位int,高16位=读锁计数,低16位=写锁计数
  • 核心规则
    • 写锁与所有锁互斥(读+写)
    • 读锁与读锁共享
    • 支持锁降级(写→读),不支持锁升级(读→写,会死锁)
  • 锁降级目的:保证数据可见性,避免写后读间隙被其他线程修改

4. Lock vs synchronized 核心区别(面试高频)

维度 synchronized Lock
实现层面 JVM内置,字节码(monitorenter/monitorexit) JDK层面,纯Java代码(AQS+CAS)
释放方式 自动(代码结束/异常) 手动(必须finally调用unlock())
公平性 仅非公平 支持公平/非公平
可中断性 不支持 支持(lockInterruptibly())
超时获取 不支持 支持(tryLock超时)
条件变量 1个(Object) 多个(Condition)
锁类型 仅独占锁 独占+共享(读写锁)
性能 低并发好,高并发差 高并发下显著更优

5. 最佳实践与常见陷阱

  • 铁律:Lock必须在finally中释放,否则死锁
  • 优先非公平锁:除非有严格公平性要求
  • tryLock优先:避免无限阻塞
  • 读写锁严格区分:读用读锁,写用写锁
  • 禁止锁升级:持有读锁时不能获取写锁
  • 锁顺序一致:多个锁按固定顺序获取,避免循环等待

二、StampedLock 补充分析(JDK8新增,面试高频对比)

1. 产生背景

解决ReentrantReadWriteLock写饥饿问题:当读线程非常多时,写线程可能长时间无法获取锁。

2. 核心设计思想

引入乐观读模式:读操作不需要加锁,仅通过一个戳记(stamp)验证数据是否被修改。如果验证通过,直接读取;如果验证失败,再升级为悲观读锁。

3. 三种核心模式

模式 特性 返回值 互斥规则
写锁(WriteLock) 独占锁,与所有锁互斥 非0戳记 与读锁、写锁都互斥
悲观读锁(ReadLock) 共享锁,与写锁互斥 非0戳记 与写锁互斥,与读锁共享
乐观读(OptimisticRead) 无锁,仅返回戳记 非0戳记 不与任何锁互斥

4. 乐观读使用示例

StampedLock lock = new StampedLock();
long stamp = lock.tryOptimisticRead(); // 获取乐观读戳记
// 读取数据
int data = this.data;
if (!lock.validate(stamp)) {
    // 验证戳记是否有效(数据是否被修改)
    // 戳记无效,升级为悲观读锁
    stamp = lock.readLock();
    try {
   
        data = this.data;
    } finally {
   
        lock.unlockRead(stamp);
    }
}
return data;

5. StampedLock vs ReentrantReadWriteLock 全面对比

特性 ReentrantReadWriteLock StampedLock
可重入性 支持 不支持
条件变量 支持 不支持
锁降级 支持 支持(写→悲观读)
锁升级 不支持 支持(乐观读→悲观读→写)
写饥饿 严重(读多写少) 大幅缓解
性能 读多写少较好 读多写少更优(乐观读无锁)
适用场景 读多写少,需要重入/条件变量 读多写少,追求极致性能

6. 使用注意事项

  • 乐观读不是锁,必须调用validate(stamp)验证数据一致性
  • 不支持重入,同一线程不能多次获取同一把锁
  • 不支持Condition条件变量
  • 写锁和悲观读锁必须使用对应的unlockWrite(stamp)/unlockRead(stamp)释放
  • 戳记无效时必须升级为悲观读锁,不能直接重试乐观读

三、终极面试答题框架(直接套用)

当面试官问"ReentrantLock和synchronized的区别"时,按以下顺序回答:

  1. 实现层面:JVM内置 vs JDK AQS+CAS
  2. 释放方式:自动 vs 手动(finally)
  3. 高级特性:公平性、可中断性、超时获取、多条件变量
  4. 锁类型:仅独占 vs 独占+共享
  5. 性能:低并发synchronized好,高并发Lock好
  6. 适用场景:简单场景用synchronized,需要高级特性用Lock
相关文章
|
4天前
|
安全 Java C++
【Java基础】集合框架: ConcurrentHashMap核心原理:JDK1.7 vs 1.8+ 区别、线程安全实现、分段锁 vs CAS+synchronized、扩容机制
ConcurrentHashMap是Java高并发场景下线程安全的哈希表实现,JDK1.7采用Segment分段锁(16段独立加锁),JDK1.8升级为CAS+synchronized细粒度桶锁,并引入红黑树与多线程协助扩容,显著提升性能与扩展性。
|
4天前
|
安全 Java API
【Java基础】Java 8-21新特性:JDK21 LTS:虚拟线程、模式匹配switch、结构化并发、序列集合(附《思维导图》+《面试高频考点清单》)
本文系统梳理Java 8至21的演进脉络,聚焦JDK 21 LTS四大核心特性:虚拟线程(M:N轻量调度,百万级I/O并发)、模式匹配switch(类型+守卫+null安全)、结构化并发(父子任务生命周期绑定)、序列集合(统一有序集合操作)。兼顾版本战略、迁移实践与面试高频考点,助力高效掌握现代Java开发核心能力。
|
2月前
|
消息中间件 监控 Kafka
【消息队列MQ】消息丢失:全链路原因、解决方案、消息可靠性保证
消息队列MQ全链路防丢失体系:覆盖生产→Broker→消费三阶段,直击6大关键节点风险;涵盖确认机制、同步刷盘、主从复制、手动提交Offset、事务消息、死信兜底等核心方案,兼顾可靠性与性能折中。
|
设计模式 Java 机器人
SpringBoot3自动配置流程 SPI机制 核心注解 自定义starter
SpringBoot3自动配置流程 SPI机制 核心注解 自定义starter
|
2月前
|
存储 安全 前端开发
【微服务】微服务安全:OAuth2.0、JWT、SSO单点登录、RBAC权限模型
本文系统梳理微服务安全四大核心:OAuth2.0(授权协议)、JWT(无状态凭证)、SSO(统一认证)、RBAC(权限模型),从边界定位、原理剖析、落地规范到协同架构四维展开,厘清分层职责与互补关系,提供企业级可落地的安全闭环实践指南。
|
1月前
|
缓存 NoSQL 算法
【Redis】Redis——过期键删除策略、内存淘汰8种策略、LRU/LFU实现
Redis过期删除与内存淘汰是两大核心内存管理机制:前者按TTL自动清理失效键(惰性+定期组合),后者在`maxmemory`超限时主动淘汰键(8种策略,含LRU/LFU近似实现)。二者目标、触发条件与作用范围截然不同,需精准区分与配置。
|
4天前
|
存储 缓存 监控
【JVM虚拟机】堆内存分代模型:年轻代(Eden+Survivor)、老年代、元空间Metaspace(附《思维导图》+《面试高频考点清单》)
本文系统梳理JVM堆内存分代模型:基于弱分代假说,划分为年轻代(Eden+2×Survivor,8:1:1)和老年代(2:1),辅以元空间;详解Minor GC复制机制、对象年龄晋升、四大入老年代路径及Full GC触发条件,涵盖核心参数与调优要点。
|
4天前
|
存储 网络协议 Java
【Spring全家桶】Spring Cloud 2023.0.x:服务注册与发现:Nacos、Eureka、Consul(附《思维导图》+《面试高频考点清单》)
本文系统梳理Spring Cloud 2023.0.x(Leyton)服务注册与发现核心体系,涵盖Nacos(AP/CP双模)、Consul(CP)、Eureka(维护模式)三大组件原理、对比与实战,深度解析CAP理论、健康检查、高可用集群及迁移方案,助力微服务架构落地。
|
3月前
|
安全 Java 编译器
【泛型】泛型:泛型擦除、通配符、上下界限定
Java泛型通过类型参数实现代码复用与编译期类型安全。其核心是**类型擦除**(运行时泛型信息被擦除,兼容旧JVM),配合**通配符**(`?`、`? extends T`、`? super T`)解决类型不变性问题,并依**上下界限定**约束类型范围。遵循PECS原则(生产者用extends,消费者用super),兼顾安全与灵活。
|
4天前
|
存储 缓存 Java
【JVM虚拟机】垃圾回收GC:四种引用类型:强引用、软引用、弱引用、虚引用(附《思维导图》+《面试高频考点清单》)
本文系统梳理JVM四种引用类型:强引用(永不回收)、软引用(内存不足时回收)、弱引用(GC即回收)、虚引用(仅跟踪回收,需配引用队列)。涵盖原理、回收时机、典型场景(如缓存、ThreadLocal、WeakHashMap)及面试高频对比,助你深入理解Java内存管理与防泄漏机制。