JUC多线程:读写锁
文章提示:本文包含大量代码案例及生活化比喻,适合刚接触锁机制的新手。建议边阅读边动手实践,效果更佳!
目录
文章前言
一、锁的分类与核心概念
1. 悲观锁 vs 乐观锁
2. 表锁 vs 行锁(数据库层面)
3. 读锁 vs 写锁(JUC)
防止死锁的策略
二、读写锁(ReadWriteLock)
1. 核心规则
2. 代码案例:缓存系统
代码分析 :
执行流程:
三、锁降级(Lock Downgrading)
1. 什么是锁降级?
2. 代码示例
四、总结与进阶
1. 如何选择锁?
2. 避坑指南
3. 实战技巧
文章总结
文章前言
在多线程编程中,锁机制是保证数据安全的核心工具。但面对“读写锁”“乐观锁”“表锁行锁”这些概念时,很多初学者会感到困惑:它们有什么区别?什么时候该用哪种锁?本文将通过通俗易懂的语言、生活化案例和代码演示,带你彻底搞懂Java中的各类锁机制,让你写出更高效、更安全的并发程序!
一、锁的分类与核心概念
1. 悲观锁 vs 乐观锁
-
悲观锁:像“老管家”,总担心有人搞破坏。
-
行为:每次操作数据前先加锁(比如
synchronized
)。 -
适用场景:写操作频繁,冲突概率高。
// 示例:synchronized实现悲观锁 public synchronized void updateData() {// 写操作 }
-
-
乐观锁:像“乐观青年”,相信冲突很少发生。
-
行为:先操作数据,提交时检查是否冲突(如CAS算法)。
-
适用场景:读多写少,冲突概率低。
// 示例:AtomicInteger使用CAS(乐观锁思想) AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // 内部通过CAS实现
-
2. 表锁 vs 行锁(数据库层面)
-
表锁:锁住整张表(像“封楼”)。
-
特点:简单粗暴,但并发性能差。
-
-
行锁:只锁住某一行数据(像“封教室”)。
-
特点:粒度细,并发性能好。
-
3. 读锁 vs 写锁(JUC)
读锁:适合于只读操作,并允许同一时间多个线程同时持有。
共享锁,发生死锁
虽然读锁是共享的,并且允许多个线程同时持有,但在特定情况下,依然可能形成死锁。一个常见的场景涉及多个资源以及嵌套的获取锁的操作。
示例:
假设有两个共享资源
ResourceA
和ResourceB
,每个资源都有自己的读写锁。考虑以下情况:
- 线程T1首先获取了
ResourceA
的读锁,然后尝试获取ResourceB
的读锁。- 同时,线程T2首先获取了
ResourceB
的读锁,然后尝试获取ResourceA
的读锁。如果这两个操作交错执行,即T1持有了
ResourceA
的读锁但等待ResourceB
的读锁,而T2持有了ResourceB
的读锁但等待ResourceA
的读锁,这将不会直接导致死锁,因为读锁是可以被多个线程持有的。然而,如果有其他条件或锁加入到这个过程中,比如某个线程需要升级读锁为写锁,那么就可能引发死锁。
写锁:用于修改操作,要求独占访问,确保数据一致性。
写锁:独占锁,发生死锁
写锁由于其独占性质,在多线程环境中更容易造成死锁问题,特别是当涉及到多个锁并且这些锁以不同的顺序请求时。
示例:
假设我们有两个共享资源
ResourceX
和ResourceY
,每个资源都有自己的读写锁。现在考虑以下情况:
- 线程T1首先获取了
ResourceX
的写锁,然后尝试获取ResourceY
的写锁。- 同时,线程T2首先获取了
ResourceY
的写锁,然后尝试获取ResourceX
的写锁。如果这两个操作交错执行,即T1持有了
ResourceX
的写锁但等待ResourceY
的写锁,而T2持有了ResourceY
的写锁但等待ResourceX
的写锁,这样就会形成典型的循环等待条件,导致死锁。
防止死锁的策略
为了避免上述死锁的情况,可以采取一些预防措施:
- 固定顺序加锁:确保所有线程总是按照相同的顺序来获取锁。
- 尝试锁定:使用非阻塞的方式尝试获取锁,如Java中的
tryLock()
方法,允许你在无法立即获得锁时回退并重试。 - 锁超时:设置锁的获取超时时间,超过一定时间未能获得锁则放弃尝试,避免无限期等待。
通过合理设计程序逻辑和谨慎管理锁的获取与释放过程,可以在很大程度上减少甚至避免死锁的发生。
二、读写锁(ReadWriteLock)
1. 核心规则
ReadWriteLock 是 Java 并发工具包(java.util.concurrent.locks)中的一种接口,提供了读锁(ReadLock)和写锁(WriteLock)两种类型的锁。读锁是共享锁,允许多个线程同时读取数据;写锁是独占锁,一次只能被一个线程持有。ReadWriteLock 通过这种方式提高了并发性能,特别是在读多写少的场景中。
定义:ReadWriteLock 接口提供了读锁和写锁,分别通过 readLock() 和 writeLock() 方法获取。
工作原理:
- 读锁(ReadLock):允许多个线程同时持有,适用于读操作。
- 写锁(WriteLock):一次只能被一个线程持有,适用于写操作。
互斥规则:
- 读-读:可以共存,多个线程可以同时读取数据。
- 读-写:不能共存,写操作会阻塞所有读操作。
- 写-写:不能共存,写操作会阻塞其他写操作。
操作组合 | 是否兼容 | 生活案例 |
---|---|---|
读 + 读 | ✅ | 多人同时阅读同一本书 |
读 + 写 | ❌ | 有人写书时禁止阅读 |
写 + 写 | ❌ | 只能有一个人修改书内容 |
2. 代码案例:缓存系统
package JUC.rw;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/*** 独占锁(写锁) 一次只能被一个线程占有* 共享锁(读锁) 多个线程可以同时占有* ReadWriteLock* 读-读 可以共存!* 读-写 不能共存!* 写-写 不能共存!*/
public class ReadWriteLockDemo {public static void main(String[] args) {MyCacheLock myCache = new MyCacheLock();// 写入for (int i = 1; i <= 5 ; i++) {final int temp = i;new Thread(()->{myCache.put(temp+"",temp+"");},String.valueOf(i)).start();}// 读取for (int i = 1; i <= 5 ; i++) {final int temp = i;new Thread(()->{myCache.get(temp+"");},String.valueOf(i)).start();}}
}
// 加锁的
class MyCacheLock{private volatile Map<String,Object> map = new HashMap<>();// 读写锁: 更加细粒度的控制private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();// 存,写入的时候,只希望同时只有一个线程写public void put(String key,Object value){readWriteLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName()+"写入"+key);map.put(key,value);System.out.println(Thread.currentThread().getName()+"写入OK");} catch (Exception e) {e.printStackTrace();} finally {readWriteLock.writeLock().unlock();}}// 取,读,所有人都可以读!public void get(String key){readWriteLock.readLock().lock();try {System.out.println(Thread.currentThread().getName()+"读取"+key);Object o = map.get(key);System.out.println(Thread.currentThread().getName()+"读取OK");} catch (Exception e) {e.printStackTrace();} finally {readWriteLock.readLock().unlock();}}
}
/*** 自定义缓存实现读写锁*/
class MyCache{private volatile Map<String,Object> map = new HashMap<>();// 存,写public void put(String key,Object value){System.out.println(Thread.currentThread().getName()+"写入"+key);map.put(key,value);System.out.println(Thread.currentThread().getName()+"写入OK");}// 取,读public void get(String key){System.out.println(Thread.currentThread().getName()+"读取"+key);Object o = map.get(key);System.out.println(Thread.currentThread().getName()+"读取OK");}
}
代码分析 :
ReadWriteLockDemo 类展示了如何使用 ReadWriteLock 来实现一个自定义缓存 MyCacheLock。以下是关键点分析:
初始化 ReadWriteLock:
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
创建一个 ReentrantReadWriteLock 实例,该实例实现了 ReadWriteLock 接口。
ReentrantReadWriteLock 提供了可重入的读锁和写锁。
写操作 (put 方法)
public void put(String key, Object value) {readWriteLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + "写入" + key);map.put(key, value);System.out.println(Thread.currentThread().getName() + "写入OK");} catch (Exception e) {e.printStackTrace();} finally {readWriteLock.writeLock().unlock();}}
线程调用 readWriteLock.writeLock().lock() 方法获取写锁。
获取写锁后,线程可以安全地写入数据。
写操作完成后,调用 readWriteLock.writeLock().unlock() 方法释放写锁。
读操作 (get 方法)
public void get(String key) {readWriteLock.readLock().lock();try {System.out.println(Thread.currentThread().getName() + "读取" + key);Object o = map.get(key);System.out.println(Thread.currentThread().getName() + "读取OK");} catch (Exception e) {e.printStackTrace();} finally {readWriteLock.readLock().unlock();}}
线程调用 readWriteLock.readLock().lock() 方法获取读锁。
获取读锁后,线程可以安全地读取数据。
读操作完成后,调用 readWriteLock.readLock().unlock() 方法释放读锁。
启动线程
public static void main(String[] args) {MyCacheLock myCache = new MyCacheLock();// 写入for (int i = 1; i <= 5; i++) {final int temp = i;new Thread(() -> {myCache.put(temp + "", temp + "");}, String.valueOf(i)).start();}// 读取for (int i = 1; i <= 5; i++) {final int temp = i;new Thread(() -> {myCache.get(temp + "");}, String.valueOf(i)).start();}}
启动 5 个线程进行写操作。
启动 5 个线程进行读操作。
执行流程:
1.初始化 ReadWriteLock
- 创建 ReentrantReadWriteLock 实例,初始状态下没有线程持有读锁或写锁。
2.启动写线程
- 启动 5 个写线程,每个线程尝试获取写锁。
- 假设线程 1 成功获取写锁,其他写线程会被阻塞。
- 线程 1 打印 "写入1",写入数据,打印 "写入OK"。
- 线程 1 释放写锁。
3.启动读线程
- 启动 5 个读线程,每个线程尝试获取读锁。
- 假设线程 2 成功获取读锁,其他读线程也可以获取读锁。
- 线程 2 打印 "读取1",读取数据,打印 "读取OK"。
- 线程 2 释放读锁。
4.继续执行
- 假设线程 3 成功获取写锁,其他写线程和读线程会被阻塞。
- 线程 3 打印 "写入2",写入数据,打印 "写入OK"。
- 线程 3 释放写锁。
5.继续执行
- 假设线程 4 和线程 5 成功获取读锁,其他读线程也可以获取读锁。
- 线程 4 和线程 5 打印 "读取2",读取数据,打印 "读取OK"。
- 线程 4 和线程 5 释放读锁。
6.所有线程完成
- 所有写线程和读线程依次获取锁,执行写入和读取操作,最终完成所有任务。
输出结果:
三、锁降级(Lock Downgrading)
1. 什么是锁降级?
将写锁降为读锁
-
场景:先获取写锁 → 再获取读锁 → 释放写锁→ 释放读锁
-
目的:保证数据修改后,其他线程能立即看到最新结果
2. 代码示例
public class LockDowngradeDemo {private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();private int data = 0;public void processData() {rwLock.writeLock().lock(); // 1. 获取写锁try {data = 42; // 修改数据rwLock.readLock().lock(); // 2. 获取读锁(锁降级开始)} finally {rwLock.writeLock().unlock(); // 3. 释放写锁}try {System.out.println("当前数据: " + data);} finally {rwLock.readLock().unlock(); // 4. 释放读锁}}
}
关键点:在释放写锁前获取读锁,确保后续操作能安全读取数据。
四、总结与进阶
1. 如何选择锁?
场景 | 推荐锁类型 | 原因 |
---|---|---|
读多写少 | 读写锁 | 提高并发读性能 |
写冲突频繁 | 悲观锁 | 避免反复重试 |
简单操作且线程安全要求高 | synchronized | 编码简单,JVM自动优化 |
2. 避坑指南
-
死锁预防:按固定顺序获取锁,设置超时时间。
-
性能优化:尽量减少锁的持有时间。
-
锁粒度:优先选择细粒度锁(如行锁 > 表锁)。
3. 实战技巧
// 使用tryLock避免死锁
if (lock.tryLock(1, TimeUnit.SECONDS)) {try {// 操作代码} finally {lock.unlock();}
} else {System.out.println("获取锁失败,执行其他逻辑");
}
文章总结
锁机制就像交通信号灯:读写锁是“潮汐车道”(读时放宽,写时严控),悲观锁是“全程红灯等待”,乐观锁是“绿灯时小心通过”。理解它们的原理后,再结合业务场景选择:
-
高并发读取(如商品详情页)→ 读写锁
-
秒杀库存扣减 → 乐观锁(CAS或版本号)
-
数据库转账操作 → 悲观锁(行级锁)
记住:没有“万能锁”,只有“最适合的锁”。动手运行文中的代码案例,调整线程数量观察输出结果,你会对锁机制有更深刻的理解!