前面介绍了 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 是否为空? 
评论