ReentrantReadWriteLock实现 ReadWriteLock 接口,可重入的读写锁实现类。在它内部,维护了一对相关的锁,一个用于只读操作,另一个用于写入操作。读取锁可以由多个 Reader 线程同时保持。也就说,写锁是独占的,读锁是共享的。
ReentrantReadWriteLock 代码结构如下:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
// ignore code
}
abstract static class Sync extends AbstractQueuedSynchronizer {
}
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;
}
// ignore code
}
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;
}
// ignore code
}
在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int
类型的 state
来表示同步状态,表示锁被一个线程重复获取的次数。但是,问题在于:读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,怎么办??
需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。
分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为S,那么:
- 写状态,等于
S & 0x0000FFFF
(将高 16 位全部抹去) - 读状态,等于
S >>> 16
(无符号补 0 右移 16 位)。
代码如下:
// Sync.java
static final int SHARED_SHIFT = 16; // 位数
static final int SHARED_UNIT = (1 << SHARED_SHIFT);//读运算使用
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; // 每个锁的最大重入次数,65535
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//写运算使用
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
不同于写锁,读锁可以同时被多个线程持有。而每个线程持有的读锁支持重入的特性,所以需要对每个线程持有的读锁的数量单独计数,这就需要用到 HoldCounter 计数器,代码如下:
/**
* ThreadLocal subclass. Easiest to explicitly define for sake
* of deserialization mechanics.
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
/**
* A counter for per-thread read hold counts.
* Maintained as a ThreadLocal; cached in cachedHoldCounter
*/
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
static final long getThreadId(Thread thread) {
return UNSAFE.getLongVolatile(thread, TID_OFFSET);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long TID_OFFSET;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class<?> tk = Thread.class;
TID_OFFSET = UNSAFE.objectFieldOffset
(tk.getDeclaredField("tid"));
} catch (Exception e) {
throw new Error(e);
}
}
HoldCounter 的作用就是当前线程持有共享锁的数量,这个数量必须要与线程绑定在一起,否则操作其他线程锁就会抛出异常。
通过 ThreadLocalHoldCounter 类,HoldCounter 就可以与线程进行绑定了。故而,HoldCounter 应该就是绑定线程上的一个计数器,而 ThreadLocalHoldCounter 则是线程绑定的 ThreadLocal。从上面我们可以看到 ThreadLocal 将 HoldCounter 绑定到当前线程上,同时 HoldCounter 也持有线程编号,之所以持有线程编号是因为这样HoldCounter 绑定线程编号而不绑定线程对象的原因是,避免 HoldCounter 和 ThreadLocal 互相绑定而导致 GC 难以释放它们(尽管 GC 能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助 GC 快速回收对象而已。
ReentrantReadWriteLock 继承Sync, Sync 继承AQS,总结来看,ReentrantReadWriteLock 是基于AQS的锁框架上的,通过 Sync 实现tryAcquire, tryAcquireShared, tryRelease, tryReleaseShared 完成读锁和写锁的功能。
以独占锁获取锁为例,其中AQS的锁框架完成获取锁失败后的所有操作,步骤及代码如下:
- 线程包装为队列结点,加入CLH队列(addWaiter)
- 设置前置结点通知标记(作用:前置结点释放锁后,唤醒后置结点,完成队列中结点的唤醒),线程休眠。
- 自旋不断获取共享资源state,直到获取成功。
//写锁流程框架
//class Sync extends AbstractQueuedSynchronizer
//Sync 继承 AbstractQueuedSynchronizer 功能
//WriteLock#lock
public void lock() {
sync.acquire(1);
}
//AbstractQueuedSynchronizer#acquire
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
AQS源码分析:https://segmentfault.com/a/1190000015739343
最后需要思考一个问题,为什么AQS只维护一个共享变量呢?我想主要原因是因为CAS的限制,而AQS的代码中共享资源的获取都是基于CAS的所以设计上共享变量只有一个。