1,前言
ReentrantLock
是一个排他锁,这种锁在同一时刻只允许一个线程进行访问。在实际生产中,多个线程同时读一个资源是不会产生并发问题的
读写锁在同一时刻可以允许多个线程访问,但是在写线程访问时,所有的读线程和其他写线程均会被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁来提高性能。
讨论读写锁时,会涉及到重入锁
2,读写锁的实现分析
2.1,读写状态设计
读写锁是通过分离读锁与写锁,以实现共享读和独占写
那么同步状态的设计是一个问题。
读写锁使用整型变量来维护多个读线程和一个写线程的状态
读写锁将变量切分成了两个部分,用高16位表示读,低16位表示写
因为读写锁是一个可重入锁,所以这里的读状态统计实际上是多个线程重复获取读锁的次数总和。而每个线程各自获取读锁的次数只能选择保存在ThreadLocal
中,由线程自身维护,这使得获取读锁的实现变得复杂。
读写锁中关于获取状态的相关源代码如下:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
......
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
......
}
}
2.2,写锁的获取和释放
写锁是一个支持重进入的排他锁。如果当前线程获取了写锁,则增加了写状态。
如果一个线程试图获取写锁时,发现读锁已经被获取;或者线程试图获取写锁时,发现写锁已经被其他线程获取了;这些情况会导致线程被阻塞。
之所以当获取写锁,而读锁已经被获取时,线程会被阻塞;是因为读写锁要保证写锁的操作对于读锁是可见的。否则就会产生并发问题。
相关的源代码如下:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
......
abstract static class Sync extends AbstractQueuedSynchronizer {
......
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)
//这里代表着:当前有线程获取了锁,如果w为0,代表着写锁并未被获取,那么自然是读锁被获取了;如果w不为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;
}
......
}
......
}
读锁的释放与ReentrantLock
的释放过程是一致的,有多少次获取该锁,就有对应次数的释放动作。
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 {
......
}
return 1;
}
return fullTryAcquireShared(current);
}
在这个方法中,如果其他线程已经获取了写锁,那么当前线程获取读锁时就会失败,并进入等待状态;如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠 CAS 保证)增加读状态,成功获取读锁(获取写锁再然获取读锁,这与锁降级有关)
2.4,锁降级
锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,那么在持有写锁的情况下,获取读锁,获取之后再释放写锁,这个过程就叫锁降级。