java并发包下面提供了读写锁ReentrantReadWriteLock,在读多余写的情况下,续写所能提供比排它锁更好的并发性和吞吐量。它内部维护了一对读写锁,一个用于只读操作,称作读锁;一个用于写入操作,称作写锁。此外,在没有写操作时,读读操作可以并发,其他写读、读写和写写操作都是互斥的。
读写锁三个重要的特性:
公平性选择:支持非公平和公平的锁获取方式,默认是非公平,因为非公平吞吐量比公平锁高。
可重入:读锁和写锁都支持线程重入
锁降级:线程获取写锁后,在获取读锁之后释放写锁的过程为写锁降级为读锁
应用场景
我们知道HashMap是线程不安全的,通过读写锁ReentrantReadWriteLock不仅可以使其保证线程安全,在读多写少的情况下还能保证良好的吞吐量。从应用场景来看,不难猜测,读锁是通过AQS的共享锁实现的,支持多线程同时获得锁;而写锁为互斥锁是通过AQS的独占锁实现的。
public class RWDictionary {
private final Map<String, Object> m = new HashMap<>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Object get(String key) {
r.lock();
try {
return m.get(key);
} finally {
r.unlock();
}
}
public Object[] allKeys() {
r.lock();
try {
return m.keySet().toArray();
} finally {
r.unlock();
}
}
public Object put(String key, Object value) {
w.lock();
try {
return m.put(key, value);
} finally {
w.unlock();
}
}
public void clear() {
w.lock();
try {
m.clear();
} finally {
w.unlock();
}
}
public static void main(String[] args) {
RWDictionary dictionary = new RWDictionary();
dictionary.put("str", "str");
log.debug(dictionary.get("str").toString());
}
}
锁降级:
锁降级是指写锁降级为读锁。注意:锁降级是在把持住写锁,再获取读锁,随后释放写锁的过程;锁降级可以帮助当前线程修改公共资源后继续使用不被其他线程更新而导致更新丢失。
public class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
if (!cacheValid) {
data = "data";
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
log.debug("test:{}", data);
} finally {
rwl.readLock().unlock();
}
}
}
读写锁状态的state设计
上面已经说到,ReentrantReadWriteLock是通过AQS的独占锁和共享锁实现写锁和读锁的,那么在底层是如何通过一个int类型变量state表示这两种状态的呢?我们知道int类型在java中是有4个字节32位构成的,这里通过高16位表示读状态,低16位表示写状态,高低位的数值大小分别表示读写的重入次数。
写锁的获取逻辑
写锁的释放逻辑
判断当前线程是否为获得锁的线程,不是直接抛异常,是则将state值减1,判断低16位值是否等于0,是则将AQS#exclusiveOwnerThread置位null并表示释放锁成功,否则表示释放锁失败。最后设置state低16位减1的值。释放锁成功则会唤醒双向列表中头结点的下一个节点的线程。
获取读锁的逻辑
读锁的获取逻辑相对写锁要复杂一些,前面提到state高16位表示读锁的状态值,高16位大小表示获得读锁的线程数量,而某一线程获得读锁的次数是通过ThreadLocal实现的,每个线程会维护一个计数器HoldCounter存入ThreadLocal中。
释放读锁的逻辑
首先将当前线程的重入次数减1,然后for循环+CASstate,如果读锁得数量等于0,则唤醒下一个节点的线程可能是阻塞的获取写锁和读锁的线程。