写点什么

Java 多线程系列 9:读写锁

作者:BigBang!
  • 2023-12-27
    广东
  • 本文字数:2103 字

    阅读完需:约 7 分钟

Java多线程系列9:读写锁

前面介绍了 JDK 包的管程实现:Lock 和 Condition,然后介绍了可重入锁和条件变量的基本用法。之前介绍的锁实现都是互斥的,只要有一个线程获得锁,其它线程就只能等待,确保每次只有一个线程在访问共享变量,这很好的实现了多线程的访问安全。但这样简单粗暴的做法,在某些场景下会牺牲并发的性能。比如对于某个共享变量的访问,大部分时候线程都是读这个变量,只在极少数情况下才更新这个变量。如果采用两两互斥的做法,当一个线程读取变量时,别的读线程也必须等待,造成性能问题。这样的场景有很多:比如缓存的实现,主从数据库中对从数据库的更新(少)和读取(多)。


针对多读少写的场景,读写锁被设计出来。Java 并发包中提供了读写锁的实现:java.util.concurrent.locks.ReadWriteLock 接口和 java.util.concurrent.locks.ReentrantReadWriteLock 实现类,从名字上看这个实现类实现的是可重入锁。读写锁的使用也非常简单,写的时候加写锁,读的时候加读锁。


先看一个简单的例子代码:

package demo;
import java.util.HashMap;import java.util.concurrent.locks.Lock;import java.util.concurrent.locks.ReadWriteLock;import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReadWriteLockDemo { private HashMap<Object,Object> cache = new HashMap<>(); //共享变量,模拟缓存 private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock readLock = readWriteLock.readLock(); private Lock writeLock = readWriteLock.writeLock();
public Object put(Object key, Object value) { writeLock.lock(); try { return cache.put(key, value); } finally { writeLock.unlock(); } }
public Object get(Object key) { Object value; readLock.lock(); try { value = cache.get(key); if (value != null) { return value; } } finally { readLock.unlock(); }
//否则,模拟从数据库加载缓存,更新这个key的值.记得要先释放读锁,让其它线程有机会执行 writeLock.lock(); try { value = cache.get(key); if (value != null) { return value; } value = cache.put(key, 0L); } finally { writeLock.unlock(); }
return value; }
public static void main(String[] args) throws InterruptedException { final int num = 2; final ReadWriteLockDemo demo = new ReadWriteLockDemo(); final String key = "something";
//writer thread Thread writer = new Thread(() -> { int value = 0; while (true) { demo.put(key, value); System.out.println("writer put: " + value); value ++; try { Thread.sleep(2000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); writer.start();

Thread[] readers = new Thread[num];
for (int i = 0; i < num; i++) { readers[i] = new Thread(() -> { while (true) { System.out.println(Thread.currentThread().getName() + " read: " + demo.get(key)); try { Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); readers[i].start(); } writer.join(); for (Thread reader: readers) { reader.join(); } }}
复制代码

读写锁具有以下特性:

  1. 当写线程在写数据时,获取写锁,所有读线程不可以读数据;

  2. 当有 reader 线程在读时,获取读锁,所有读线程可以并发读,但写线程不可以写;

  3. 读写锁不支持升级和降级(在不释放锁的情况下直接转换成另外一种锁),一个线程同时只能持有读锁或者写锁,比如要写数据,必须先释放读锁,否则会造成死锁;

  4. 读写锁也支持条件变量,可以使用 await/signalAll 来进行线程间的协同;


通过这样的设计,读写锁很好的提升了特定的读多写少场景下的性能(主要是读性能)。但读写锁因为读写互斥,在读特别频繁的情况下,可能会造成写锁一直等待的情况,导致写入性能下降。有没有办法进一步提升读写锁写入的性能呢?答案下一篇文章揭晓。


最后留两道思考题:

  1. 为什么对于读操作还需要加锁?

  2. 为什么 get 方法在读不到 key 值的数据后,在尝试加载数据写入前需要再检查一下 value 是否为空?

发布于: 刚刚阅读数: 2
用户头像

BigBang!

关注

宁静致远,快乐随行,知行合一,得大自在! 2008-10-08 加入

一个程序员,一名架构师,一位技术管理人......

评论

发布
暂无评论
Java多线程系列9:读写锁_Java多线程_BigBang!_InfoQ写作社区