前面介绍了 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();
}
}
}
复制代码
读写锁具有以下特性:
当写线程在写数据时,获取写锁,所有读线程不可以读数据;
当有 reader 线程在读时,获取读锁,所有读线程可以并发读,但写线程不可以写;
读写锁不支持升级和降级(在不释放锁的情况下直接转换成另外一种锁),一个线程同时只能持有读锁或者写锁,比如要写数据,必须先释放读锁,否则会造成死锁;
读写锁也支持条件变量,可以使用 await/signalAll 来进行线程间的协同;
通过这样的设计,读写锁很好的提升了特定的读多写少场景下的性能(主要是读性能)。但读写锁因为读写互斥,在读特别频繁的情况下,可能会造成写锁一直等待的情况,导致写入性能下降。有没有办法进一步提升读写锁写入的性能呢?答案下一篇文章揭晓。
最后留两道思考题:
为什么对于读操作还需要加锁?
为什么 get 方法在读不到 key 值的数据后,在尝试加载数据写入前需要再检查一下 value 是否为空?
评论