01提出问题
首先,要解决这个问题,需要弄清楚什么是读写锁?
读写锁,并不是 Java 语言特有的,而是一个广为使用的通用技术,所有的读写锁都遵守以下三条基本原则:
- 允许多个线程同时读共享变量;
- 只允许一个线程写共享变量;
- 如果一个写线程正在执行写操作,此时禁止读线程读共享变量。
读写锁与互斥锁的一个重要区别就是读写锁允许多个线程同时读共享变量,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但读写锁的写操作是互斥的,当一个线程在写共享变量的时候,是不允许其他线程执行写操作和读操作。
而在Java中,读写锁是通过ReadWriteLock接口来实现的,其常用的实现类就是:ReentrantReadWriteLock。使用起来也很简单,就是调用ReentrantReadWriteLock的readLock()方法和writeLock()方法分别获得读锁和写锁,然后进行进一步的锁隔离操作。
我们提到的锁升级,其实就是读锁升级为写锁。换句换说,就是在读锁还没有解锁的情况下,又直接获取了写锁。
为了更好的理解锁升级,我们举个例子,有如下的代码:
Class Cache<K,V> { final Map<K, V> m = new HashMap<>(); final ReadWriteLock rwl = new ReentrantReadWriteLock(); final Lock r = rwl.readLock(); final Lock w = rwl.writeLock(); V get(K key) { V v = null; r.lock(); try { v = m.get(key); if (v == null) { w.lock(); try { // 再次验证并更新缓存 v = m.get(key); if(v == null){ v = 查询数据库 m.put(key, v); } } finally{ w.unlock(); } } } finally{ r.unlock(); } }}
代码内容是通过读写锁来实现一个按需加载的缓存。简单的来看,代码似乎没有多大的问题,而且还考虑了添加写锁后的再次校验,避免多线程都运行到12行w.lock()位置后,一个线程获取锁完成数据加载释放锁后,后序线程获取锁,没有进行再次校验,而导致重复加载数据的问题。二次校验减少了重复加载资源的开销。
可是当我们运行这段代码后,发现程序卡死,没办法继续运行下去。
这就是因为 ReadWriteLock 并不支持这种锁升级。在上面的代码示例中,读锁还没有释放,此时获取写锁,会导致写锁永久等待,最终导致相关线程都被阻塞,永远也没有机会被唤醒。
那么不能让读锁直接升级成写锁呢?
02问题解答
我们知道读写锁的特点是如果线程都申请读锁,是可以多个线程同时持有的,可是如果是写锁,只能有一个线程持有,并且不可能存在读锁和写锁同时持有的情况。
正是因为不可能有读锁和写锁同时持有的情况,所以升级写锁的过程中,需要等到所有的读锁都释放,此时才能进行升级。
假设有 A,B 和 C 三个线程,它们都已持有读锁。假设线程 A 尝试从读锁升级到写锁。那么它必须等待 B 和 C 释放掉已经获取到的读锁。如果随着时间推移,B 和 C 逐渐释放了它们的读锁,此时线程 A 确实是可以成功升级并获取写锁。
但是我们考虑一种特殊情况。假设线程 A 和 B 都想升级到写锁,那么对于线程 A 而言,它需要等待其他所有线程,包括线程 B 在内释放读锁。而线程 B 也需要等待所有的线程,包括线程 A 释放读锁。这就是一种非常典型的死锁的情况。谁都愿不愿意率先释放掉自己手中的锁。
但是读写锁的升级并不是不可能的,也有可以实现的方案,如果我们保证每次只有一个线程可以升级,那么就可以保证线程安全。只不过最常见的 ReentrantReadWriteLock 对此并不支持。