「而我们的读写锁(非公平)就是采用第二种策略,只要等待队列的头结点是尝试获取写锁的线程,那么读锁依然是不能插队的,目的是避免 "饥饿"」。
策略选择演示
口说无凭不写代码的行为简直不讲码德。还是得写代码演示下上述流程图的结论:
/** * 读写锁(非公平),读锁不插队 */ public class ReadLockJumpQueue { private static final ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock(); private static final ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock(); private static final ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock(); private static void read() { readLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到读锁,正在读取"); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() + "释放读锁"); readLock.unlock(); } } private static void write() { writeLock.lock(); try { System.out.println(Thread.currentThread().getName() + "得到写锁,正在写入"); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } finally { System.out.println(Thread.currentThread().getName() + "释放写锁"); writeLock.unlock(); } } public static void main(String[] args) throws InterruptedException { new Thread(ReadLockJumpQueue::read, "帅比狗哥").start(); Thread.sleep(100); new Thread(ReadLockJumpQueue::read, "渣男小钊").start(); Thread.sleep(100); new Thread(ReadLockJumpQueue::write, "写线程1").start(); Thread.sleep(100); new Thread(ReadLockJumpQueue::read, "海王小宝").start(); } }
从这个结果可以看出,不公平下的读锁选择了不允许插队的策略,从而很大程度上减小了发生 "饥饿" 的概率.
什么是读写锁的升降级?
先看段代码。代码演示的是在更新缓存的时候,如何使用读写锁的升降级。
/** * 更新缓存演示锁降级 */ public class CachedData { Object data; volatile boolean cacheValid; final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); void processCachedData() { rwl.readLock().lock(); if (!cacheValid) { //在获取写锁之前,必须首先释放读锁。 rwl.readLock().unlock(); rwl.writeLock().lock(); try { // 这里需要再次判断数据的有效性 // 因为在我们释放读锁和获取写锁的空隙之内,可能有其他线程修改了数据。 if (!cacheValid) { data = new Object(); cacheValid = true; } //在不释放写锁的情况下,直接获取读锁,这就是读写锁的降级。 rwl.readLock().lock(); } finally { //释放了写锁,但是依然持有读锁 rwl.writeLock().unlock(); } } try { System.out.println(data); } finally { //释放读锁 rwl.readLock().unlock(); } } }
先看看程序,注释写得很清楚了。大概的流程就是先获取读锁读缓存,再释放读锁获取写锁,写锁修改缓存。然后重点来了,「线程在不释放写锁的情况下,获取读锁(这就是锁的降级)」,然后释放写锁。读锁读取数据,最后释放读锁。
「PS:由于写锁是独占锁,当前线程获取写锁之后,其它线程就既不能获取写锁也不能获取读锁了,但是当前已经获取写锁的线程仍然可以获取读锁」。
为什么需要锁的降级?
你可能会说,写锁既能修改、又能读取。那直接用写锁就好了呀。干嘛要降级?其实不然,仔细看刚刚的代码:
data = new Object();
只有这一句是写的操作。如果这个线程一直用写锁,那其他线程在这段时间就无法获取锁操作了。浪费资源、降低了效率。「所以针对读多,写非常少的任务,还是用锁的降级比较明智」。
只支持降级,不支持升级
运行下面这段代码,「在不释放读锁的情况下直接尝试获取写锁,也就是锁的升级,会让线程直接阻塞,程序是无法运行的」。
final static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); public static void main(String[] args) { upgrade(); } public static void upgrade() { rwl.readLock().lock(); System.out.println("获取到了读锁"); rwl.writeLock().lock(); System.out.println("成功升级"); }
为什么不支持锁的升级?
读写锁的特点是如果线程都申请读锁,是可以多个线程同时持有的,可是如果是写锁,只能有一个线程持有,并且不可能存在读锁和写锁同时持有的情况。
举个栗子:假设线程 A 和 B 都想升级到写锁,那么对于线程 A 而言,它需要等待其他所有线程,包括线程 B 在内释放读锁。而线程 B 也需要等待所有的线程,包括线程 A 释放读锁。这就是一种非常典型的死锁的情况。谁都愿不愿意率先释放掉自己手中的锁。
一句话总结就是「多个线程同时发生锁升级的时候,会发生死锁,因为发生锁升级的线程会等待其它线程释放读锁」。
总结
1、定义
- 写锁也叫独占锁,它既能读取数据也能修改数据,同一时间只能有一个线程持有,它是非线程安全的
- 读锁也叫共享锁,它只能读取数据,允许多个线程同时持有,它是线程安全的
2、为什么?
- 读写锁的出现可以提高程序的执行效率
3、加锁规则
读读共享、其他都互斥(写写互斥、读写互斥、写读互斥)
4、插队策略
- 公平策略下,只要队列里有线程已经在排队,就不允许插队。
非公平策略下:
- 为了防止 “饥饿”,在等待队列的头结点是尝试获取写锁的线程的时候,不允许读锁插队。
- 写锁可以随时插队,因为写锁并不容易插队成功,写锁只有在当前没有任何其他线程持有读锁和写锁的时候,才能插队成功,同时写锁一旦插队失败就会进入等待队列,所以很难造成 “饥饿” 的情况,允许写锁插队是为了提高效率。
5、升降级策略
- 升降级策略:只能从写锁降级为读锁,不能从读锁升级为写锁。