读写锁。

关于读写锁的底层原理

  • 读写锁比较适合于读多写少的并发场景,它的实现主要是将int类型变量的32位拆分为高低16位,高16用于表示读锁,低16位用于表示写锁,对于读锁属于共享锁,写锁属于独占锁,所以写锁的重入次数只要通过移位操作就能计算,但是读锁却不能直接这么做,所以采用的是第一个线程用firstReader变量记录,而重入次数使用firstReaderCount记录,而其他线程通过ThreadLocal的线程变量副本HoldCounter来存,其中tid表示线程,count表示重入次数

  • 对于读写锁的使用,要注意的是写锁能降级为读锁,但是读锁不能升级为写锁,而一般的用法是先加写锁,然后加读锁,释放写锁,再释放读锁,在写锁中加读锁主要是为了避免脏读的情况

  • 对于读写锁属于悲观读,因为读的时候不能写,所以JDKs的时候引入了stampedLock来保证乐观读,采用CAS的方式来避免加锁阻塞线程,但是操作起来比较复杂,一般不推荐使用

1.读写锁是怎么计算重入次数的

通过state的高 16位表示读锁重入次数  低16位表示写锁重入次数。一个int类型是4个字节(byte)32位(bit) 当state位0时为 0000000000000000|0000000000000000   前16位位高位表示读锁  后16位位低位表示写锁。当state为0000000000000001|0000000000000000 时表示有一个线程占有读锁。当state为0000000000000000|0000000000000010则表示一个线程重入了两次写锁。而读锁重入依靠额外开启线程单独计数。

写锁的持有线程标识继承自 AQS,写锁(独占锁)的持有线程标识存储在父类 AbstractQueuedSynchronizer的 exclusiveOwnerThread字段中。

image

 


 

刚接触读写锁时知道是在读多写少的业务中使用但是疑惑这个锁的实际意义在哪,既然读锁能共享那和没加锁有什么区别。

后面经过询问AI了解到了他的真正作用,有相同疑问的可以看一下。

我:这段代码是我对读写锁的一些疑惑。既然读锁并不互斥那加读锁和不加锁有什么区别呢?我了解到读写锁是在一些读多写少的业务场景中进行使用,但是我不知道他具体应该怎么应用才能展示出他的优势或者灵活性。

 1 import java.util.concurrent.locks.ReentrantReadWriteLock;
 2 
 3 public class Main {
 4 
 5 
 6     static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
 7     static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
 8     static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
 9     static final Object synLock = new Object();
10 
11     public static void main(String[] args) {
12 
13 //        readWriteLock();
14         syn();
15     }
16 
17     private static void readWriteLock() {
18         Runnable task = () -> {
19             readLock.lock();
20             try {
21                 System.out.println(Thread.currentThread().getName() + "begin Reading" + System.currentTimeMillis());
22             } finally {
23                 readLock.unlock();
24             }
25             writeLock.lock();
26             try {
27                 Thread.sleep(3000);
28                 System.out.println(Thread.currentThread().getName() + "begin Writing" + System.currentTimeMillis());
29             } catch (InterruptedException e) {
30                 throw new RuntimeException(e);
31             } finally {
32                 writeLock.unlock();
33             }
34         };
35         new Thread(task, "Thread-1").start();
36         new Thread(task, "Thread-2").start();
37         new Thread(task, "Thread-3").start();
38     }
39 
40     public static void syn() {
41         Runnable task = () -> {
42             System.out.println(Thread.currentThread().getName() + "begin Reading" + System.currentTimeMillis());
43 
44             synchronized (synLock) {
45                 try {
46                     Thread.sleep(3000);
47                 } catch (InterruptedException e) {
48                     throw new RuntimeException(e);
49                 }
50                 System.out.println(Thread.currentThread().getName() + "begin Writing" + System.currentTimeMillis());
51             }
52 
53 
54         };
55         new Thread(task, "Thread-1").start();
56         new Thread(task, "Thread-2").start();
57         new Thread(task, "Thread-3").start();
58     }
59 }  
对读写锁的错误使用

AI:

读写锁的核心优势在于读读并发、读写互斥、写写互斥。让我们通过一个更合适的示例来展示它的优势:

1. 问题分析

在你的代码中,readWriteLock()方法存在几个问题:

  • 每个线程先读后写,这实际上是"写多读少"的场景

  • 没有区分真正的"读线程"和"写线程"

  • 没有展示出多个线程同时读的优势

  1 import java.util.concurrent.locks.ReentrantReadWriteLock;
  2 import java.util.ArrayList;
  3 import java.util.List;
  4 import java.util.Random;
  5 
  6 public class CacheSystem {
  7     // 模拟一个缓存系统
  8     private static List<String> cache = new ArrayList<>();
  9     private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
 10     private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
 11     private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
 12     
 13     public static void main(String[] args) throws InterruptedException {
 14         // 初始化一些数据
 15         for (int i = 0; i < 5; i++) {
 16             cache.add("Data-" + i);
 17         }
 18         
 19         System.out.println("=== 测试读写锁(读多写少场景)===");
 20         
 21         // 创建多个读线程
 22         for (int i = 0; i < 5; i++) {
 23             new Thread(new Reader(), "Reader-" + i).start();
 24         }
 25         
 26         // 创建一个写线程
 27         new Thread(new Writer(), "Writer-1").start();
 28         
 29         Thread.sleep(1000);
 30         
 31         System.out.println("\n=== 测试synchronized(对比)===");
 32         
 33         // 同样的场景用synchronized
 34         for (int i = 0; i < 5; i++) {
 35             new Thread(new SynchronizedReader(), "SyncReader-" + i).start();
 36         }
 37         new Thread(new SynchronizedWriter(), "SyncWriter-1").start();
 38     }
 39     
 40     // 使用读写锁的读操作
 41     static class Reader implements Runnable {
 42         @Override
 43         public void run() {
 44             for (int i = 0; i < 3; i++) {
 45                 readLock.lock();
 46                 try {
 47                     long start = System.currentTimeMillis();
 48                     // 模拟读操作耗时
 49                     Thread.sleep(100);
 50                     System.out.println(Thread.currentThread().getName() + 
 51                         " 读取缓存,大小:" + cache.size() + 
 52                         ",耗时:" + (System.currentTimeMillis() - start) + "ms");
 53                 } catch (InterruptedException e) {
 54                     e.printStackTrace();
 55                 } finally {
 56                     readLock.unlock();
 57                 }
 58             }
 59         }
 60     }
 61     
 62     // 使用读写锁的写操作
 63     static class Writer implements Runnable {
 64         @Override
 65         public void run() {
 66             for (int i = 0; i < 3; i++) {
 67                 writeLock.lock();
 68                 try {
 69                     long start = System.currentTimeMillis();
 70                     // 模拟写操作耗时更长
 71                     Thread.sleep(500);
 72                     cache.add("NewData-" + System.currentTimeMillis());
 73                     System.out.println(Thread.currentThread().getName() + 
 74                         " 写入缓存,新大小:" + cache.size() + 
 75                         ",耗时:" + (System.currentTimeMillis() - start) + "ms");
 76                 } catch (InterruptedException e) {
 77                     e.printStackTrace();
 78                 } finally {
 79                     writeLock.unlock();
 80                 }
 81             }
 82         }
 83     }
 84     
 85     // 使用synchronized的读操作
 86     static class SynchronizedReader implements Runnable {
 87         @Override
 88         public void run() {
 89             for (int i = 0; i < 3; i++) {
 90                 synchronized (cache) {
 91                     try {
 92                         long start = System.currentTimeMillis();
 93                         Thread.sleep(100);
 94                         System.out.println(Thread.currentThread().getName() + 
 95                             " 读取缓存,大小:" + cache.size() + 
 96                             ",耗时:" + (System.currentTimeMillis() - start) + "ms");
 97                     } catch (InterruptedException e) {
 98                         e.printStackTrace();
 99                     }
100                 }
101             }
102         }
103     }
104     
105     // 使用synchronized的写操作
106     static class SynchronizedWriter implements Runnable {
107         @Override
108         public void run() {
109             for (int i = 0; i < 3; i++) {
110                 synchronized (cache) {
111                     try {
112                         long start = System.currentTimeMillis();
113                         Thread.sleep(500);
114                         cache.add("NewData-" + System.currentTimeMillis());
115                         System.out.println(Thread.currentThread().getName() + 
116                             " 写入缓存,新大小:" + cache.size() + 
117                             ",耗时:" + (System.currentTimeMillis() - start) + "ms");
118                     } catch (InterruptedException e) {
119                         e.printStackTrace();
120                     }
121                 }
122             }
123         }
124     }
125 }
读写锁的正确应用

运行上述代码,你会看到:

使用读写锁时

  • 多个读线程可以同时执行(读读并发)

  • 只有写线程在执行时,其他线程才会阻塞

  • 总体执行时间较短

使用synchronized时

  • 即使只是读操作,所有线程也必须串行执行

  • 读线程会相互阻塞

  • 总体执行时间较长

. 关键点总结

  1. 适用场景:读操作 >> 写操作(如缓存、配置管理、元数据查询)

  2. 读锁的作用

    • 不是为了防止并发读(多个读本来就可以并发)

    • 而是为了在读取时阻止写入,保证读取的一致性

    • 同时也阻止了写入时被读取,防止读到中间状态

  3. 优势体现:当有大量读线程和少量写线程时,吞吐量显著提升

在你的原始代码中,每个线程都是"先读后写",这实际上变成了"写多"的情况,没有体现出读写锁的优势。真正的优势在于多个线程可以同时读,而写操作相对较少的场景。


 

通过上面AI的示例可以知道,只要读锁和写锁是从同一个ReentrantReadWriteLock对象中获取的,那么即使读锁和写锁在不同的任务中,读锁被持有时依然可以阻塞写锁。

但是SynchronizedReader锁如果要实现相同的效果就必须在读操作也添加锁才行,如果只在写任务添加锁那么读操作时依然可能会有并发安全问题导致脏读。

 

  1 import java.util.Map;
  2 import java.util.concurrent.ConcurrentHashMap;
  3 import java.util.concurrent.locks.ReentrantReadWriteLock;
  4 import java.util.concurrent.TimeUnit;
  5 
  6 /**
  7  * 通过读写锁保证MySQL与Redis数据一致性
  8  * 实现读写分离,提高读性能的同时保证数据一致性
  9  */
 10 public class CacheConsistencyWithRWLock {
 11 
 12     // 模拟MySQL数据库
 13     static class MySQLDataSource {
 14         private final Map<String, String> database = new ConcurrentHashMap<>();
 15 
 16         public MySQLDataSource() {
 17             // 初始化一些数据
 18             database.put("user:1", "Alice");
 19             database.put("user:2", "Bob");
 20             database.put("user:3", "Charlie");
 21         }
 22 
 23         public String query(String key) {
 24             // 模拟数据库查询
 25             try {
 26                 TimeUnit.MILLISECONDS.sleep(50); // 模拟数据库查询延迟
 27             } catch (InterruptedException e) {
 28                 Thread.currentThread().interrupt();
 29             }
 30             return database.get(key);
 31         }
 32 
 33         public boolean update(String key, String value) {
 34             // 模拟数据库更新
 35             try {
 36                 TimeUnit.MILLISECONDS.sleep(100); // 模拟数据库更新延迟
 37             } catch (InterruptedException e) {
 38                 Thread.currentThread().interrupt();
 39                 return false;
 40             }
 41             database.put(key, value);
 42             return true;
 43         }
 44 
 45         public boolean delete(String key) {
 46             try {
 47                 TimeUnit.MILLISECONDS.sleep(80);
 48             } catch (InterruptedException e) {
 49                 Thread.currentThread().interrupt();
 50                 return false;
 51             }
 52             return database.remove(key) != null;
 53         }
 54     }
 55 
 56     // 模拟Redis缓存
 57     static class RedisCache {
 58         private final Map<String, String> cache = new ConcurrentHashMap<>();
 59 
 60         public String get(String key) {
 61             // 模拟缓存读取
 62             try {
 63                 TimeUnit.MILLISECONDS.sleep(10); // 模拟缓存读取延迟
 64             } catch (InterruptedException e) {
 65                 Thread.currentThread().interrupt();
 66             }
 67             return cache.get(key);
 68         }
 69 
 70         public void set(String key, String value) {
 71             // 模拟缓存写入
 72             try {
 73                 TimeUnit.MILLISECONDS.sleep(20);
 74             } catch (InterruptedException e) {
 75                 Thread.currentThread().interrupt();
 76             }
 77             cache.put(key, value);
 78         }
 79 
 80         public void delete(String key) {
 81             try {
 82                 TimeUnit.MILLISECONDS.sleep(15);
 83             } catch (InterruptedException e) {
 84                 Thread.currentThread().interrupt();
 85             }
 86             cache.remove(key);
 87         }
 88 
 89         public boolean exists(String key) {
 90             return cache.containsKey(key);
 91         }
 92     }
 93 
 94     // 数据服务类,使用读写锁保证一致性
 95     static class DataService {
 96         private final MySQLDataSource mysql;
 97         private final RedisCache redis;
 98 
 99         // 使用细粒度锁:为每个key创建独立的读写锁
100         // 实际生产环境可以使用ConcurrentHashMap + 锁分段
101         private final Map<String, ReentrantReadWriteLock> keyLocks = new ConcurrentHashMap<>();
102 
103         public DataService(MySQLDataSource mysql, RedisCache redis) {
104             this.mysql = mysql;
105             this.redis = redis;
106         }
107 
108         // 获取key对应的锁
109         private ReentrantReadWriteLock getLockForKey(String key) {
110             return keyLocks.computeIfAbsent(key, k -> new ReentrantReadWriteLock());
111         }
112 
113         /**
114          * 读取数据 - 先读缓存,缓存没有则读数据库并回填缓存
115          * 使用读锁:允许多个线程同时读
116          */
117         public String readData(String key) {
118             ReentrantReadWriteLock.ReadLock readLock = getLockForKey(key).readLock();
119 
120             try {
121                 readLock.lock(); // 获取读锁
122 
123                 // 1. 先尝试从缓存读取
124                 String value = redis.get(key);
125                 if (value != null) {
126                     System.out.println(Thread.currentThread().getName() +
127                             " [缓存命中] key: " + key + ", value: " + value);
128                     return value;
129                 }
130 
131                 // 2. 缓存未命中,从数据库读取
132                 value = mysql.query(key);
133                 if (value != null) {
134                     // 3. 回填到缓存(使用异步或同步,这里用同步简化)
135                     redis.set(key, value);
136                     System.out.println(Thread.currentThread().getName() +
137                             " [缓存回填] key: " + key + ", value: " + value);
138                 } else {
139                     System.out.println(Thread.currentThread().getName() +
140                             " [数据不存在] key: " + key);
141                 }
142 
143                 return value;
144             } finally {
145                 readLock.unlock(); // 释放读锁
146             }
147         }
148 
149         /**
150          * 更新数据 - 先更新数据库,再删除缓存(Cache-Aside Pattern)
151          * 使用写锁:保证更新时互斥
152          */
153         public boolean updateData(String key, String newValue) {
154             ReentrantReadWriteLock.WriteLock writeLock = getLockForKey(key).writeLock();
155 
156             try {
157                 writeLock.lock(); // 获取写锁
158 
159                 System.out.println(Thread.currentThread().getName() +
160                         " [开始更新] key: " + key + ", newValue: " + newValue);
161 
162                 // 1. 先更新数据库
163                 boolean success = mysql.update(key, newValue);
164 
165                 if (success) {
166                     // 2. 删除缓存(而不是更新缓存)
167                     redis.delete(key);
168 
169                     System.out.println(Thread.currentThread().getName() +
170                             " [更新完成] 数据库已更新,缓存已删除");
171 
172                     // 注意:这里采用删除而不是更新缓存,是为了避免复杂的并发问题
173                     // 下次读取时会自动从数据库加载最新数据到缓存
174                 }
175 
176                 return success;
177             } finally {
178                 writeLock.unlock(); // 释放写锁
179             }
180         }
181 
182         /**
183          * 删除数据 - 先删除数据库,再删除缓存
184          */
185         public boolean deleteData(String key) {
186             ReentrantReadWriteLock.WriteLock writeLock = getLockForKey(key).writeLock();
187 
188             try {
189                 writeLock.lock(); // 获取写锁
190 
191                 System.out.println(Thread.currentThread().getName() +
192                         " [开始删除] key: " + key);
193 
194                 // 1. 先删除数据库
195                 boolean success = mysql.delete(key);
196 
197                 if (success) {
198                     // 2. 删除缓存
199                     redis.delete(key);
200 
201                     System.out.println(Thread.currentThread().getName() +
202                             " [删除完成] 数据库和缓存都已删除");
203                 }
204 
205                 return success;
206             } finally {
207                 writeLock.unlock(); // 释放写锁
208             }
209         }
210 
211         /**
212          * 批量读取 - 演示多个key的读取如何加锁
213          */
214         public void batchRead(String... keys) {
215             // 对多个key的读取,需要按固定顺序加锁,避免死锁
216             // 这里简化为对每个key独立加读锁
217             for (String key : keys) {
218                 readData(key);
219             }
220         }
221     }
222 
223     // 测试程序
224     public static void main(String[] args) throws InterruptedException {
225         MySQLDataSource mysql = new MySQLDataSource();
226         RedisCache redis = new RedisCache();
227         DataService dataService = new DataService(mysql, redis);
228 
229         System.out.println("=== 测试1: 多个线程同时读同一个key(读读并发)===");
230 
231         // 多个线程同时读取同一个数据
232         Runnable readTask = () -> {
233             for (int i = 0; i < 3; i++) {
234                 dataService.readData("user:1");
235                 try {
236                     Thread.sleep(100);
237                 } catch (InterruptedException e) {
238                     Thread.currentThread().interrupt();
239                 }
240             }
241         };
242 
243         Thread reader1 = new Thread(readTask, "读线程-1");
244         Thread reader2 = new Thread(readTask, "读线程-2");
245         Thread reader3 = new Thread(readTask, "读线程-3");
246 
247         reader1.start();
248         reader2.start();
249         reader3.start();
250 
251         reader1.join();
252         reader2.join();
253         reader3.join();
254 
255         System.out.println("\n=== 测试2: 读写并发(读写互斥)===");
256 
257         // 创建一个读线程和一个写线程同时操作同一个key
258         Thread readThread = new Thread(() -> {
259             for (int i = 0; i < 5; i++) {
260                 dataService.readData("user:2");
261                 try {
262                     Thread.sleep(200);
263                 } catch (InterruptedException e) {
264                     Thread.currentThread().interrupt();
265                 }
266             }
267         }, "读线程");
268 
269         Thread writeThread = new Thread(() -> {
270             try {
271                 Thread.sleep(100); // 让读线程先开始
272                 dataService.updateData("user:2", "Bob-Updated");
273             } catch (InterruptedException e) {
274                 Thread.currentThread().interrupt();
275             }
276         }, "写线程");
277 
278         readThread.start();
279         writeThread.start();
280 
281         readThread.join();
282         writeThread.join();
283 
284         System.out.println("\n=== 测试3: 写写互斥 ===");
285 
286         // 两个写线程同时更新同一个key
287         Runnable writeTask = () -> {
288             for (int i = 0; i < 2; i++) {
289                 dataService.updateData("user:3", Thread.currentThread().getName() + "-v" + i);
290                 try {
291                     Thread.sleep(300);
292                 } catch (InterruptedException e) {
293                     Thread.currentThread().interrupt();
294                 }
295             }
296         };
297 
298         Thread writer1 = new Thread(writeTask, "写线程-A");
299         Thread writer2 = new Thread(writeTask, "写线程-B");
300 
301         writer1.start();
302         writer2.start();
303 
304         writer1.join();
305         writer2.join();
306 
307         System.out.println("\n=== 测试4: 不同key的读写不互相影响 ===");
308 
309         // 测试不同key的并发操作
310         Thread t1 = new Thread(() -> {
311             dataService.readData("user:1");
312         }, "操作user:1的线程");
313 
314         Thread t2 = new Thread(() -> {
315             dataService.updateData("user:2", "NewValue");
316         }, "操作user:2的线程");
317 
318         Thread t3 = new Thread(() -> {
319             dataService.readData("user:3");
320         }, "操作user:3的线程");
321 
322         t1.start();
323         t2.start();
324         t3.start();
325 
326         t1.join();
327         t2.join();
328         t3.join();
329     }
330 }
读写锁保证数据库与缓存数据一致

 

posted @ 2026-01-20 18:30  意凌云丶  阅读(1)  评论(0)    收藏  举报