一、简介
读写锁在同一时刻容许多个读线程拜访,然而在写线程拜访时,所有的读线程和其余写线程均被阻塞。读写锁保护了两把锁,一把读锁和一把写锁。获取读写锁可分为上面两种状况:
- 同一线程:该线程获取读锁后,可能再次获取读锁,但不能获取写锁。该线程获取写锁后,可能再次获取写锁,也能够再获取读锁。
- 不同线程:A线程获取读锁后,B线程能够再次获取读锁,不能够获取写锁。A线程获取写锁后,B线程无奈获取读锁和写锁。
二、读写锁示例
public class Cache { static Map<String, Object> map = new HashMap<String, Object>(); static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); static Lock r = rwl.readLock(); static Lock w = rwl.writeLock(); public static final Object get(String key) { r.lock(); try { return map.get(key); } finally { r.unlock(); } } public static final Object put(String key, Object value) { w.lock(); try { return map.put(key, value); } finally { w.unlock(); } }}
上述示例中,Cache组合一个非线程平安的HashMap作为缓存的实现,同时应用读写锁来保障Cache是线程平安的。
三、读写锁的实现剖析
3.1 读写状态的设计
回忆AQS的实现,同步状态示意锁被获取的次数,而读写锁的自定义同步器须要在同步状态(一个整型变量)上保护多个读线程和一个写线程的状态,该状态的设计成为设计读写锁的要害。
如果在一个整型变量上保护多种状态,就须要“按位切割应用”这个变量,读写锁将变量切分成了两局部,高16位示意读,低16位示意写,划分形式如下图。
上图同步状态示意一个线程曾经获取了写锁,且重进入了两次,同时也间断获取了两次读锁。读写锁时如何确定读和写的状态?答案是通过位运算。假如以后同步状态为S,写状态等于 S & 0x0000FFFF(将高16位抹去),读状态等于S >>> 16(无符号补0右移16位)。当写状态减少1时,等于S + 1,当读状态减少1时,等于S + (1 << 16),也就是 S + 0x00010000。
3.2 写锁的获取与开释
写锁是一个反对重入的排它锁。如果以后线程曾经获取了写锁,则减少写状态。如果以后线程在获取写锁时,读锁曾经被获取(读状态不为0)或者该线程不是曾经获取写锁的线程,则以后线程进入期待状态。代码如下
protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); if (c != 0) { // 存在读锁或者以后获取线程不是曾经获取写锁的线程 if (w == 0 || current != getExclusiveOwnerThread()) { return false; } if (w + exclusiveCount(acquires) > MAX_COUNT) throw new Error("Maximum lock count exceeded"); setState(c + acquires); return true; } if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) { return false; } setExclusiveOwnerThread(current); return true;}
如果存在读锁(即便只有以后线程获取了读锁也不行),则写锁不能被获取,起因在于:读写锁要确保写锁的操作对读锁可见,因而只有期待其余读线程都开释了读锁,写锁能力被以后线程获取,而写锁一旦获取,其余读写线程的后续拜访均被阻塞。
3.3 读锁的获取与开释
读锁是一个反对重入的共享锁,它可能被多个线程同时获取,在写状态为0时,读锁总会被胜利获取,而所做的也只是减少读状态。如果以后线程在获取读锁时,写锁已被其余线程获取,则进入期待状态。
protected final int tryAcquireShared(int unused) { for (;;) { int c = getState(); int nextc = c + (1 << 16); if (nextc < c) throw new Error("Maximum lock count exceeded"); if (exclusiveCount(c) != 0 && owner != Thread.currentThread()) return -1; if (compareAndSetState(c, nextc)) return 1; }}
如果其余线程曾经获取了写锁,则获取读锁失败,进入期待状态。
读锁的每次开释均缩小读状态,缩小的值是1 << 16。
3.4 锁降级
锁降级指的是写锁降级成读锁。锁降级是持有了写锁之后,在获取到读锁,随后开释之前领有的写锁,那么只剩下读锁,这个过程是锁降级(读写锁不反对锁降级)。代码如下
public void processData() { readLock.lock(); if (!update) { // 必须先开释读锁 readLock.unlock(); // 锁降级从写锁获取开始 writeLock.lock(); try { if (!update) { update = true; } readLock.lock(); } finally { writeLock.unlock(); } // 锁降级实现,写锁降级为读锁 } try { // 应用update } finally { readLock.unlock(); }}
下面代码中锁降级中,读锁的获取是否必要?是必要的,因为批改update之后,后续还要应用到update,所以为了避免其余线程批改update,所以须要加读锁。
四、总结
个别状况下,读写锁的性能优于ReentrantLock,因为大多数场景读是远大于写的,所以在读多于写的状况下,读写锁可能提供更好的并发性和吞吐量。