你可能有这样一个疑问,Java SDK 并发包里为什么还有很多其他的工具类呢?原因很简单:分场景优化性能,提升易用性。
针对读多写少这种并发场景,Java SDK 并发包提供了读写锁——ReadWriteLock
读写锁,并不是 Java 语言特有的,而是一个广为使用的通用技术,所有的读写锁都遵守以下三条,尤其注意第三条,之后我们讲跟其他锁的对比会用到。
基本原则:
1. 允许多个线程同时读共享变量;
2.只允许一个线程写共享变量;
3. 如果一个写线程正在执行写操作,此时禁止读线程读共享变量(读锁 写锁是互斥的,不能同时存在)
互斥锁:
|
|
|
|
|
互斥锁 |
互斥锁 |
升降级 |
|
ReadWriteLock |
读操作允许多个线程同时读共享变量 写操作是互斥的,当一个 线程在写共享变量的时候,是不允许其他线程执行写操作和读操作。
|
不允许升级,允许降级。 读锁不支持条件变量newCondition() |
|
|
|
一:用 ReadWriteLock 快速实现一个通用的缓存工具类
1 class MyCache<K,V> { 2 3 final Map<K, V> m = new HashMap<>(); 4 final ReadWriteLock rwl = new ReentrantReadWriteLock(); 5 // 读锁 6 final Lock readLock = rwl.readLock(); 7 8 // 写锁 9 final Lock writeLock = rwl.writeLock(); 10 // 读缓存 11 V get(K key) { 12 readLock.lock(); 13 try { return m.get(key); } 14 finally { 15 readLock.unlock(); 16 } 17 } 18 19 // 写缓存 20 V put(String key, Data v) { 21 writeLock.lock(); 22 try { return m.put(key, v); } 23 finally { 24 writeLock.unlock(); 25 } 26 } 27 }
面的这段代码实现了按需加载的功能,这里我们假设缓存的源头是数据库。先从缓存中获取数据,若缓存中没有,就从数据库中加载,然后写入缓存。
经验之谈:在获取写锁之后,并不直接查数据库,而是再次验证缓存是否存在,为什么呢?
1 class MyCache<K,V> { 2 final Map<K, V> m = new HashMap<>(); 3 final ReadWriteLock rwl = new ReentrantReadWriteLock(); 4 final Lock r = rwl.readLock(); 5 final Lock w = rwl.writeLock(); 6 V get(K key) { 7 V v = null; 8 // 读缓存 9 r.lock(); ① 10 try { 11 v = m.get(key); ② 12 } finally{ 13 r.unlock(); ③ 14 } 15 // 缓存中存在,返回 16 if(v != null) { ④ 17 return v; 18 } 19 // 缓存中不存在,查询数据库 20 w.lock(); ⑤ 21 try { 22 // 获取到写锁,再次验证 23 v = m.get(key); ⑥ 24 if(v == null){ ⑦ 25 //假设A1线程获取读锁,并更新了数据库,释放了读锁 26 // 接下来A2线程得到读锁,获取读锁,并更新了数据库,释放了读锁 27 // A3线程也是如此操作, 28 // 获取到读锁再次验证是否存在,就能避免后续线程多次操作数据库 29 // 查询数据库 代码省略 30 //.................... 31 m.put(key, v); 32 } 33 } finally{ 34 w.unlock(); 35 } 36 return v; 37 } 38 }