写在前面:
Mutex(互斥锁)、ReentrantLock(重入锁)本质上都是一种排他锁,在同一时刻只允许一个线程进行访问,而读写锁允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程都会被阻塞。
ReadWriteLock:
- 引入:
在JDK5之前,读写锁的实现,是利用了等待通知机制+synchronized关键字。等待通知机制可以让所有晚于写操作的读操作进入等待状态,而同样都是写操作,则会用一个synchronized来修饰,进行同步。
- 使用场景:
在对读操作的要求>写操作的时候,读写锁具有很好的并发性和吞吐量。
- Java API:
在java.util.concurrent.lock包下,定义了一个ReadWriteLock接口,但是接口中只有两个方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
我们应用时,是用了他的实现类ReetrantReadWriteLock,他的一些工作状态方法如下:
方法名称 | 描述 |
getReadLockCount | 返回当前读锁被获取的次数。次数不等于读锁的线程数 eg:仅一个线程,连续获取了n次读锁,那么占据读锁的线程是1,但是方法返回值为n |
getReadHoldCount | 返回当前线程获取读锁的次数 |
isWriteLocked | 判断写锁是否被获取 |
getWriteHoldCount | 返回当前写锁被获取的次数 |
- 读写锁状态的设计:
读写锁与Mutex(互斥锁)、ReentrantLock(重入锁)的同步状态设计有所不同,由于读写锁支持多线程同步读操作,唯一线程写操作,所以仅一个状态位(int 类型)要维护多种状态,就必须将32位的二进制数字,进行按位切割使用。高16位表示读,低16位表示写。
补充:同步状态是指锁被一个线程重复获取的次数。
示例:假设当前的状态位为S,那么锁的写状态为S&0X0000FFFF,即将高16位抹去。读状态则S>>>16,即无符号右移16位。当写状态+1时,就等于=S+1,当读状态+1时,就等于=S+(1<<16)=S+0x000100000.
- 写锁的获取与释放:
在获取写锁的时候,有两种情况
(1) 当前锁已经获取写锁,这时候,只要增加写状态。
(2) 如果读锁已经被获取或者该线程不是已经获取写锁的线程,那么当前线程则进入了等待状态。
我们来看获取写锁的tryAcquire方法:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
读写锁是对读锁,写锁都可见的锁,所以说,当读锁被线程获取的时候,写锁未被获取时,写锁也不能被当前线程所获取,因为如果这是获取了写锁,则获取读锁的线程读到的数据不是最新的数据。
- 读锁的获取与释放:
读锁的获取也有两种情况:
(1) 当前没有线程占有写锁时,读锁会直接获取。
(2) 如果当前线程已经占据读锁,请求重新获取读锁,或增加读状态。
(3) 如果当前写锁已经被其他线程获取,那么当前线程则进入等待状态。
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)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
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);
}
- 锁降级:
锁降级到过程是:
(1) 当前线程持有写锁
(2)当前线程再获取到读锁
(3)释放掉原来持有的写锁
这一过程则成为锁降级,另外,ReetrantReadWriteLock不支持锁升级(把持读锁>>获取写锁>>释放读锁)。
注:为什么必须有第二步获取读锁才能释放写锁呢?
因为,假设当前线程A持有写锁,然后直接释放掉写锁。再去获取读锁,因为写锁已经被释放,那么其他线程B就会获取到写锁,对数据进行更新,此时当前线程修改的数据,可能对线程A是不可见的,所以,这种操作不锁降级。