ReentrantLock中,我们知道锁的状态是保存在Sync实例的state字段中的(继承自父类AQS),现在有了读写两把锁,然而可以看到还是只有一个Sync实例,那么一个Sync实例的state是如何同时保存两把锁的状态的呢?答案就是用了位分隔:
state字段是32位的int,读写锁用state的低16位保存写锁(独占锁)的状态;高16位保存读锁(共享锁)的状态。
因此要获取独占锁当前的重入数量,就是 state & ((1 << 16) -1) (即 exclusiveCount 方法)
要获取共享锁当前的重入数量,就是 state >>> 16 (即 sharedCount 方法)
static final int SHARED_SHIFT = 16; static final int SHARED_UNIT = (1 << SHARED_SHIFT); //每次要让共享锁+1,就应该让state加 1<<16 static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; //每种锁的最大重入数量 static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//计算写锁的掩码16个1。写状态:高16位都和0按位与运算,抹去高16位 state & Ox0000FFFF。 /** Returns the number of shared holds represented in count */ static int sharedCount(int c) { return c >>> SHARED_SHIFT; }//如果大于0有读锁 /** Returns the number of exclusive holds represented in count */ static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }//如果大于0有写锁
写锁
看下WriteLock类中的lock和unlock方法:
public void lock() { sync.acquire(1); } public void unlock() { sync.release(1); }
可以看到就是调用的独占式同步状态的获取与释放,因此真实的实现就是Sync的 tryAcquire和 tryRelease。
写锁的获取
看下tryAcquire:
protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread(); int c = getState(); int w = exclusiveCount(c); //获取独占锁的重入数 if (c != 0) { // 当前state不为0,此时:如果写锁状态为0说明读锁此时被占用返回false;如果写锁状态不为0且写锁没有被当前线程持有,说明别的线程持有写锁返回false 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; }
//到这里了说明state为0,尝试直接cas。writerShouldBlock是为了实现公平或非公平策略的,非公平锁直接返回false,公平锁判断是否有同步队列节点,如果有返回true,让同步队列头节点先获得锁,该线程添加到队尾。 if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) return false; setExclusiveOwnerThread(current);//设置保存获得锁的线程,以便进行重入所时判断 return true; }
逻辑很简单,直接看注释就能理解。
写锁的释放
看下tryRelease:
protected final boolean tryRelease(int releases) { if (!isHeldExclusively())//是否独占锁判断 throw new IllegalMonitorStateException(); //非独占模式直接抛异常 int nextc = getState() - releases; boolean free = exclusiveCount(nextc) == 0; if (free) setExclusiveOwnerThread(null); //如果独占模式重入数为0了,说明独占模式被释放 setState(nextc); //不管独占模式是否被释放,更新独占重入数 return free; }
读锁
类似于写锁,读锁的lock和unlock的实际实现对应Sync的 tryAcquireShared 和 tryReleaseShared方法。
读锁的获取
protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread(); int c = getState(); if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)//如果存在写锁,且不是本线程持有返回-1进入准备同步队列 return -1; //如果独占模式被占且不是当前线程持有,则获取失败 int r = sharedCount(c);
//如果有公平策略,先进行同步队列头节点获取状态,进行准备添加到同步队列尾;如果没有公平策略,判断同步队列第一个节点是否独占写,如果是不进行获取锁状态,进入下一轮锁获取;返回没有要求阻塞且重入数没有到达最大值,则直接尝试CAS更新state if (!readerShouldBlock() &&//是否公平策略,公平该方法判断是否同步队列不为空 r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
//更新成功后会在firstReaderHoldCount中或readHolds(ThreadLocal类型的)的本线程副本中记录当前线程重入数(浅蓝色代码),这是为了实现jdk1.6中加入的getReadHoldCount()方法的,这个方法能获取当前线程重入共享锁的次数(state中记录的是多个线程的总重入次数),加入了这个方法让代码复杂了不少,但是其原理还是很简单的:如果当前只有一个线程的话,还不需要动用ThreadLocal,直接往firstReaderHoldCount这个成员变量里存重入数,当有第二个线程来的时候,就要动用ThreadLocal变量readHolds了,每个线程拥有自己的副本,用来保存自己的重入数。这三个变量就是为readHolds变量服务的,是一个优化手段,尽量减少直接使用readHolds.get方法的次数,firstReader与firstReadHoldCount保存第一个获取读锁的线程,也就是readHolds中并不会保存第一个获取读锁的线程;cachedHoldCounter 缓存的是最后一个获取线程的HolderCount信息,该变量主要是在如果当前线程多次获取读锁时,减少从readHolds中获取HoldCounter的次数。
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId())
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
//用来处理CAS没成功的情况,逻辑和上面的逻辑是类似的,就是加了无限循环 }
jdk1.6中加入的getReadHoldCount()方法的,这个方法能获取当前线程重入共享锁的次数
final int getReadHoldCount() {
if (getReadLockCount() == 0)
return 0;
Thread current = Thread.currentThread();
if (firstReader == current)
return firstReaderHoldCount;
HoldCounter rh = cachedHoldCounter;
if (rh != null && rh.tid == current.getId())
return rh.count;
int count = readHolds.get().count;
if (count == 0) readHolds.remove();
return count;
}
下面这个方法就不用细说了,和上面的处理逻辑类似,加了无限循环用来处理CAS失败的情况。
final int fullTryAcquireShared(Thread current) { HoldCounter rh = null; for (;;) { int c = getState(); if (exclusiveCount(c) != 0) { if (getExclusiveOwnerThread() != current) return -1; // else we hold the exclusive lock; blocking here // would cause deadlock. } else if (readerShouldBlock()) { // Make sure we're not acquiring read lock reentrantly if (firstReader == current) { // assert firstReaderHoldCount > 0; } else { if (rh == null) { rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) { rh = readHolds.get(); if (rh.count == 0) readHolds.remove(); } } if (rh.count == 0) return -1; } } if (sharedCount(c) == MAX_COUNT) throw new Error("Maximum lock count exceeded"); if (compareAndSetState(c, c + SHARED_UNIT)) { if (sharedCount(c) == 0) { firstReader = current; firstReaderHoldCount = 1; } else if (firstReader == current) { firstReaderHoldCount++; } else { if (rh == null) rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); rh.count++; cachedHoldCounter = rh; // cache for release } return 1; } } }
读锁的释放
protected final boolean tryReleaseShared(int unused) { Thread current = Thread.currentThread();
//浅蓝色代码也是为了实现jdk1.6中加入的getReadHoldCount()方法,在更新当前线程的重入数。 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; }
//这里是真正的释放同步状态的逻辑,就是直接同步状态-SHARED_UNIT,然后CAS更新,没啥好说的 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; } }
补充内容
通过上面的源码分析,我们可以发现一个现象:
在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)
在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)
仔细想想,这个设计是合理的:因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
综上:
一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;
写锁可以“降级”为读锁;
读锁不能“升级”为写锁。
在非公平锁下有线程获得读锁的情况下,后面一直有连续的读锁中间穿插了一个写锁,那写锁会不会饿死????
写锁不会饿死的,因为读锁在获取时在非公平锁下会先判断同步队列的头节点是否是写锁,如果是写锁头节点,会不抢占CAS,进入到下一轮循环。
总结
读写锁还是很实用的,因为一般场景下,数据的并发操作都是读多于写,在这种情况下,读写锁能够提供比排它锁更好的并发性。
在读写锁的实现方面,本来以为会比较复杂,结果看完源码的感受也是快刀切西瓜,看来AQS的设计真的很棒,在AQS的基础上构建的组件实现都很简单。