关于读写锁状态的存取
// ReentrantReadWriteLock.Sync
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; }// 高16位存储读锁
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }// 低16位存储写锁
// 设置读锁的状态
compareAndSetState(c, c + SHARED_UNIT)
// 设置写锁的状态
compareAndSetState(c, c + 1)
使用state(int类型,32位)的高16位存储读锁状态,低16位存储写锁状态
读写锁的结构
ReentrantReadWriteLock本身不是一把锁,但是它管理着两把锁,读锁和写锁。写锁是独占锁,读锁是共享锁。
这里ReadWriteLock只是一个接口,并不是一把锁。
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
公平锁和非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {// 返回false会尝试去获取锁
return apparentlyFirstQueuedIsExclusive();
}
}
// 这个方法指用在获取读锁上,即获取共享锁上
// 即下一个要被唤醒的节点是不是独占节点,即这个节点是不是在申请写锁
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
// 下一个要唤醒的节点不是共享节点,就返回true
// 即如果下一个要唤醒的是申请写锁的线程,就阻塞读锁的获取
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// h!=t证明起码有一个线程在等待 s.thread!= currentThread 因为读,写锁都是可重入的,所以,如果等待的这个线程正好是当前线程
// 可以直接放行让它获取锁
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
在ReentrantLock中,公平锁与非公平锁的区别是,非公平锁不管同步队列中有没有线程在排队,都会先去获取锁。而公平锁,就是直接先去排队。
那么同样的定义,在ReentrantReadWriteLock中也体现得很明显。
FairSync和NonFairSync都重写了writerShouldBlock,readerShouldBlock方法。
writerShouldBlock用在获取独占锁,也就是写锁的方法上。
// Sync acquire方法会调用tryAcquire方法获取锁,acquire方法会被WriteLock.lock方法调用,即获取写锁的时候调用
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())// c != 0,w == 0,证明有线程获取了读锁
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
// 如果是公平锁,writerShouldBlock将会看前面有没有线程在排队,有排队的,writerShouldBlock才返回true,即这里直接返回false(获取锁失败)
// 如果是非公平锁,这里返回false,会调用CAS设置state,设置失败才返回false
// 所以,就一个writerShouldBlock方法就可以实现了公平锁和非公平锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
获取写锁流程图如下:
readerShouldBlock方法被tryAcquireShared和fullyTryAcquireShared调用
// Sync tryAcquireShared会被acquireShared调用,acquireShared被ReadLock.lock调用,即获取读锁的时候调用
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);
}
读锁和写锁
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquireShared(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean tryLock() {
return sync.tryReadLock();// 这是唯一一个没有用AQS方法的方法
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.releaseShared(1);
}
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
// ReentrantReadWriteLock.Sync
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
int c = getState();
// exclusiveCount指的是获取写锁的数目
// 如果有别的线程已经获取了写锁,则失败(如果是同一个线程获取了写锁,则无所谓,因为同一个线程的读写肯定不会冲突)
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
int r = sharedCount(c);// 读锁的数目
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {// 获取读锁成功
if (r == 0) {// 如果原来没有线程获取到读锁
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// 一个同步器有一个cachedHoldCounter,一个Lock有一个同步器
// cachedHoldCounter是上一个获取到读锁的线程的获取锁的数目
// 如果当前线程获取了读锁,更新占有锁的数目
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 true;
}
}
}
可以看到ReadLock中,除了tryLock方法,其他调的都是AQS提供的方法。其中lock方法也在公平锁和非公平锁中讲过了。
tryLock的流程也跟lock的流程基本一致,除了lock中加了readerShouldBlock方法的判断。这里就不详解了。
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquire(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock( ) {
return sync.tryWriteLock();// 也是只有这个方法是自定义的
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}
public int getHoldCount() {
return sync.getWriteHoldCount();
}
}
// ReentrantReadWriteLock.Sync
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {// 可能有写锁或读锁被获取了
int w = exclusiveCount(c);
// w == 0表示,如果有线程获取了读锁,就直接失败吗(为什么)不是同一个线程可以直接获取读锁和写锁吗?如果它之前已经获取了读锁,现在再来获取写锁会直接失败吗?
// 或者有线程获取了写锁,但是只要不是当前线程,就直接失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1))
return false;
setExclusiveOwnerThread(current);
return true;
}
写锁与读锁结构也基本一致,自己实现了tryLock方法,流程与tryAcquire基本一致,这里不细讲。
读写锁的同步队列
读写锁虽然持有了两把锁,但是只有一个Sync,即只有一个同步队列。
那只有一个同步队列,是怎么管理独占节点和共享节点的阻塞和唤醒的呢?
获取锁在上面都讲了,现在讲讲读写锁是怎么释放的。
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
这是写锁的释放,总体来说比较简单。
看看当前线程还持有多少个锁,如果释放完以后,还持有0个锁,就将当前独占的线程设为空。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
实例
构建几个场景:
场景一:
读+读+写(这里读、写分别代表申请读写的线程,按+的先后顺序,表示线程申请读写锁的时间顺序,比如在这里就代表,线程1申请了读锁,之后,线程2申请读锁,线程3申请写锁),这时候同步队列应该是什么样的呢?
public class LockExample {
public void testReadWriteLock() {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
CountDownLatch countDownLatch = new CountDownLatch(3);
t1ReadLock(readWriteLock,countDownLatch,0,3);
t2WriteLock(readWriteLock,countDownLatch,2,3);
t3ReadLock(readWriteLock,countDownLatch,1,3);
try {
countDownLatch.await();
} catch (InterruptedException e) {
}
}
}
以下为线程对应的工作,在所有场景中会复用。
private void t1ReadLock(ReentrantReadWriteLock readWriteLock,CountDownLatch countDownLatch,int startDelay, int duration) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(startDelay);
} catch (InterruptedException e) {
}
System.out.println("thread1 begin fetch readLock");
readWriteLock.readLock().lock();
System.out.println("thread1 has fetch readLock");
try {
System.out.println("======= thread1 do things ==========");
TimeUnit.SECONDS.sleep(duration);// 休眠过程中不会放开锁
} catch (InterruptedException e) {
} finally {
System.out.println("now readWriteLock queue:"+readWriteLock.getQueueLength());
System.out.println("======= thread1 do things end ,unlock readLock ==========");
readWriteLock.readLock().unlock();
countDownLatch.countDown();
}
}
});
thread1.start();
}
private void t2WriteLock(ReentrantReadWriteLock readWriteLock,CountDownLatch countDownLatch,int startDelay,int duration) {
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(startDelay);
} catch (InterruptedException e) {
}
System.out.println("thread2 begin fetch writeLock");
readWriteLock.writeLock().lock();
try {
System.out.println("======= thread2 do things ==========");
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
} finally {
System.out.println("now readWriteLock queue:"+readWriteLock.getQueueLength());
System.out.println("======= thread2 do things end,unlock writeLock ==========");
readWriteLock.writeLock().unlock();
countDownLatch.countDown();
}
}
});
thread2.start();
}
private void t3ReadLock(ReentrantReadWriteLock readWriteLock,CountDownLatch countDownLatch,int startDelay, int duration) {
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(startDelay);
} catch (InterruptedException e) {
}
System.out.println("thread3 begin fetch readLock");
readWriteLock.readLock().lock();
System.out.println("thread3 has fetch readLock");
try {
System.out.println("======= thread3 do things ==========");
TimeUnit.SECONDS.sleep(duration);
} catch (InterruptedException e) {
} finally {
System.out.println("now readWriteLock queue:"+readWriteLock.getQueueLength());
System.out.println("======= thread3 do things end unLock ReadLock ==========");
readWriteLock.readLock().unlock();
countDownLatch.countDown();
}
}
});
thread3.start();
}
thread1 begin fetch readLock
thread1 has fetch readLock
======= thread1 do things ==========
thread3 begin fetch readLock
thread3 has fetch readLock
======= thread3 do things ==========
thread2 begin fetch writeLock
now readWriteLock queue:1
======= thread1 do things end ,unlock readLock ==========
now readWriteLock queue:1
======= thread3 do things end unLock ReadLock ==========
======= thread2 do things ==========
now readWriteLock queue:0
======= thread2 do things end,unlock writeLock ==========
从结果可以看出,读+读+写的时候,两个读锁可以并行执行,写锁需要等两个读锁执行完后才能获取锁。看同步队列中,线程数目为1也可以证实这一点。
从代码逻辑来看:
获取写锁的前提是,当前没有任何线程持有读锁,且写锁的获取者为当前线程,或没有线程获取到写锁。
获取读锁的前提是,除了当前线程,其他线程都没有获取到写锁。
所以读+读+写,最终的结论是(读,读+写)
场景二:
读+写+读
public void testReadWriteLock() {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
CountDownLatch countDownLatch = new CountDownLatch(3);
t1ReadLock(readWriteLock,countDownLatch,0,3);
t2WriteLock(readWriteLock,countDownLatch,1,3);// 只调整了写锁和读锁的开始时间
t3ReadLock(readWriteLock,countDownLatch,2,3);
try {
countDownLatch.await();
} catch (InterruptedException e) {
}
}
thread1 begin fetch readLock
thread1 has fetch readLock
======= thread1 do things ==========
thread2 begin fetch writeLock
thread3 begin fetch readLock
now readWriteLock queue:2
======= thread1 do things end ,unlock readLock ==========
======= thread2 do things ==========
now readWriteLock queue:1
======= thread2 do things end,unlock writeLock ==========
thread3 has fetch readLock
======= thread3 do things ==========
now readWriteLock queue:0
======= thread3 do things end unLock ReadLock ==========
神奇了!如果按场景一的理解,这时候的结果就应该跟场景一的一致。但现在线程3却跟线程2一起阻塞了,这是为什么呢?
原因是,读写锁默认是非公平锁。
非公平锁中重写了readerShouldBlock方法。其中readerShouldBlock方法中
判断,如果下一个要唤醒的节点为写锁,则阻塞当前读锁的获取(至于为什么这么做,可能是因为担心饥饿问题把,毕竟读锁是共享的,如果一直都让读锁共享,很容易导致写锁一直没有机会获取)。
所以,对于读+写+读,最终的顺序就是(读+写+读)
附+测试:
对于读+写+读+读,后面两个读线程会并发执行吗?
public void testReadWriteLock() {
final int DURATION_TIME = 5;
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
CountDownLatch countDownLatch = new CountDownLatch(4);
t1ReadLock(readWriteLock,countDownLatch,0,DURATION_TIME);
t2WriteLock(readWriteLock,countDownLatch,1,DURATION_TIME);
t3ReadLock(readWriteLock,countDownLatch,2,DURATION_TIME);
t4ReadLock(readWriteLock,countDownLatch,3,DURATION_TIME);
try {
countDownLatch.await();
} catch (InterruptedException e) {
}
}
thread1 begin fetch readLock
thread1 has fetch readLock
======= thread1 do things ==========
thread2 begin fetch writeLock
thread3 begin fetch readLock
thread4 begin fetch readLock
now readWriteLock queue:3
======= thread1 do things end ,unlock readLock ==========
======= thread2 do things ==========
now readWriteLock queue:2
======= thread2 do things end,unlock writeLock ==========
thread3 has fetch readLock
======= thread3 do things ==========
thread4 has fetch readLock
======= thread4 do things ==========
now readWriteLock queue:0
now readWriteLock queue:0
======= thread3 do things end unLock ReadLock ==========
======= thread4 do things end unLock ReadLock ==========
这里后面thread3和thread4是并行执行完的。因为线程2释放的时候,会去释放线程3。线程3在执行doAcquireShared方法的时候,会释放后继节点,即线程4。
场景三:
写+写+读
这其实很简单,写线程是独占锁,所以最终顺序就是写+写+读
public void testReadWriteLock() {
final int DURATION_TIME = 4;
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
CountDownLatch countDownLatch = new CountDownLatch(3);
t1ReadLock(readWriteLock,countDownLatch,2,DURATION_TIME);
t2WriteLock(readWriteLock,countDownLatch,0,DURATION_TIME);
t5WriteLock(readWriteLock,countDownLatch,1,DURATION_TIME);
try {
countDownLatch.await();
} catch (InterruptedException e) {
}
}
thread2 begin fetch writeLock
======= thread2 do things ==========
thread5 begin fetch writeLock
thread1 begin fetch readLock
now readWriteLock queue:2
======= thread2 do things end,unlock writeLock ==========
======= thread5 do things ==========
now readWriteLock queue:1
======= thread5 do things end,unlock writeLock ==========
thread1 has fetch readLock
======= thread1 do things ==========
now readWriteLock queue:0
======= thread1 do things end ,unlock readLock ==========
场景四:
同一个线程,先获取写锁再获取读锁
private void writeToReadLock(ReentrantReadWriteLock readWriteLock) {
Thread thread = new Thread(()->{
readWriteLock.writeLock().lock();
System.out.println("do write things");
readWriteLock.readLock().lock();
try {
} finally {
System.out.println("unlock writeLock");
readWriteLock.writeLock().unlock();
System.out.println("unlock readLock");
readWriteLock.readLock().unlock();
}
});
thread.start();
}