在讲解 读写锁之前 首先要 讲以下
1.读锁的获取 每次获取一次 那么state 就会自增 63356 为什么是 63356 呢 因为 转换城二进制 = 1 0000 0000 0000 0000 每获取一次 那么就在 第 17位加1即可
获取 读锁获取的次数 就可以 使用 state >> 16 那么就可以拿到
2. 写锁 的获取 一次就自增1. 写锁获取的次数 就让state & 65535 ,因为65535转换成二进制后 等于 1111 1111 1111 1111 就是如此的巧妙,因为 > 65535 那么 & 65535 那么肯定是=0的 如果 锁降级 那么state= 65537 (65536 +1)那么 & 65535 也是=1 这一点在读写锁中 非常的重要
也就是和 线程池 里面差不多 高于 16位的 保存 共享的次数
低于等于16位的 保存 独占的次数
**
**
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) //尝试获取共享锁 重点
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
获取当前状态
int c = getState();
通过 state & 65535 获取写锁获取的次数
如果不等于0 代表已经有线程 获取了写锁
如果 获取写锁的线程不是自己 那么 直接返回 -1 加入到等待队列
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
走到这里 就是有机会获取读锁了 获取读锁 共享的次数
state >> 16 即可得到
int r = sharedCount(c);
这个方法分为 公平 与 非公平 2种情况
if (!readerShouldBlock() &&
获取共享的次数 因为共享的次数大于 65535 之后 再次共享就是 <=0了不符合逻辑
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
如果cas成功 将 每个线程 所重入的次数 保存在 ThreadLoacl中
而get()方法会得不到的时候会有一个默认值 因为他重写了 initValue方法
HoldCounter 保留了 重入的次数 默认从1开始 和线程id
每次重入一次加加
为什么单独把 第一个共享的线程 和 后续的一个线程放在成员变量中
应该是为了性能,暂时考虑重 ThreadLoaclMap中获取是比直接获取耗时的
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);
}
readerShouldBlock() 方法的 公平情况
public final boolean hasQueuedPredecessors() {
这里如果是 公平状态 比较简单 直接判断 headNode的下一个节点的线程是不是当前线程 公平锁 先进先出的 概念
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
readerShouldBlock() 方法的 不公平情况
情况1. head=空 没有人排队直接 拿到返回fasle 进行cas
情况2. head != null tail=null 可能 刚有等待节点加入到链表种 new Node()
才初始化 那么非公平状态直接拿锁, 还有一种情况就是 某个线程刚被unPark()
情况3,头尾都不为空 如果等待被唤醒的线程 是 共享节点 那么直接返回进行cas
如果是 独占节点 再次判断 是否取消排队
PS:情况3 会出现因为 state=0了还没来得及 unPark 所以首位不为空
公平状态也会有这种情况出现
为什么 如果是 独占节点 那么会有 “优先权呢”
这儿代码 作者 的注释写了 不要让 共享节点 让 独占节点等待太久
默认 独占节点 优先级会更高点
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
该方法位 返回<0 表示 没有获取到 读锁的情况
private void doAcquireShared(int arg) {
//自旋 添加到 等待链表中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
在aqs的设计之中 后面的节点在被唤醒 或者 有机会尝试获取锁之前
要保证 preNode就是headNode headNode表示了 当前持有锁的Node对应的线程 第一次除外【其实第一次也隐藏表示了拿到锁的线程就是 new Node()】
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
这个地方 有一个比较经典的 唤醒步骤 如果走到这里
代表 要么被其他线程唤醒 要么 直接进来 繁殖不管怎么样
都会去尝试 唤醒后续的节点
避免 共享节点 过长的让 独占节点阻塞 因为读读是共享的
使得 等待链表中的所有 阻塞住的读节点 京可能的 复苏
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
该方法 主要是将 preNode 的 状态设置成signal 代表后续节点
需要 被唤醒 经过2次 循环直接 park 等待
if (shouldParkAfterFailedAcquire(p, node) &&
此处会 park 当前线程 进入watting状态
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
readLock.unLock 释放读锁 比较简单
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
是否为 在共享的同时 第一个拿到读锁的
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
不是就从 ThreadLocal中 获取 然后 自减 有重入的情况存在
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 (;;) {
如果state=0 代表所有共享的线程都 释放了锁 那么将要唤醒后续的线程了
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;
}
}
当 释放读锁 state=0 的情况 这个方法 是共享锁比较关键的地方
这个方法 的意图就是 如果后续 独占节点拿到锁 则 退出循环【因为 写 与 读写都是互
斥的 所以只要有写锁被 独占节点拿到 那么 后续被唤醒的节点 都无法获取到锁也就无法
更改 headNode 你是 共享节点不同 共享节点 每共享成功一次 那么headNode就被换成
自己对应的 节点】
如果是共享节点拿到锁 那么经快 轮询把 处于 等待的共享节点 尽可能的给唤醒
减少 共享节点 等待的时间过长 从而是 独占线程 等待太长时间
后面的 加那个判断 就是如上意思
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
这个地方考虑的是性能问题 避免重复 的唤醒 nextNode
为什么呢 因为想想 第一个 共享节点 开始 将后面的节点 风暴试唤醒的情况下
当把nextNode唤醒之后 然后他吧自己设置成了HeadNode
但是想想 这个第一个执行unlock的线程也在循环遍历呢 那么 就有可能 回来的时候
那么 操作的就是 同一个head 第一个unlock操作的 是nextNode 的node
nextNode 操作他本身 然后 2个线程一起 cas
其实这个不一定只有2个线程同时操作一个 因为唤醒的 共享节点越多
那么 每个共享节点 都在用这个方法 就有可能操作同一个head 所以这是一个性能的考虑的cas操作
避免对同一个headNode 的nextNode对应的线程 unpark多次
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
在状态 不为signal 但是=0的情况下 代表后续节点 没有处于等待
但是 如果cas 失败 代表这个时候 后面 又有了等待节点
如果有了等待的节点那么 立即 再次循环 唤醒 算是一个优化 但是显得没必要
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
//写锁的获取 和reentranLock 种独占锁的获取 其实 差不多
public final void acquire(int arg) {
if (!tryAcquire(arg) && 尝试获取 加入失败加入到队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
尝试获取写锁
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
计算 独占的次数 & 65535
int w = exclusiveCount(c);
如果state 不等于 那么 有可能之前有人获取了 读或者写锁
反正不等于0 只要不是当前线程是无法获取锁的 不是直接加入同步队列等待
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;
}
判断 是否应该阻塞 也存在 公平非公平
不公平直接返回fasle cas
公平的话 就是 是不是headNode。nextNode的Thread 先进先出的盖帘
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
写锁的释放
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
尝试释放写锁
protected final boolean tryRelease(int releases) {
如果不是当前线程 抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
state & 65535 之所以这么做 因为会有 锁降级 的情况 所以不能直接
所以
当独占的次数=0的时候 即使锁降级 读读也是共享的 后续也会 把后续节点唤醒 经可能 不然 共享节点阻塞 独占节点 unparkSuccessor()
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
唤醒后续线程
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
将当前节点 恢复成0 后续节点 已经要唤醒了改变状态
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
这儿之所以会有一个方向获取是 因为
在 enq方法种 cas之前 一定是将 node.prev = t;这个执行了的
所以这个pre一定对 其他线程可见 volatile语义 ,因为有可能cas
之后 t.next = node;并没有执行 所以 会这么做
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}