用途:
用于读多写少的场景。
特点:
- 有一个线程获取到了写锁,则在锁释放前除当前线程外,其他线程都不能获取到读锁或写锁。但当前线程可获取读锁(降级)和写锁(重入)。
- 有一个线程获取到了读锁,则在锁释放前所有线程只能获取到读锁(当前线程可再次重入读锁),不能获取到写锁(即不支持锁升级)。
- 读锁不支持条件变量,使用则抛异常。
读写锁的使用
private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
private Lock r = readWriteLock.readLock();
private Lock w = readWriteLock.writeLock(); // 读操作上读锁
public Data get(String key) {
r.lock();
try {
// TODO 业务逻辑
} finally {
r.unlock();
}
}
// 写操作上写锁
public Data put(String key, Data value) {
w.lock();
try {
// TODO 业务逻辑
} finally {
w.unlock();
}
}
锁降级
锁降级指的是写锁降级成为读锁。
如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。
锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。
锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失。
锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性
,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
RentrantReadWriteLock不支持锁升级
(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。
读写状态的设计
设计的精髓:用一个变量如何维护多种状态
在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,
读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。
分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为S,
那么:
- 写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.
- 读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于S+(1<<16),也就是S+0x00010000根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
HoldCounter 计数器
读锁的内在机制其实就是一个共享锁
。一次共享锁的操作就相当于对HoldCounter 计数器的操作。
获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。
只有当线程获取共享锁后才能对共享锁进行释放、重入操作。
通过 ThreadLocalHoldCounter 类,HoldCounter 与线程进行绑定。HoldCounter 是绑定线程的一个计数器,而 ThreadLocalHoldCounter 则是线程绑定的ThreadLocal。
HoldCounter是用来记录读锁重入数的对象ThreadLocalHoldCounter是ThreadLocal变量,用来存放不是第一个获取读锁的线程的其他线程的读锁重入数对象
写锁的获取
写锁是一个支持重进入的排它锁。
如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。写锁的获取是通过重写AQS中的tryAcquire方法实现的。
通过源码我们可以知道:
- 读写互斥
- 写写互斥
- 写锁支持同一个线程重入
- writerShouldBlock写锁是否阻塞实现取决公平与非公平的策略(FairSync和 NonfairSync)
写锁的释放
写锁释放通过重写AQS的tryRelease方法实现
读锁的获取
实现共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法。
- 读锁共享,读读不互斥
- 读锁可重入,每个获取读锁的线程都会记录对应的重入数
- 读写互斥,锁降级场景除外
- 支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放
- readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略FairSync和NonfairSync)
读锁的释放
获取到读锁,执行完临界区后,要记得释放读锁(如果重入多次要释放对应的次数),不然会
阻塞其他线程的写操作。读锁释放的实现主要通过方法tryReleaseShared。