ReentrantReadWriteLock
JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁。
线程进入读锁的前提条件:
- 没有其他线程的写锁
- 没有其他请求或者有写请求,但调用线程和持有锁的线程是同一个
线程计入写锁的前提条件
- 没有其他线程的读锁
- 没有其他线程的写锁
读写锁有下面三个重要的特性:
- 公平选择性:支持非公平(默认)和公平锁的获取方式,吞吐量还是非公平由于公平。
- 可重入:读锁和写锁都支持线程重入。读线程获取读锁后,能够再次获取读锁;写锁在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
- 锁降级:遵循获取写锁,再获取读锁最后释放锁写的次序,写锁能够降级为读锁。
ReentrantReadWriteLock实现读写分离,代码示例
public class ReentrantReadWriteLockDemo {
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock readLock = lock.readLock();//读锁
private final Lock writeLock = lock.writeLock();//写锁
private final String[] data = new String[10];
public void write(int index, String value) {
writeLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"获取写锁");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
read(2);
data[index] = value;
} finally {
System.out.println(Thread.currentThread().getName()+"释放写锁");
writeLock.unlock();
}
}
public String read(int index) {
readLock.lock();
try {
System.out.println(Thread.currentThread().getName()+"获取读锁");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 此时会直接导致死锁
// write(2, "rwl");
return data[index];
} finally {
System.out.println(Thread.currentThread().getName()+"释放读锁");
readLock.unlock();
}
}
public static void main(String[] args) {
ReentrantReadWriteLockDemo rwl = new ReentrantReadWriteLockDemo();
// 测试读读,读写,写写场景
new Thread(()->{
//rwl.read(2);
rwl.write(2,"rwl");
// rwl.read(2);
}).start();
// new Thread(()->rwl.read(2)).start();
// new Thread(()->rwl.write(2,"rwl")).start();
// new Thread(()->rwl.write(2,"rwl")).start();
}
其中ReentrantReadWriteLock实现ReadWriteLock接口
ReentrantReadWriteLock类内部中存在ReadLock.class和WriteLock.class两个内部类分别实现读锁和写锁,
下图为两个类的派生关系
读写锁状态的设计
设计的精髓:用一个变量如何维护多种状态
在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。
分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为S,那么:
- 写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.
- 读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于S+(1<<16)
根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// (1 << 16 ) -1 ) = 1111111111111111 = 65535
// (1 << 16 ) = 10000000000000000 = 65536
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 获取共享读锁,获取持有读锁状态的锁的数量
// 而对于每个持有锁的线程的重入次数则由ThreadLocalHoldCounter(ThreadLocal)进行计数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 获取独占写锁,获取写状态的锁的重入次数。
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
其中ReadLock锁的Lock方法主要实现方法tryAcquireShared()如下
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果存在写锁并且独占线程不为当前线程,直接结束,然后执行入阻塞队列操作
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
// 获取读锁数量
int r = sharedCount(c);
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
// 如果没有读锁,则记录第一个线程及初始化对应HoldCount
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 如第一个记录线程为当前线程,对应HoldCount ++
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// 后续线程将其对应的重入次数记录到对应的ThreadLocalHoldCounter中,每个线程独享的count
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
其中WriteLock的上锁如下tryAcquire()方法
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// 如果state状态不为0,但写锁为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;
}
// 如果state为0,则设置CAS state状态,并且将当前线程设置为独占线程
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
锁降级
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失。
如下为ReentrantReadWriteLock.class注释中实例的锁降级
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) {
cacheValid = true;
}
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // 关闭写锁,仍保持读锁
}
}
// 锁降级完成,写锁降级为读锁
try {
// 使用数据的流程
} finally {
rwl.readLock().unlock();
}
}
}