第八章 - JUC
J.U.C
AQS 原理
全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架
特点:
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取
锁和释放锁- getState - 获取 state 状态
- setState - 设置 state 状态
- compareAndSetState - cas 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively
获取锁的姿势
// 如果获取锁失败
if (!tryAcquire(arg)) {
// 入队, 可以选择阻塞当前线程 park unpark
}
释放锁的姿势
// 如果释放锁成功
if (tryRelease(arg)) {
// 让阻塞线程恢复运行
}
结点状态waitStatus
这里我们说下Node。Node结点是对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。变量waitStatus则表示当前Node结点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。
- CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
- SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
- CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
- 0:新结点入队时的默认状态。
注意,负值表示结点处于有效等待状态,而正值表示结点已被取消。所以源码中很多地方用>0、<0来判断结点的状态是否正常。
实现不可重入锁
@Slf4j
public class TestAQS {
public static void main(String[] args) {
MyLock lock = new MyLock();
new Thread(() -> {
lock.lock();
try {
log.debug("locking...");
sleep(1000);
} finally {
log.debug("unlocking...");
lock.unlock();
}
}, "t1").start();
new Thread(() -> {
lock.lock();
try {
log.debug("locking...");
} finally {
log.debug("unlocking...");
lock.unlock();
}
}, "t2").start();
}
static class MyLock implements Lock {
public MyLock() {
}
private final Sync sync = new Sync();
@Override
public void lock() {
while (true) {
if (sync.tryAcquire(1)) {
break;
}
}
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.tryRelease(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 尝试加锁
*
* @param arg
*
* @return
*/
@Override
protected boolean tryAcquire(int arg) {
if (1 == arg) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
return false;
}
/**
* 尝试释放
*
* @param arg
*
* @return
*/
@Override
protected boolean tryRelease(int arg) {
if (1 == arg) {
// if (getState() == 0) {
// throw new IllegalMonitorStateException();
// }
setExclusiveOwnerThread(null);
// 后写入state,因为state是volatile所以写入volatile变量方式,volatile修饰的变量前面的所有变量都需要从主存读取一次,防止可能影响到state的写入
// 这样就能保证 setExclusiveOwnerThread 方法的写入也是volatile的
setState(0);
return true;
}
return false;
}
/**
* 状态
*
* @return
*/
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
/**
* 设置条件
*
* @return
*/
protected Condition newCondition() {
return new AbstractQueuedSynchronizer.ConditionObject();
}
}
}
}
心得
AQS 要实现的功能目标
- 阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire
- 获取锁超时机制
- 通过打断取消机制
- 独占机制及共享机制
- 条件不满足时的等待机制
设计
AQS 的基本思想其实很简单
获取锁的逻辑
while(state 状态不允许获取) {
if(队列中还没有此线程) {
入队并阻塞
}
}
当前线程出队
释放锁的逻辑
if(state 状态允许了) {
恢复阻塞的线程(s)
}
要点
- 原子维护 state 状态
- 阻塞及恢复线程
- 维护队列
state 设计
- state 使用 volatile 配合 cas 保证其修改时的原子性
- state 使用了 32bit int 来维护同步状态,因为当时使用 long 在很多平台下测试的结果并不理想
阻塞恢复设计
- 早期的控制线程暂停和恢复的 api 有 suspend 和 resume,但它们是不可用的,因为如果先调用的 resume 那么 suspend 将感知不到
- 解决方法是使用 park & unpark 来实现线程的暂停和恢复,具体原理在之前讲过了,先 unpark 再 park 也没问题
- park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
- park 线程还可以通过 interrupt 打断
队列设计
使用了 FIFO 先入先出队列,并不支持优先级队列
设计时借鉴了 CLH 队列,它是一种单向无锁队列
队列中有 head 和 tail 两个指针节点,都用 volatile 修饰配合 cas 使用,每个节点有 state 维护节点状态入队伪代码,只需要考虑 tail 赋值的原子性
do {
// 原来的 tail
Node prev = tail;
// 用 cas 在原来 tail 的基础上改为 node
} while(tail.compareAndSet(prev, node))
出队伪代码
// prev 是上一个节点
while((Node prev=node.prev).state != 唤醒状态) {
}
// 设置头节点
head = node;
CLH 好处:
- 无锁,使用自旋
- 快速,无阻塞
AQS 在一些方面改进了 CLH
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 队列中还没有元素 tail 为 null
if (t == null) {
// 将 head 从 null -> dummy
if (compareAndSetHead(new Node()))
tail = head;
} else {
// 将 node 的 prev 设置为原来的 tail
node.prev = t;
// 将 tail 从原来的 tail 设置为 node
if (compareAndSetTail(t, node)) {
// 原来 tail 的 next 设置为 node
t.next = node;
return t;
}
}
}
}
主要用到 AQS 的并发工具类
ReentrantLock 原理
非公平锁实现原理
加锁流程分析
先从构造器开始看,默认为非公平锁实现
public ReentrantLock() {
// 这是一个不公平锁
sync = new NonfairSync();
}
NonfairSync 继承自 AQS
没有竞争时
第一个竞争出现时
然后再看lock方法
public void lock() {
sync.acquire(1);
}
进入
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
我们开始分析代码非公平上锁方案
分析这三个方法开始
(1)tryAcquire
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// acquires == 1
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
下面就是我们需要分析的代码
@ReservedStackAccess
// acquires == 1
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取state状态
int c = getState();
// 如果状态为 0
if (c == 0) {
// cas 设置 state 状态,如果预期值为0(未上锁),则将其上锁为 1
if (compareAndSetState(0, acquires)) {
// 设置当前拥有独占访问权的线程
setExclusiveOwnerThread(current);
// 返回为 true 表示当前线程获取了独占权限
return true;
}
}
// 如果当前线程 == 独占线程,说明这是同一个线程,表示当前线程独占
else if (current == getExclusiveOwnerThread()) {
// c不等于0,c + 1,如果第二次的话,c==1 nextc 最终等于2
int nextc = c + acquires;
// 如果下一个状态为负数则抛出错误,说明int state 溢出了(超出了Integer.MAX_VALUE值)
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 将 state 设置为 2
setState(nextc);
// 本线程线程独占,返回true
return true;
}
// 线程获取独占状态错误
return false;
}
- CAS 尝试将 state 由 0 改为 1,结果失败
- 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败
- 接下来进入 addWaiter 逻辑,构造 Node 队列
- 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
- Node 的创建是懒惰的
- 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程
(2)addWorker
private Node addWaiter(Node mode) {
// 这里内部将当前线程绑定到指定的Node
Node node = new Node(mode);
for (;;) {
// 获取双向链表尾节点
Node oldTail = tail;
// 尾节点不为空
if (oldTail != null) {
// 将node.prev的前一个节点设置为原链表的最后一个节点(线程安全cas)
node.setPrevRelaxed(oldTail);
// 将尾节点设置为node(线程安全cas)
if (compareAndSetTail(oldTail, node)) {
// 如果设置成功,则原链表尾节点的next等于node节点
oldTail.next = node;
// 返回现在的尾节点
return node;
}
} else {
// 尾节点为空表示队列不存在,初始化队列的head和tail
initializeSyncQueue();
}
}
}
(2)acquireQueued
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 直接调用中断方法 Thread.currentThread().interrupt();
selfInterrupt();
}
这个方法主要是将node添加到链表中,而后根据中断状态来判断是否执行selfInterrupt方法
// node == Node.EXCLUSIVE, arg == 1
final boolean acquireQueued(final Node node, int arg) {
// 中断标志
boolean interrupted = false;
try {
for (;;) {
// 获取双向链表中 node 的前一个节点是否存在
final Node p = node.predecessor();
// 如果前一个节点刚好是头节点(这里头节点可能是null null==null),则尝试再次获取独占模式(因为这个时候内部大概率没有多余的节点抢夺独占状态)
if (p == head && tryAcquire(arg)) {
// 上锁成功,设置这个节点为头节点
setHead(node);
// 前一个节点设置为空,防止可能存在的多余引用
p.next = null; // help GC
// 不需要中断
return interrupted;
}
// 上面的加锁失败了,说明存在head的双向链表,所以需要把node挂到没被取消的节点上
if (shouldParkAfterFailedAcquire(p, node))
// park,并且通过调用interrupted方法判断是否被中断
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
// 取消掉mode的独占状态
cancelAcquire(node);
// 如果parkAndCheckInterrupt检测到了中断,把中断功能传递出来
if (interrupted)
// 调用中断
selfInterrupt();
throw t;
}
}
下面就是将 p 挂到节点上的方法
分析下面这个方法其实很简单
这个向分析这个方法的返回值
true表示会执行 parkAndCheckInterrupt 方法,这个方法代表着阻塞
false表示不会阻塞
然后再以参数来分析
pred 是前一个节点的意思
node是当前节点的意思(这里也存在pred和node是同一个节点)
小总结:
现在可以分析这个函数借助 前一个节点(pred)和 当前节点(node)通过一段代码得到 true or false 阻塞或者不阻塞当前节点的结果
现在我们分析在哪里为true 哪里为false
小心得:可以借助if{} == 正方形来辅助分析源码
if (ws == Node.SIGNAL) 如果前一个节点为SIGNAL状态的话,可以阻塞当前节点所绑定的线程
其他的if判断全是不阻塞的
if (ws > 0) { 之后的功能就是找到前面取消的节点,全部删除掉,将node挂到没取消的节点上
} else { 这个else就是使用cas尝试设置SIGNAL到我们pred节点上
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 第一次进来 prev.waitStatus == 0,走 else
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 第1+n次调用会进入这个判断, 只要compareAndSetWaitStatus设置成功,则返回true
// 这个节点多次无法获取独占状态,那么还不如阻塞,但是需要前一个节点来释放后一个节点的阻塞状态,这个时候需要SIGNAL了
return true; // 阻塞
// 大于 0 取消状态
if (ws > 0) {
// 找到前面的未被取消的节点,将node挂在到双向链表上
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 前一个节点必须为SINGAL状态才能够去释放当前节点, 第一次执行代码走这里,想让waitStatus设置为SIGNAL状态, 只有设置为这个状态才能够阻塞
pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
}
return false; // 不阻塞
}
当前线程进入 acquireQueued 逻辑
- acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
- 如果自己是紧邻着 head(排第二位),那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
- 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false
- shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,当然这时state 仍为 1,失败
- 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回true
- 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
再次有多个线程经历上述过程竞争失败,变成这个样子
(3)selfInterrupt
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
这样就已经不需要再次详解了
上面的三个方法执行完毕之后会将当前多次获取独占模式失败的线程阻塞,只有以获取独占线程调用了tryRelease函数之后才会被环境
现在我们代码开始分析
解锁流程分析
public void unlock() {
sync.release(1);
}
// arg == 1
public final boolean release(int arg) {
// 尝试释放独占模式
if (tryRelease(arg)) {
// 获取双向链表头节点
Node h = head;
// 判断是否不存在头节点并且头节点的waitStatus状态是否不等于0(存在等待状态)
if (h != null && h.waitStatus != 0)
// unpark方法
unparkSuccessor(h);
return true;
}
return false;
}
下面我们分析这几个函数
(1)tryRelease
@ReservedStackAccess
// releases == 1
protected final boolean tryRelease(int releases) {
// 把state和我们需要释放的 releases 相减
int c = getState() - releases;
// 当前线程和独占线程不相同,则抛出异常,这个判断以后的代码就是线程安全的了
if (Thread.currentThread() != getExclusiveOwnerThread())
// 抛出异常
throw new IllegalMonitorStateException();
// 判断是否跳出了重入锁的环境
boolean free = false;
// 表示重入锁重入完毕
if (c == 0) {
// 表示可以真的释放锁了
free = true;
// 设置独占状态为null,现在其他线程可以重新拿到独占线程了
setExclusiveOwnerThread(null);
}
// 把计算完毕的 c 的值复制给 state
setState(c);
return free;
}
Thread-0 释放锁,进入 tryRelease 流程,如果成功
设置 exclusiveOwnerThread 为 null
state = 0
当前队列不为 null,并且 head 的 waitStatus = -1,进入 unparkSuccessor 流程
找到队列中离 head 最近的一个 Node(没取消的),unpark 恢复其运行,本例中即为 Thread-1
回到 Thread-1 的 acquireQueued 流程
如果加锁成功(没有竞争),会设置
- exclusiveOwnerThread 为 Thread-1,state = 1
- head 指向刚刚 Thread-1 所在的 Node,该 Node 清空 Thread
- 原本的 head 因为从链表断开,而可被垃圾回收
如果这时候有其它线程来竞争(非公平的体现),例如这时有 Thread-4 来了
如果不巧又被 Thread-4 占了先
- Thread-4 被设置为 exclusiveOwnerThread,state = 1
- Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞
(2)unparkSuccessor
// node == head 头节点
private void unparkSuccessor(Node node) {
// 获取头节点等待状态
int ws = node.waitStatus;
// 如果头节点等待状态小于0
if (ws < 0)
// 修改为正确的模式
node.compareAndSetWaitStatus(ws, 0);
// 获取头节点的下一个节点
Node s = node.next;
// 如果下一个节点为null,或者下一个节点的等待状态大于0
if (s == null || s.waitStatus > 0) {
// 下一个节点赋值为 null
s = null;
// 这个循环主要是为了从尾巴开始找符合p.waitStatus <= 0条件的node
// 判断尾节点是否不等于node并且也不等于null
for (Node p = tail; p != node && p != null; p = p.prev)
// 尾节点的状态小于等于0
if (p.waitStatus <= 0)
// 赋值给s
s = p;
}
// 找到了head节点之后的那个节点
if (s != null)
// 解锁这个头节点之后的这个节点所绑定的线程
LockSupport.unpark(s.thread);
}
可重入锁原理
final boolean nonfairTryAcquire(int acquires) {
// ...
int c = getState();
if (c == 0) {
// ...
}
else if (current == getExclusiveOwnerThread()) {
// 这里就是可重入的原理
int nextc = c + acquires;
setState(nextc);
}
}
protected final boolean tryRelease(int releases) {
// 可重入原因
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
// 可重入原因, 如果满足计数器为 0 则标志为释放 free == true, 在外围的方法就会被unpark掉
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
public final boolean release(int arg) {
if (tryRelease(arg)) { // true
Node h = head;
if (h != null && h.waitStatus != 0)
// 释放 unpark
unparkSuccessor(h);
return true;
}
return false;
}
可打断原理
不可打断模式
在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后方能得知自己被打断了
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node))
// 产生interrupt但是仍然再for循环
interrupted |= parkAndCheckInterrupt();
}
可打断模式
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 产生中断,直接抛出异常
throw new InterruptedException();
}
公平锁实现原理
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
// 获取当前线程
final Thread current = Thread.currentThread();
// 获取 state
int c = getState();
// 判断 state 是否为 0
if (c == 0) {
// hasQueuedPredecessors 判断存在不存在前节点,如果没有才会去尝试使用cas竞争
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 设置当前线程独占
setExclusiveOwnerThread(current);
// 设置成功获取独占成功
return true;
}
// 否则上锁失败直接返回 false
}
// 如果刚好是当前线程自己独占
else if (current == getExclusiveOwnerThread()) {
// 设置 state
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置 state
setState(nextc);
return true;
}
// 上锁失败
return false;
}
}
public final boolean hasQueuedPredecessors() {
Node h, s;
if ((h = head) != null) {
if ((s = h.next) == null || s.waitStatus > 0) {
s = null;
// 从尾节点开始往前找
for (Node p = tail; p != h && p != null; p = p.prev) {
// 节点不是取消状态的,就把节点放入到s变量
if (p.waitStatus <= 0)
s = p;
}
}
// 找到了不是取消状态的节点,判断这个节点是否就是当前线程的节点
if (s != null && s.thread != Thread.currentThread())
// 是,这返回true,外面函数直接取反,最终导致无法上锁
return true;
}
// 存在前节点,新节点无法直接启用,需要进入等待队列
return false;
}
public final void acquire(int arg) {
// 上面的代码直接返回false取反满足为 true,则会进入 addWaiter将当前节点放入等待队列
if (!false &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
条件变量实现原理(Condition)
每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject
await 流程
开始 Thread-0 持有锁,调用 await,进入 ConditionObject 的 addConditionWaiter 流程创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部
接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁
unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功
park 阻塞 Thread-0
源码建议配合上面的图片一起看
public final void await() throws InterruptedException {
// 判断线程是否中断
if (Thread.interrupted())
// 中断抛出异常
throw new InterruptedException();
// 添加一个新节点到等待队列中
Node node = addConditionWaiter();
// 释放独占模式,修改 state
int savedState = fullyRelease(node);
int interruptMode = 0;
// 判断这个节点是否在队列上
while (!isOnSyncQueue(node)) {
// 不在的话直接上锁
LockSupport.park(this);
// 表示线程被唤醒的操作:确定中断模式并退出循环
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 前面分析过了 成功获取独占锁后,并判断 interruptMode 的值
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 清除非condition
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
// 根据前面的 mode 做对应的事情
// 若为 THROW_IE 则抛出异常中断线程
// 若为 REINTERRUPT 则设置线程中断标记位
reportInterruptAfterWait(interruptMode);
}
private Node addConditionWaiter() {
// 如果同步不只对当前线程持有,则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node t = lastWaiter;
// 如果 lastWaiter 不是condition状态,则删除掉这个节点
if (t != null && t.waitStatus != Node.CONDITION) {
// 删除掉非condition节点
unlinkCancelledWaiters();
// 把新的尾节点给 t
t = lastWaiter;
}
// 创建一个新的Node节点并且绑定当前线程
Node node = new Node(Node.CONDITION);
// 如果尾节点不存在的话,直接把新的节点丢给firstwaiter
if (t == null)
firstWaiter = node;
else
// 如果末尾节点还存在,则将新的节点赋值给末尾节点的下一个节点字段next
t.nextWaiter = node;
// 记录末尾节点为新节点
lastWaiter = node;
return node; // 返回新节点
}
// node == 新节点
final int fullyRelease(Node node) {
try {
// 获取 state
int savedState = getState();
// 释放 state(其实就是加减state)
if (release(savedState))
// 如果成功,则返回剩余的 state
return savedState;
// 否则抛出异常
throw new IllegalMonitorStateException();
} catch (Throwable t) {
// 上面代码抛出了异常,给新节点添加取消状态
node.waitStatus = Node.CANCELLED;
// 把异常抛出去
throw t;
}
}
public final boolean release(int arg) {
// 尝试释放独占模式
if (tryRelease(arg)) { // tryRelease 前面已经有源码分析
// 成功,获取当前头节点
Node h = head;
// 当前头节点存在并且头节点的waitStatus状态不为初始化状态
if (h != null && h.waitStatus != 0)
// 解锁 unpark
unparkSuccessor(h); // 前面存在源码分析
// 解锁成功
return true;
}
// 解锁失败
return false;
}
以上是await函数的底层分析
signal 流程
假设 Thread-1 要来唤醒 Thread-0
进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node
执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的waitStatus 改为 -1
Thread-1 释放锁,进入 unlock 流程,分析过了
下面是源码级分析
public final void signal() {
// 如果同步不只对当前线程持有,则抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取第一个节点
Node first = firstWaiter;
// 如果第一个节点不为空
if (first != null)
// 分析代码
doSignal(first);
}
private void doSignal(Node first) {
do {
// 如果下一个节点为空的话
if ( (firstWaiter = first.nextWaiter) == null)
// 标记尾节点为null
lastWaiter = null;
// 让下一个节点为空
first.nextWaiter = null;
// 将节点从条件队列移动到同步队列
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
final boolean transferForSignal(Node node) {
// 设置节点为COndition状态 如果成功则直接返回false,如果失败返回true
if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
return false;
// 将节点从条件队列移动到同步队列
Node p = enq(node);
int ws = p.waitStatus;
// 如果节点为取消状态或者设置SIGNAL状态失败,则上锁
if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
LockSupport.unpark(node.thread);
// 否则表示转移成功
return true;
}
读写锁 -- ReentrantReadWriteLock
读写锁允许 多读少写的引用场景
除了读和读锁其他情况都会存在阻塞问题
import java.util.concurrent.locks.ReentrantReadWriteLock;
import lombok.extern.slf4j.Slf4j;
import static com.zhazha.utils.Sleeper.sleep;
@Slf4j(topic = "c.demo")
public class Demo {
public static void main(String[] args) {
CacheData data = new CacheData();
// 读取和读取之间不存在阻塞问题
// new Thread(data::read, "r1").start();
new Thread(data::read, "r2").start();
// 数取和写入之间存在阻塞问题
new Thread(data::write, "w1").start();
// 写入和写入之间存在安全问题
// new Thread(data::write, "w2").start();
}
static class CacheData {
private Object data;
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private final ReentrantReadWriteLock.ReadLock r = rw.readLock();
private final ReentrantReadWriteLock.WriteLock w = rw.writeLock();
/**
* 读
*/
public void read() {
log.debug("获取读锁。。。");
r.lock();
try {
sleep(1000);
log.debug("读取。。。");
} finally {
log.debug("释放读锁。。。");
r.unlock();
}
}
/**
* 写
*/
public void write() {
log.debug("获取写锁。。。");
w.lock();
try {
sleep(1000);
log.debug("写入。。。");
} finally {
log.debug("释放写锁");
w.unlock();
}
}
}
}
注意事项
读锁不支持条件变量
重入时升级不支持:即持有读锁时,如果再次获取写锁则会被永远阻塞
r.lock();
try {
// ...
w.lock(); // 程序会永远阻塞在这里
try {
// ...
} finally {
w.unlock();
}
} finally {
r.unlock();
}
重入降级:即持有写锁的情况下去获取读锁
public static void main(String[] args) {
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
new Thread(() -> {
rw.writeLock().lock(); // 正常情况下写锁遇到写锁会被阻塞
rw.readLock().lock();
log.debug("在这里写锁会降级为读锁");
Sleeper.sleep(1000);
rw.writeLock().unlock();
rw.readLock().unlock();
}, "t1").start();
new Thread(() -> {
rw.writeLock().lock(); // 正常情况下写锁遇到写锁会被阻塞
rw.readLock().lock();
log.debug("在这里写锁会降级为读锁");
Sleeper.sleep(1000);
rw.writeLock().unlock();
rw.readLock().unlock();
}, "t2").start();
}
主要目标是为了在写锁解锁前立即上读锁
高并发缓存方案
缓存更新策略
这里共享变量就是缓存
把 缓存 当作共享变量,那么对共享变量的操作存在,清空、写入、查询三个操作,这三个操作在高并发场景下,无法保证顺序
such as 先写入,突然被清空,然后再查询
对共享变量 数据库 的修改和查询操作在高并发下无法保证顺序,所以也存在线程安全问题,但是在这个过程中也就有可能会出现查询延迟问题,所以无所谓了
import java.io.Serializable;
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class TestGenericDao {
public static void main(String[] args) {
GenericDao dao = new CacheGenericDao();
System.out.println("============> 查询");
String sql = "select * from emp where empno = ?";
int empno = 2;
Emp emp = dao.queryOne(Emp.class, sql, empno);
System.out.println(emp);
emp = dao.queryOne(Emp.class, sql, empno);
System.out.println(emp);
emp = dao.queryOne(Emp.class, sql, empno);
System.out.println(emp);
System.out.println("============> 更新");
dao.update("update emp set sal = ? where empno = ?", 800, empno);
emp = dao.queryOne(Emp.class, sql, empno);
System.out.println(emp);
}
}
class CacheGenericDao extends GenericDao {
/**
* 缓存
*/
private final Map<SqlPair, Object> cache = new HashMap<>();
private final GenericDao dao = new GenericDao();
private final ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
public <T> List<T> queryList(Class<T> beanClass, String sql, Object... args) {
return dao.queryList(beanClass, sql, args);
}
public <T> T queryOne(Class<T> beanClass, String sql, Object... args) {
SqlPair sqlPair = new SqlPair(sql, args);
rw.readLock().lock();
try {
T t = (T) cache.get(sqlPair);
if (null != t) {
return t;
}
} finally {
rw.readLock().unlock();
}
rw.writeLock().lock();
try {
T value = (T) cache.get(sqlPair);
if (null == value) {
value = dao.queryOne(beanClass, sql, args);
cache.put(sqlPair, value);
}
return value;
} finally {
rw.writeLock().unlock();
}
}
public int update(String sql, Object... args) {
SqlPair sqlPair = new SqlPair(sql, args);
rw.writeLock().lock();
try {
cache.remove(sqlPair);
return dao.update(sql, args);
} finally {
rw.writeLock().unlock();
}
}
private class SqlPair implements Serializable {
private final String sql;
private final Object[] args;
@Override
public String toString() {
return "SqlPair{" + "sql='" + sql + '\'' + ", args=" + Arrays.toString(args) + '}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
SqlPair sqlPair = (SqlPair) o;
return Objects.equals(sql, sqlPair.sql) && Arrays.equals(args, sqlPair.args);
}
@Override
public int hashCode() {
int result = Objects.hash(sql);
result = 31 * result + Arrays.hashCode(args);
return result;
}
public SqlPair(String sql, Object[] args) {
this.sql = sql;
this.args = args;
}
}
}
补充一种情况,假设查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询
这种情况很少见,但是会出现
注意
- 以上实现体现的是读写锁的应用,保证缓存和数据库的一致性,但有下面的问题没有考虑
- 适合读多写少,如果写操作比较频繁,以上实现性能低
- 没有考虑缓存容量
- 没有考虑缓存过期
- 只适合单机
- 并发性还是低,目前只会用一把锁
- 更新方法太过简单粗暴,清空了所有 key(考虑按类型分区或重新设计 key)
- 乐观锁实现:用 CAS 去更新
读写锁底层原理
图解流程
t1 w.lock,t2 r.lock
1)t1 成功上锁,流程与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位
2)t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写锁占据,那么 tryAcquireShared 返回 -1 表示失败
tryAcquireShared 返回值表示
-1 表示失败
0 表示成功,但后继节点不会继续唤醒
正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1
3)这时会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态
4)t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
5)如果没有成功,在 doAcquireShared 内 for (; 循环一次,把前驱节点的 waitStatus 改为 -1,再 for (; 循环一次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park
t3 r.lock,t4 w.lock
这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子
t1 w.unlock
这时会走到写锁的 sync.release(1) 流程,调用 sync.tryRelease(1) 成功,变成下面的样子
接下来执行唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内parkAndCheckInterrupt() 处恢复运行这回再来一次 for (; 执行 tryAcquireShared 成功则让读锁计数加一
这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
事情还没完,在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
这回再来一次 for (; 执行 tryAcquireShared 成功则让读锁计数加一
这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点
t2 r.unlock,t3 r.unlock
t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但由于计数还不为零
t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,这回计数为零了,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二,即
之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (; 这次自己是老二,并且没有其他竞争,tryAcquire(1) 成功,修改头结点,流程结束
源码分析
写锁上锁流程
static final class NonfairSync extends Sync {
// ... 省略无关代码
// 外部类 WriteLock 方法, 方便阅读, 放在此处
public void lock() {
sync.acquire(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquire(int arg) {
if (
// 尝试获得写锁失败
!tryAcquire(arg) &&
// 将当前线程关联到一个 Node 对象上, 模式为独占模式
// 进入 AQS 队列阻塞
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryAcquire(int acquires) {
// 获得低 16 位, 代表写锁的 state 计数
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (
// c != 0 and w == 0 表示有读锁, 或者
w == 0 ||
// 如果 exclusiveOwnerThread 不是自己
current != getExclusiveOwnerThread()) {
// 获得锁失败
return false;
}
// 写锁计数超过低 16 位, 报异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 写锁重入, 获得锁成功
setState(c + acquires);
return true;
}
if (
// 判断写锁是否该阻塞, 或者
writerShouldBlock() ||
// 尝试更改计数失败
!compareAndSetState(c, c + acquires)) {
// 获得锁失败
return false;
}
// 获得锁成功
setExclusiveOwnerThread(current);
return true;
}
// 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞
final boolean writerShouldBlock() {
return false;
}
}
写锁释放流程
static final class NonfairSync extends Sync {
// ... 省略无关代码
// WriteLock 方法, 方便阅读, 放在此处
public void unlock() {
sync.release(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean release(int arg) {
// 尝试释放写锁成功
if (tryRelease(arg)) {
// unpark AQS 中等待的线程
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 因为可重入的原因, 写锁计数为 0, 才算释放成功
boolean free = exclusiveCount(nextc) == 0;
if (free) {
setExclusiveOwnerThread(null);
}
setState(nextc);
return free;
}
}
读锁上锁流程
static final class NonfairSync extends Sync {
// ReadLock 方法, 方便阅读, 放在此处
public void lock() {
sync.acquireShared(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquireShared(int arg) {
// tryAcquireShared 返回负数, 表示获取读锁失败
if (tryAcquireShared(arg) < 0) {
doAcquireShared(arg);
}
}
// Sync 继承过来的方法, 方便阅读, 放在此处
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)) {
// ... 省略不重要的代码
return 1;
}
return fullTryAcquireShared(current);
}
// 非公平锁 readerShouldBlock 看 AQS 队列中第一个节点是否是写锁
// true 则该阻塞, false 则不阻塞
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
// AQS 继承过来的方法, 方便阅读, 放在此处
// 与 tryAcquireShared 功能类似, 但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (; ; ) {
int c = getState();
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
}
else if (readerShouldBlock()) {
// ... 省略不重要的代码
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) {
// ... 省略不重要的代码
return 1;
}
}
}
// AQS 继承过来的方法, 方便阅读, 放在此处
private void doAcquireShared(int arg) {
// 将当前线程关联到一个 Node 对象上, 模式为共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (; ; ) {
final Node p = node.predecessor();
if (p == head) {
// 再一次尝试获取读锁
int r = tryAcquireShared(arg);
// 成功
if (r >= 0) {
// ㈠
// r 表示可用资源数, 在这里总是 1 允许传播
//(唤醒 AQS 中下一个 Share 节点)
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (
// 是否在获取读锁失败时阻塞(前一个阶段 waitStatus == Node.SIGNAL)
shouldParkAfterFailedAcquire(p, node) &&
// park 当前线程
parkAndCheckInterrupt()) {
interrupted = true;
}
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// ㈠ AQS 继承过来的方法, 方便阅读, 放在此处
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置自己为 head
setHead(node);
// propagate 表示有共享资源(例如共享读锁或信号量)
// 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
// 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点
if (s == null || s.isShared()) {
// 进入 ㈡
doReleaseShared();
}
}
}
// ㈡ AQS 继承过来的方法, 方便阅读, 放在此处
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE, 为了解决 bug, 见后面分析
for (; ; ) {
Node h = head;
// 队列还有节点
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 下一个节点 unpark 如果成功获取读锁
// 并且下下个节点还是 shared, 继续 doReleaseShared
unparkSuccessor(h);
}
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
}
读锁释放流程
static final class NonfairSync extends Sync {
// ReadLock 方法, 方便阅读, 放在此处
public void unlock() {
sync.releaseShared(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryReleaseShared(int unused) {
// ... 省略不重要的代码
for (; ; ) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc)) {
// 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程
// 计数为 0 才是真正释放
return nextc == 0;
}
}
}
// AQS 继承过来的方法, 方便阅读, 放在此处
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
for (; ; ) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果有其它线程也在释放读锁,那么需要将 waitStatus 先改为 0
// 防止 unparkSuccessor 被多次执行
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 如果已经是 0 了,改为 -3,用来解决传播性,见后文信号量 bug 分析
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
}
StampedLock
该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用
加解读锁
long stamp = lock.readLock();
lock.unlockRead(stamp);
加解写锁
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
乐观读,StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
// 锁升级
}
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
class DataContainerStamped {
private int data;
private final StampedLock lock = new StampedLock();
public DataContainerStamped(int data) {
this.data = data;
}
public int read(int readTime) {
long stamp = lock.tryOptimisticRead();
log.debug("optimistic read locking...{}", stamp);
sleep(readTime);
if (lock.validate(stamp)) {
log.debug("read finish...{}, data:{}", stamp, data);
return data;
}
// 锁升级 - 读锁
log.debug("updating to read lock... {}", stamp);
try {
stamp = lock.readLock();
log.debug("read lock {}", stamp);
sleep(readTime);
log.debug("read finish...{}, data:{}", stamp, data);
return data;
} finally {
log.debug("read unlock {}", stamp);
lock.unlockRead(stamp);
}
}
public void write(int newData) {
long stamp = lock.writeLock();
log.debug("write lock {}", stamp);
try {
sleep(2);
this.data = newData;
} finally {
log.debug("write unlock {}", stamp);
lock.unlockWrite(stamp);
}
}
}
测试 读-读
可以优化
public static void main(String[] args) {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1);
}, "t1").start();
sleep(0.5);
new Thread(() -> {
dataContainer.read(0);
}, "t2").start();
}
输出结果,可以看到实际没有加读锁
15:58:50.217 c.DataContainerStamped [t1] - optimistic read locking...256
15:58:50.717 c.DataContainerStamped [t2] - optimistic read locking...256
15:58:50.717 c.DataContainerStamped [t2] - read finish...256, data:1
15:58:51.220 c.DataContainerStamped [t1] - read finish...256, data:1
测试 读-写
时优化读补加读锁
public static void main(String[] args) {
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {
dataContainer.read(1);
}, "t1").start();
sleep(0.5);
new Thread(() -> {
dataContainer.write(100);
}, "t2").start();
}
输出结果
15:57:00.219 c.DataContainerStamped [t1] - optimistic read locking...256
15:57:00.717 c.DataContainerStamped [t2] - write lock 384
15:57:01.225 c.DataContainerStamped [t1] - updating to read lock... 256
15:57:02.719 c.DataContainerStamped [t2] - write unlock 384
15:57:02.719 c.DataContainerStamped [t1] - read lock 513
15:57:03.719 c.DataContainerStamped [t1] - read finish...513, data:1000
15:57:03.719 c.DataContainerStamped [t1] - read unlock 513
注意
- StampedLock 不支持条件变量
- StampedLock 不支持可重入
Semaphore
基本使用
[ˈsɛməˌfɔr]
信号量,用来限制能同时访问共享资源的线程上限。
生动形象:
Semaphore 就是用水瓢装水到桶里拿着桶一瓢一瓢的浇水的过程,一瓢一瓢水的往桶里装水,满了拿着桶去浇水,也是一瓢一瓢的浇水
Semaphore 也是一台公共汽车(规定好限载量49),当不满49人,乘客可以随时上下车,当满了49人,只能下车了
@Slf4j(topic = "c.TestSemaphoreDemo")
public class TestSemaphoreDemo {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
log.debug("running...");
sleep(1000);
log.debug("end...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release();
}
}).start();
}
}
}
Semaphore 实现
- 使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可,当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现)
- 用 Semaphore 实现简单连接池,对比『享元模式』下的实现(用wait notify),性能和可读性显然更好,注意下面的实现中线程数和数据库连接数是相等的
@Slf4j(topic = "c.TestSemaphoreThreadPoolDemo")
public class TestSemaphoreConnectPoolDemo {
public static void main(String[] args) {
Pool pool = new Pool(10);
CopyOnWriteArrayList<Connection> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
ExecutorService threadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
for (int j = 0; j < 25; j++) {
Connection connection = pool.borrow();
copyOnWriteArrayList.add(connection);
}
});
}
while (true) {
for (int i = 0; i < copyOnWriteArrayList.size(); i++) {
Connection connection = copyOnWriteArrayList.get(i);
pool.free(connection);
copyOnWriteArrayList.remove(connection);
}
sleep(100);
}
}
@Slf4j(topic = "c.Pool")
static class Pool {
private final int poolSize;
private final Connection[] connections;
private final AtomicIntegerArray states;
private final Semaphore semaphore;
public Pool(int poolSize) {
this.poolSize = poolSize;
connections = new Connection[poolSize];
for (int i = 0; i < connections.length; i++) {
connections[i] = new MockConnection("连接" + (i + 1));
}
states = new AtomicIntegerArray(poolSize);
semaphore = new Semaphore(poolSize);
}
/**
* 借出
*
* @return
*/
public Connection borrow() {
try {
semaphore.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
return null;
}
for (int i = 0; i < poolSize; i++) {
if (states.get(i) == 0) {
if (states.compareAndSet(i, 0, 1)) {
log.debug("borrow {}", connections[i]);
return connections[i];
}
}
}
return null;
}
/**
* 释放连接
*
* @param connection
*/
public void free(Connection connection) {
for (int i = 0; i < poolSize; i++) {
if (connections[i].equals(connection)) {
states.compareAndSet(i, 1, 0);
log.debug("free {}", connection);
semaphore.release();
break;
}
}
}
}
}
应用场景
数据库的链接是有限的, 比如只能有 100 个链接, 线程 200 个, 在抢这 100 个链接, 此时可以使用 Semaphore
来做限流
@Slf4j(topic = "c.SemaphoreDemo")
public class SemaphoreDemo {
private static final int THREAD_COUNT = 30;
private static final ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);
private static final Semaphore semaphore = new Semaphore(10);
public SemaphoreDemo() {
}
public static void main(String[] args) {
LongAdder adder = new LongAdder();
for (int i = 0; i < THREAD_COUNT; i++) {
executorService.execute(() -> {
try {
semaphore.acquire();
adder.increment();
log.debug("adder = {}", adder.sum());
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
ExChanger 的使用
只针对两个线程之间的数据交换, 如果超出2个, 这爆出错误
@Slf4j(topic = "c.ExChangerDemo")
public class ExChangerDemo {
public static void main(String[] args) {
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
try {
log.info("{} 有10块钱, 准备买巧克力", Thread.currentThread().getName());
log.info("{} 买到了{}, 花了10块钱", Thread.currentThread().getName(), exchanger.exchange("10块钱"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "小明").start();
new Thread(() -> {
try {
log.info("{} 有巧克力", Thread.currentThread().getName());
Thread.sleep(100);
log.info("{}: 等下, 我给你拿...", Thread.currentThread().getName());
Thread.sleep(1000);
log.info("{} 巧克力交换获得了{}", Thread.currentThread().getName(), exchanger.exchange("巧克力"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "店铺").start();
}
}
单位时间内限流
guava 实现
```java
@RestController
public class TestController {
private final RateLimiter limiter = RateLimiter.create(50);
@GetMapping("/test")
public String test() {
// limiter.acquire();
return "ok";
}
}
Semaphore 原理
1. 加锁解锁流程
Semaphore 有点像一个停车场,permits 就好像停车位数量,当线程获得了 permits 就像是获得了停车位,然后停车场显示空余车位减一
刚开始,permits(state)为 3,这时 5 个线程来获取资源
假设其中 Thread-1,Thread-2,Thread-4 cas 竞争成功,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列 park 阻塞
这时 Thread-4 释放了 permits,状态如下
接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态
源码分析
构造方法
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
// permits 即 state
super(permits);
}
// Semaphore 方法, 方便阅读, 放在此处
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// 尝试获得共享锁
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
// Sync 继承过来的方法, 方便阅读, 放在此处
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (
// 如果许可已经用完, 返回负数, 表示获取失败, 进入 doAcquireSharedInterruptibly
remaining < 0 ||
// 如果 cas 重试成功, 返回正数, 表示获取成功
compareAndSetState(available, remaining)
) {
return remaining;
}
}
}
// AQS 继承过来的方法, 方便阅读, 放在此处
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 再次尝试获取许可
int r = tryAcquireShared(arg);
if (r >= 0) {
// 成功后本线程出队(AQS), 所在 Node设置为 head
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
// r 表示可用资源数, 为 0 则不会继续传播
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 不成功, 设置上一个节点 waitStatus = Node.SIGNAL, 下轮进入 park 阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// Semaphore 方法, 方便阅读, 放在此处
public void release() {
sync.releaseShared(1);
}
// AQS 继承过来的方法, 方便阅读, 放在此处
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// Sync 继承过来的方法, 方便阅读, 放在此处
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
}
为什么要有 PROPAGATE
早期有 bug
- releaseShared 方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
- doAcquireShared 方法
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
// 这里会有空档
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- setHeadAndPropagate 方法
private void setHeadAndPropagate(Node node, int propagate) {
setHead(node);
// 有空闲资源
if (propagate > 0 && node.waitStatus != 0) {
Node s = node.next;
// 下一个
if (s == null || s.isShared())
unparkSuccessor(node);
}
}
- 假设存在某次循环中队列里排队的结点情况为 head(-1)->t1(-1)->t2(-1)
- 假设存在将要信号量释放的 T3 和 T4,释放顺序为先 T3 后 T4
正常流程
产生 bug 的情况
修复前版本执行流程
- T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head 的等待状态从 -1 变为 0
- T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,假设返回值为 0(获取锁成功,但没有剩余资源量)
- T4 调用 releaseShared(1),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个head),不满足条件,因此不调用 unparkSuccessor(head)
- T1 获取信号量成功,调用 setHeadAndPropagate 时,因为不满足 propagate > 0(2 的返回值也就是 propagate(剩余资源量) == 0),从而不会唤醒后继结点, T2 线程得不到唤醒
bug 修复后
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置自己为 head
setHead(node);
// propagate 表示有共享资源(例如共享读锁或信号量)
// 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
// 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点
if (s == null || s.isShared()) {
doReleaseShared();
}
}
}
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
- T3 调用 releaseShared(),直接调用了 unparkSuccessor(head),head 的等待状态从 -1 变为 0
- T1 由于 T3 释放信号量被唤醒,调用 tryAcquireShared,假设返回值为 0(获取锁成功,但没有剩余资源量)
- T4 调用 releaseShared(),此时 head.waitStatus 为 0(此时读到的 head 和 1 中为同一个 head),调用 doReleaseShared() 将等待状态置为 PROPAGATE(-3)
- T1 获取信号量成功,调用 setHeadAndPropagate 时,读到 h.waitStatus < 0,从而调用 doReleaseShared() 唤醒 T2
CountdownLatch
用来进行线程同步协作,等待所有线程完成倒计时
其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一
生动形象:
CountdownLatch 就是搞前10名奖励大型家电的活动,只有前十名的客人才能获得奖励,超过10个人后,这个活动奖品没了,活动举办完毕,一直等待的快递员把快递送到客人家里
@Slf4j(topic = "c.TestCountDownLatchDemo")
public class TestCountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
log.debug("线程:{}", Thread.currentThread().getName());
latch.countDown();
}, "线程" + i).start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("主线程等待完毕。。。");
}
}
CyclicBarrier
[ˈsaɪklɪk ˈbæriɚ]
循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置『计数个数』,每个线程执行到某个需要“同步”的时刻调用 await() 方法进行等待,当等待的线程数满足『计数个数』时,继续执行
生动形象:
CyclicBarrier 是客运车,限载量49人,乘客上车直到满人了,才发车,到目的地后乘客下车,重新等待乘客上车
@Slf4j(topic = "c.TestCyclicBarrierDemo")
public class TestCyclicBarrierDemo {
public static void main(String[] args) {
CyclicBarrier cyclicBarrier = new CyclicBarrier(3);
log.debug("客车准备发车。。。");
for (int i = 0; i < 4; i++) {
new Thread(() -> {
log.debug("{}上车。。。", Thread.currentThread().getName());
try {
cyclicBarrier.await();
log.debug("客车满员了。。。{}叫:发车。。。", Thread.currentThread().getName());
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
Sleeper.sleep(1000);
log.debug("{}到站,下车", Thread.currentThread().getName());
}, "A乘客" + i).start();
}
Sleeper.sleep(2000);
log.debug("客车准备发车。。。");
/**
* 可重入的证明
*/
for (int i = 0; i < 2; i++) {
new Thread(() -> {
log.debug("{}上车等待中。。。", Thread.currentThread().getName());
try {
cyclicBarrier.await();
log.debug("车辆满了,{}叫:发车。。。", Thread.currentThread().getName());
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
log.debug("{}到站,下车", Thread.currentThread().getName());
Sleeper.sleep(1000);
}, "B乘客" + i).start();
}
}
}
线程安全集合类概述
线程安全集合类可以分为三大类:
- 遗留的线程安全集合如
Hashtable
,Vector
- 使用
Collections
装饰的线程安全集合,如:Collections.synchronizedCollection
Collections.synchronizedList
Collections.synchronizedMap
Collections.synchronizedSet
Collections.synchronizedNavigableMap
Collections.synchronizedNavigableSet
Collections.synchronizedSortedMap
Collections.synchronizedSortedSet
java.util.concurrent.*
重点介绍java.util.concurrent.*
下的线程安全集合类,可以发现它们有规律,里面包含三类关键词:
Blocking、CopyOnWrite、Concurrent- Blocking 大部分实现基于锁,并提供用来阻塞的方法
- CopyOnWrite 之类容器修改开销相对较重
- Concurrent 类型的容器
- 内部很多操作使用 cas 优化,一般可以提供较高吞吐量
- 弱一致性
- 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍
历,这时内容是旧的 - 求大小弱一致性,size 操作未必是 100% 准确
- 读取弱一致性
- 遍历时弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍
遍历时如果发生了修改,对于非安全容器来讲,使用 fail-fast 机制也就是让遍历立刻失败,抛出
ConcurrentModificationException
,不再继续遍历
ConcurrentHashMap
练习:单词计数
@Slf4j(topic = "c.TestWordCountDemo")
public class TestWordCountDemo {
private static final String ALPHA = "abcdefghijklmnopqrstuvwxyz";
@Test
public void testReadFiles() throws Exception {
Map<String, LongAdder> map = demo(ConcurrentHashMap::new, (stringMap, words) -> words.forEach(word -> {
LongAdder longAdder = stringMap.computeIfAbsent(word, s -> new LongAdder());
longAdder.increment();
}));
System.out.println(map);
/**
* 这种方式存在线程安全问题
*/
// Map<String, Integer> map = demo(HashMap::new, (stringTMap, words) -> {
// words.forEach(word -> {
// Integer counter = stringTMap.get(word);
// int newValue = counter == null ? 1 : counter + 1;
// stringTMap.put(word, newValue);
// });
// });
// System.out.println(map);
}
/**
* 统计每个单词出现的数量
*
* @param supplier
* 提供Map
* @param biConsumer
* 提供 Map 和 List 把 List 的值累加到 Map 中
* @param <T>
*/
public <T> Map<String, T> demo(Supplier<Map<String, T>> supplier, BiConsumer<Map<String, T>, List<String>> biConsumer) {
Map<String, T> counterMap = supplier.get();
List<Thread> threadList = new ArrayList<>();
for (int i = 0; i < 26; i++) {
int finalI = i;
Thread thread = new Thread(() -> {
List<String> wordList = readFromFile(finalI);
biConsumer.accept(counterMap, wordList);
});
threadList.add(thread);
}
threadList.forEach(Thread::start);
for (Thread thread : threadList) {
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return counterMap;
}
/**
* 写入文件
*
* @throws Exception
*/
@Test
public void testWriteFiles() throws Exception {
int length = ALPHA.length();
int count = 200;
List<String> stringList = new ArrayList<>();
for (int i = 0; i < length; i++) {
char ch = ALPHA.charAt(i);
for (int j = 0; j < count; j++) {
stringList.add(String.valueOf(ch));
}
}
Collections.shuffle(stringList);
for (int i = 0; i < 26; i++) {
PrintWriter printWriter = new PrintWriter(new OutputStreamWriter(new FileOutputStream("tmp/" + (i + 1) + ".txt")));
try (printWriter) {
String collect = String.join("\n", stringList.subList(i * count, (i + 1) * count));
printWriter.print(collect);
} catch (Exception e) {
}
}
}
/**
* 读取文件
*
* @param i
*
* @return
*/
public List<String> readFromFile(int i) {
List<String> wordList = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new FileReader("tmp/" + (i + 1) + ".txt"))) {
String line = null;
while ((line = reader.readLine()) != null) {
if (StringUtils.isEmptyOrWhitespaceOnly(line)) {
continue;
}
wordList.add(line);
}
} catch (Exception e) {
e.printStackTrace();
}
return wordList;
}
}
JDK 7 HashMap 并发死链
死链复现
注意
- 要在 JDK 7 下运行,否则扩容机制和 hash 的计算方法都变了
- 以下测试代码是精心准备的,不要随便改动
public static void main(String[] args) {
// 测试 java 7 中哪些数字的 hash 结果相等
System.out.println("长度为16时,桶下标为1的key");
for (int i = 0; i < 64; i++) {
if (hash(i) % 16 == 1) {
System.out.println(i);
}
}
System.out.println("长度为32时,桶下标为1的key");
for (int i = 0; i < 64; i++) {
if (hash(i) % 32 == 1) {
System.out.println(i);
}
}
// 1, 35, 16, 50 当大小为16时,它们在一个桶内
final HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();
// 放 12 个元素
map.put(2, null);
map.put(3, null);
map.put(4, null);
map.put(5, null);
map.put(6, null);
map.put(7, null);
map.put(8, null);
map.put(9, null);
map.put(10, null);
map.put(16, null);
map.put(35, null);
map.put(1, null);
System.out.println("扩容前大小[main]:" + map.size());
new Thread() {
@Override
public void run() {
// 放第 13 个元素, 发生扩容
map.put(50, null);
System.out.println("扩容后大小[Thread-0]:" + map.size());
}
}.start();
new Thread() {
@Override
public void run() {
// 放第 13 个元素, 发生扩容
map.put(50, null);
System.out.println("扩容后大小[Thread-1]:" + map.size());
}
}.start();
}
final static int hash(Object k) {
int h = 0;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
调试工具使用 idea
在 HashMap 源码 590 行加断点
int newCapacity = newTable.length;
断点的条件如下,目的是让 HashMap 在扩容为 32 时,并且线程为 Thread-0 或 Thread-1 时停下来
newTable.length==32 &&
(
Thread.currentThread().getName().equals("Thread-0")||
Thread.currentThread().getName().equals("Thread-1")
)
断点暂停方式选择 Thread,否则在调试 Thread-0 时,Thread-1 无法恢复运行
运行代码,程序在预料的断点位置停了下来,输出
长度为16时,桶下标为1的key
1
16
35
50
长度为32时,桶下标为1的key
1
35
扩容前大小[main]:12
接下来进入扩容流程调试
在 HashMap 源码 594 行加断点
Entry<K,V> next = e.next; // 593
if (rehash) // 594
// ...
这是为了观察 e 节点和 next 节点的状态,Thread-0 单步执行到 594 行,再 594 处再添加一个断点(条件 Thread.currentThread().getName().equals("Thread-0"))
这时可以在 Variables 面板观察到 e 和 next 变量,使用 view as -> Object
查看节点状态
e (1)->(35)->(16)->null
next (35)->(16)->null
在 Threads 面板选中 Thread-1 恢复运行,可以看到控制台输出新的内容如下,Thread-1 扩容已完成
newTable[1] (35)->(1)->null
扩容后大小:13
这时 Thread-0 还停在 594 处, Variables 面板变量的状态已经变化为
e (1)->null
next (35)->(1)->null
为什么呢,因为 Thread-1 扩容时链表也是后加入的元素放入链表头,因此链表就倒过来了,但 Thread-1 虽然结
果正确,但它结束后 Thread-0 还要继续运行
接下来就可以单步调试(F8)观察死链的产生了
下一轮循环到 594,将 e 搬迁到 newTable 链表头
newTable[1] (1)->null
e (35)->(1)->null
next (1)->null
下一轮循环到 594,将 e 搬迁到 newTable 链表头
newTable[1] (35)->(1)->null
e (1)->null
next null
再看看源码
e.next = newTable[1];
// 这时 e (1,35)
// 而 newTable[1] (35,1)->(1,35) 因为是同一个对象
newTable[1] = e;
// 再尝试将 e 作为链表头, 死链已成
e = next;
// 虽然 next 是 null, 会进入下一个链表的复制, 但死链已经形成了
源码分析
HashMap 的并发死链发生在扩容时
// 将 table 迁移至 newTable
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
// 1 处
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity);
// 2 处
// 将新元素加入 newTable[i], 原 newTable[i] 作为新元素的 next
e.next = newTable[i];
newTable[i] = e;
e = next;
}
}
}
假设 map 中初始元素是
原始链表,格式:[下标] (key,next)
[1] (1,35)->(35,16)->(16,null)
线程 a 执行到 1 处 ,此时局部变量 e 为 (1,35),而局部变量 next 为 (35,16) 线程 a 挂起
线程 b 开始执行
第一次循环
[1] (1,null)
第二次循环
[1] (35,1)->(1,null)
第三次循环
[1] (35,1)->(1,null)
[17] (16,null)
切换回线程 a,此时局部变量 e 和 next 被恢复,引用没变但内容变了:e 的内容被改为 (1,null),而 next 的内
容被改为 (35,1) 并链向 (1,null)
第一次循环
[1] (1,null)
第二次循环,注意这时 e 是 (35,1) 并链向 (1,null) 所以 next 又是 (1,null)
[1] (35,1)->(1,null)
第三次循环,e 是 (1,null),而 next 是 null,但 e 被放入链表头,这样 e.next 变成了 35 (2 处)
[1] (1,35)->(35,1)->(1,35)
已经是死链了
小结
- 究其原因,是因为在多线程环境下使用了非线程安全的 map 集合
- JDK 8 虽然将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序),但仍不意味着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)
ConcurrentHashMap源码分析
jdk8 ConcurrentHashMap 源码解析
重要属性和内部类
// sizeCtl:默认为0,用来控制table的初始化和扩容操作.它的数值有以下含义
// -1: 代表table正在初始化,其他线程应该交出CPU时间片,退出
// -N: 表示正有N-1个线程执行扩容操作
// >0: 如果table已经初始化,代表table容量,默认为table大小的0.75,如果还未初始化,代表需要初始化的大小
private transient volatile int sizeCtl;
// 整个 ConcurrentHashMap 就是一个 Node[]
static class Node<K,V> implements Map.Entry<K,V> {}
// hash 表
transient volatile Node<K,V>[] table;
// 扩容时的 新 hash 表
private transient volatile Node<K,V>[] nextTable;
// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
static final class ForwardingNode<K,V> extends Node<K,V> {}
// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
static final class ReservationNode<K,V> extends Node<K,V> {}
// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}
// 作为 treebin 的节点, 存储 parent, left, right
static final class TreeNode<K,V> extends Node<K,V> {}
重要方法
// 获取 Node[] 中第 i 个 Node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i)
// cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v)
// 直接修改 Node[] 中第 i 个 Node 的值, v 为新值
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v)
构造器分析
可以看到实现了懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
// tableSizeFor 仍然是保证计算的大小是 2^n, 即 16,32,64 ...
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
get 流程
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// spread 方法能确保返回结果是正数, 并且借助hashCode计算新的 h 码为了尽量确保节点散列分布
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果头结点已经是要查找的 key
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// hash 为负数表示该 bin 在扩容中或是 treebin, 这时调用 find 方法来查找
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 正常遍历链表, 用 equals 比较
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
put 流程
以下数组简称(table),链表简称(bin)
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null)
throw new NullPointerException();
// 其中 spread 方法会综合高位低位, 具有更好的 hash 性
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K, V>[] tab = table; ; ) {
// f 是链表头节点
// fh 是链表头结点的 hash
// i 是链表在 table 中的下标
Node<K, V> f;
int n, i, fh;
// (1)要初始化 table
if (tab == null || (n = tab.length) == 0)
// 初始化 table 使用了 cas, 无需 synchronized 创建成功, 进入下一轮循环
tab = initTable();
// 如果hash对应的位置为空,则使用cas创建节点
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 添加链表头使用了 cas, 无需 synchronized
if (casTabAt(tab, i, null, new Node<K, V>(hash, key, value, null)))
break;
}
// (2)帮忙扩容, sizeCtl为负数,表示正在扩容table
else if ((fh = f.hash) == MOVED)
// 帮忙之后, 进入下一轮循环
tab = helpTransfer(tab, f);
else {
V oldVal = null;
// 锁住链表头节点
synchronized (f) {
// 再次确认链表头节点没有被移动
if (tabAt(tab, i) == f) {
// 链表
if (fh >= 0) {
binCount = 1;
// 遍历链表
for (Node<K, V> e = f; ; ++binCount) {
K ek;
// 找到相同的 key
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
oldVal = e.val;
// 更新
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K, V> pred = e;
// 已经是最后的节点了, 新增 Node, 追加至链表尾
if ((e = e.next) == null) {
pred.next = new Node<K, V>(hash, key, value, null);
break;
}
}
}
// 红黑树
else if (f instanceof TreeBin) {
Node<K, V> p;
binCount = 2;
// putTreeVal 会看 key 是否已经在树中, 是, 则返回对应的 TreeNode
if ((p = ((TreeBin<K, V>) f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
// 释放链表头节点的锁
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
// 如果链表长度 >= 树化阈值(8), 进行链表转为红黑树
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 增加 size 计数
addCount(1L, binCount);
return null;
}
// (1)要初始化 table
private final Node<K, V>[] initTable() {
Node<K, V>[] tab;
int sc;
while ((tab = table) == null || tab.length == 0) {
// sizeCtl 小于 0 ,表示正在创建初始化 table
if ((sc = sizeCtl) < 0)
Thread.yield();
// 尝试将 sizeCtl 设置为 -1(表示初始化 table)
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 获得锁, 创建 table, 这时其它线程会在 while() 循环中 yield 直至 table 创建
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
Node<K, V>[] nt = (Node<K, V>[]) new Node<?, ?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
// 帮忙扩容
// (1) tab = table (2) f = tabAt(tab, i = (n - 1) & hash)
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
Node<K,V>[] nextTab; int sc;
// 判断是否为 ForwardingNode , 是则获取下一个节点
if (tab != null && (f instanceof ForwardingNode) &&
(nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
int rs = resizeStamp(tab.length);
while (nextTab == nextTable && table == tab &&
(sc = sizeCtl) < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || transferIndex <= 0)
break;
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1)) {
transfer(tab, nextTab);
break;
}
}
return nextTab;
}
return table;
}
// check 是之前 binCount 的个数
private final void addCount(long x, int check) {
CounterCell[] as;
long b, s;
if (
// 已经有了 counterCells, 向 cell 累加
(as = counterCells) != null ||
// 还没有, 向 baseCount 累加
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a;
long v;
int m;
boolean uncontended = true;
if (
// 还没有 counterCells
as == null || (m = as.length - 1) < 0 ||
// 还没有 cell
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
// cell cas 增加计数失败
!(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 创建累加单元数组和cell, 累加重试
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
// 获取元素个数
s = sumCount();
}
if (check >= 0) {
Node<K, V>[] tab, nt;
int n, sc;
while (s >= (long) (sc = sizeCtl) && (tab = table) != null && (n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 || sc == rs + MAX_RESIZERS || (nt = nextTable) == null || transferIndex <= 0)
break;
// newtable 已经创建了,帮忙扩容
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
// 需要扩容,这时 newtable 未创建
else if (U.compareAndSwapInt(this, SIZECTL, sc, (rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
size 计算流程
size 计算实际发生在 put,remove 改变集合元素的操作之中
- 没有竞争发生,向 baseCount 累加计数
- 有竞争发生,新建 counterCells,向其中的一个 cell 累加计数
- counterCells 初始有两个 cell
- 如果计数竞争比较激烈,会创建新的 cell 来累加计数
public int size() {
long n = sumCount();
return ((n < 0L) ? 0 :
(n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
(int)n);
}
final long sumCount() {
CounterCell[] as = counterCells; CounterCell a;
// 将 baseCount 计数与所有 cell 计数累加
long sum = baseCount;
if (as != null) {
for (int i = 0; i < as.length; ++i) {
if ((a = as[i]) != null)
sum += a.value;
}
}
return sum;
}
Java 8 数组(Node) +( 链表 Node | 红黑树 TreeNode ) 以下数组简称(table),链表简称(bin)
- 初始化,使用 cas 来保证并发安全,懒惰初始化 table
- 树化,当 table.length < 64 时,先尝试扩容,超过 64 时,并且 bin.length > 8 时,会将链表树化,树化过程会用 synchronized 锁住链表头
- put,如果该 bin 尚未创建,只需要使用 cas 创建 bin;如果已经有了,锁住链表头进行后续 put 操作,元素添加至 bin 的尾部
- get,无锁操作仅需要保证可见性,扩容过程中 get 操作拿到的是 ForwardingNode 它会让 get 操作在新 table 进行搜索
- 扩容,扩容时以 bin 为单位进行,需要对 bin 进行 synchronized,但这时妙的是其它竞争线程也不是无事可做,它们会帮助把其它 bin 进行扩容,扩容时平均只有 1/6 的节点会把复制到新 table 中
- size,元素个数保存在 baseCount 中,并发时的个数变动保存在 CounterCell[] 当中。最后统计数量时累加即可
注意:
- forwardingNode 节点用于标记是否转移完毕,如果节点存在ForwardingNode则表示转移完毕请到另一个table去查找
JDK 7 ConcurrentHashMap
它维护了一个 segment 数组,每个 segment 对应一把锁
- 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 jdk8 中是类似的
- 缺点:Segments 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化
构造器分析
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// ssize 必须是 2^n, 即 2, 4, 8, 16 ... 表示了 segments 数组的大小
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
// segmentShift 默认是 32 - 4 = 28
this.segmentShift = 32 - sshift;
// segmentMask 默认是 15 即 0000 0000 0000 1111
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// 创建 segments and segments[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
构造完成,如下图所示
可以看到 ConcurrentHashMap 没有实现懒惰初始化,空间占用不友好
其中 this.segmentShift 和 this.segmentMask 的作用是决定将 key 的 hash 结果匹配到哪个 segment
例如,根据某一 hash 值求 segment 位置,先将高位向低位移动 this.segmentShift 位
put 流程
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
// 计算出 segment 下标
int j = (hash >>> segmentShift) & segmentMask;
// 获得 segment 对象, 判断是否为 null, 是则创建该 segment
if ((s = (Segment<K,V>)UNSAFE.getObject
(segments, (j << SSHIFT) + SBASE)) == null) {
// 这时不能确定是否真的为 null, 因为其它线程也发现该 segment 为 null,
// 因此在 ensureSegment 里用 cas 方式保证该 segment 安全性
s = ensureSegment(j);
}
// 进入 segment 的put 流程
return s.put(key, hash, value, false);
}
segment 继承了可重入锁(ReentrantLock),它的 put 方法为
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
// 尝试加锁
HashEntry<K,V> node = tryLock() ? null :
// 如果不成功, 进入 scanAndLockForPut 流程
// 如果是多核 cpu 最多 tryLock 64 次, 进入 lock 流程
// 在尝试期间, 还可以顺便看该节点在链表中有没有, 如果没有顺便创建出来
scanAndLockForPut(key, hash, value);
// 执行到这里 segment 已经被成功加锁, 可以安全执行
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
// 更新
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
// 新增
// 1) 之前等待锁时, node 已经被创建, next 指向链表头
if (node != null)
node.setNext(first);
else
// 2) 创建新 node
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
// 3) 扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
// 将 node 作为链表头
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
rehash 流程
发生在 put 中,因为此时已经获得了锁,因此 rehash 时不需要考虑线程安全
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
int idx = e.hash & sizeMask;
if (next == null) // Single node on list
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
// 过一遍链表, 尽可能把 rehash 后 idx 不变的节点重用
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
newTable[lastIdx] = lastRun;
// 剩余节点需要新建
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
// 扩容完成, 才加入新的节点
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
// 替换为新的 HashEntry table
table = newTable;
}
附,调试代码
public static void main(String[] args) {
ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>();
for (int i = 0; i < 1000; i++) {
int hash = hash(i);
int segmentIndex = (hash >>> 28) & 15;
if (segmentIndex == 4 && hash % 8 == 2) {
System.out.println(i + "\t" + segmentIndex + "\t" + hash % 2 + "\t" + hash % 4 +
"\t" + hash % 8);
}
}
map.put(1, "value");
map.put(15, "value"); // 2 扩容为 4 15 的 hash%8 与其他不同
map.put(169, "value");
map.put(197, "value"); // 4 扩容为 8
map.put(341, "value");
map.put(484, "value");
map.put(545, "value"); // 8 扩容为 16
map.put(912, "value");
map.put(941, "value");
System.out.println("ok");
}
private static int hash(Object k) {
int h = 0;
if ((0 != h) && (k instanceof String)) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// Spread bits to regularize both segment and index locations,
// using variant of single-word Wang/Jenkins hash.
h += (h << 15) ^ 0xffffcd7d;
h ^= (h >>> 10);
h += (h << 3);
h ^= (h >>> 6);
h += (h << 2) + (h << 14);
int v = h ^ (h >>> 16);
return v;
}
get 流程
get 时并未加锁,用了 UNSAFE 方法保证了可见性,扩容过程中,get 先发生就从旧表取内容,get 后发生就从新表取内容
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
// u 为 segment 对象在数组中的偏移量
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
// s 即为 segment
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
size 计算流程
- 计算元素个数前,先不加锁计算两次,如果前后两次结果如一样,认为个数正确返回
- 如果不一样,进行重试,重试次数超过 3,将所有 segment 锁住,重新计算个数返回
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
if (retries++ == RETRIES_BEFORE_LOCK) {
// 超过重试次数, 需要创建所有 segment 并加锁
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
问题一:
java.util.concurrent.ConcurrentHashMap#computeIfAbsent
看下面源码引发的几个问题:
对临时变量加对象锁?
synchronized对象后头的第一个if方法不是cas操作么?为什么还要加上 synchronized
此处如果不加上 synchronized 的话,哪个共享变量会出问题?(话说它会发生问题么?每个线程进来全是单独的 r 对象)
答: 对临时变量加锁确实有这种应用场景,但要根据上下文判断,
英文:ReservationNode 翻译为预留节点
主要功能是存访预留节点,然后对预留节点上锁,此时通过cas设置预留节点进tab数组的 i 位置,这样,其他线程在设置 tab 的 i 位置时,会发现预留节点已经上锁,别的线程就无法修改这个位置,等到我们的 computeIfAbsent 执行完 setTabAt 方法将预留节点位置替换成新节点后,此时预留节点还是不是上锁状态已经不重要了
LinkedBlockingQueue 原理
1. 基本的入队出队
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
static class Node<E> {
E item;
/**
* 下列三种情况之一
* - 真正的后继节点
* - 自己, 发生在出队时
* - null, 表示是没有后继节点, 是最后了
*/
Node<E> next;
Node(E x) { item = x; }
}
}
初始化链表 last = head = new Node<E>(null);
Dummy 节点用来占位,item 为 null
当一个节点入队 last = last.next = node;
再来一个节点入队 last = last.next = node;
出队
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
h = head
first = h.next
h.next = h
head = first
E x = first.item;
first.item = null;
return x;
2. 源码分析
高明之处在于用了两把锁和 dummy 节点
- 用一把锁,同一时刻,最多只允许有一个线程(生产者或消费者,二选一)执行
- 用两把锁,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- 消费者与消费者线程仍然串行
- 生产者与生产者线程仍然串行
线程安全分析
- 当节点总数大于 2 时(包括 dummy 节点),putLock 保证的是 last 节点的线程安全,takeLock 保证的是
- head 节点的线程安全。两把锁保证了入队和出队没有竞争
- 当节点总数等于 2 时(即一个 dummy 节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争
- 当节点总数等于 1 时(就一个 dummy 节点)这时 take 线程会被 notEmpty 条件阻塞,有竞争,会阻塞
// 用于 put(阻塞) offer(非阻塞)
private final ReentrantLock putLock = new ReentrantLock();
// 用户 take(阻塞) poll(非阻塞)
private final ReentrantLock takeLock = new ReentrantLock();
put 操作
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
// count 用来维护元素计数
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
// 满了等待
while (count.get() == capacity) {
// 倒过来读就好: 等待 notFull
notFull.await();
}
// 有空位, 入队且计数加一
enqueue(node);
c = count.getAndIncrement();
// 除了自己 put 以外, 队列还有空位, 由自己叫醒其他 put 线程
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// 如果队列中有一个元素, 叫醒 take 线程
if (c == 0)
// 这里调用的是 notEmpty.signal() 而不是 notEmpty.signalAll() 是为了减少竞争
signalNotEmpty();
}
take 操作
public E take() throws InterruptedException {
final E x;
final int c;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
// 数量是空的
while (count.get() == 0) {
// 他空的,就让他等待
notEmpty.await();
}
// 删除头节点的下一个节点
x = dequeue();
// 数量 - 1
c = count.getAndDecrement();
if (c > 1)
// 他不是空的,释放
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 如果队列中只有一个空位时, 叫醒 put 线程
// 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacity
if (c == capacity)
// 这里调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争
signalNotFull();
return x;
}
由 put 唤醒 put 是为了避免信号不足
offer 方法
这个方法主要是根据时间判断是否能够完成添加操作,不能够则返回false,否则true
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
final int c;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
// 上锁
putLock.lockInterruptibly();
try {
// 判断是否满了
while (count.get() == capacity) {
if (nanos <= 0L)
// 等待的时间已经结束,直接返回false
return false;
nanos = notFull.awaitNanos(nanos);
}
// 将节点添加到队列中
enqueue(new Node<E>(e));
// 添加数量
c = count.getAndIncrement();
// 判断是否超出总数
if (c + 1 < capacity)
// 未超出总数,释放不满信号
notFull.signal();
} finally {
// 解锁
putLock.unlock();
}
if (c == 0)
// 如果数量为空,释放不满信号
signalNotEmpty();
return true;
}
remove 方法
public boolean remove(Object o) {
if (o == null) return false;
// 上锁,上put锁和task查询锁
fullyLock();
try {
// for循环遍历单向链表
for (Node<E> pred = head, p = pred.next;
p != null;
pred = p, p = p.next) {
// 找到节点
if (o.equals(p.item)) {
// 删除掉节点(1)
unlink(p, pred);
return true;
}
}
return false;
} finally {
// 解锁 takeLock 和 putLock
fullyUnlock();
}
}
void unlink(Node<E> p, Node<E> pred) {
// 要删除的节点数据置为null
p.item = null;
// 拿出要删除节点的下一个节点,给上上一个节点的.next节点,这样中间的那个节点就删除了
pred.next = p.next;
// 如果要删除的节点是last尾节点
if (last == p)
// 则最后节点指向前面,那么pred就是最后一个节点了
last = pred;
// 如果当前数量 等于总数量,那么现在可以释放不满信号了,然后count - 1
if (count.getAndDecrement() == capacity)
notFull.signal();
}
如果满足则删除 removeIf()
这是一个批量删除的方法,主要借助二进制做标记删除链表上节点的方法
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
return bulkRemove(filter);
}
private boolean bulkRemove(Predicate<? super E> filter) {
// 标记删除与否的变量
boolean removed = false;
Node<E> p = null, ancestor = head;
Node<E>[] nodes = null;
int n, len = 0;
do {
// 获取一个64位大小的数组,并且记录了个 n 变量,n 表示数组长度
// 设计一个 64 bit 的数组,为了我们直接使用索引的速度比较快
fullyLock();
try {
if (nodes == null) {
// 从这里能看到,第一个节点不存放任何数据
p = head.next;
// 计算 len 的长度
for (Node<E> q = p; q != null; q = succ(q))
if (q.item != null && ++len == 64)
break;
// new 一个这个大的节点数组
nodes = (Node<E>[]) new Node<?>[len];
}
// 将链表中的节点全部放入到数组中
for (n = 0; p != null && n < len; p = succ(p))
nodes[n++] = p;
} finally {
fullyUnlock();
}
// 利用 deathRow 的算法设计了个标记作用的变量
// 如果标记位为 1 则表示需要删除的,否则无须操作
// 由于 long 是 64 bit, 所以它最多能够标识 64 位的数据
// 不理解的可以看看 1001 0100 这里这个数组大小为8 ,标记为 1 的数组都需要删除掉
long deathRow = 0L;
for (int i = 0; i < n; i++) {
final E e;
if ((e = nodes[i].item) != null && filter.test(e))
deathRow |= 1L << i;
}
if (deathRow != 0) {
fullyLock();
try {
// 遍历数组
for (int i = 0; i < n; i++) {
final Node<E> q;
// 获取 deathRow 标记数组的索引 && 判断数组在该索引的位置下判断元素是否不为 null
if ((deathRow & (1L << i)) != 0L
&& (q = nodes[i]).item != null) {
// findPred 这个方法将q的前一个节点找到
ancestor = findPred(q, ancestor);
// 删除 q 节点, ancestor 就是 q 的前一个节点,ancestor.next = q.next, 然后 q 节点就没了
unlink(q, ancestor);
removed = true;
}
// 节点虽然从链表上删除了,但是还需要在数组上也删除
nodes[i] = null; // help GC
}
} finally {
fullyUnlock();
}
}
} while (n > 0 && p != null);
return removed;
}
字段分析
- capacity:容器可以存放的总数量
- count:当前容器中的数量
- head:链表头
- last:链表尾
- takeLock:可重入锁
- notEmpty:队列不为空条件变量,队列为空则阻塞,队列不为空释放
- putLock:添加节点锁,方式并发问题
- notFull:满了就阻塞,没满就释放(capacity)
构造方法分析
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
- 如果构造器没写容量,默认为
Integer.MAX_VALUE
大小 - 初始化了 capacity 容量大小
- 将last和head头节点和尾节点都指向一个空Node节点上
3. 性能比较
主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较
- Linked 支持有界,Array 强制有界
- Linked 实现是链表,Array 实现是数组
- Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
- Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
- Linked 两把锁,Array 一把锁
ConcurrentLinkedQueue 原理
1. 模仿 ConcurrentLinkedQueue
初始代码
```java
public class Test3 {
public static void main(String[] args) {
MyQueue<String> queue = new MyQueue<>();
queue.offer("1");
queue.offer("2");
queue.offer("3");
System.out.println(queue);
}
}
class MyQueue<E> implements Queue<E> {
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (Node<E> p = head; p != null; p = p.next.get()) {
E item = p.item;
if (item != null) {
sb.append(item).append("->");
}
}
sb.append("null");
return sb.toString();
}
public MyQueue() {
head = last = new Node<>(null, null);
}
private final Node<E> last;
private final Node<E> head;
private E dequeue() {
/*Node<E> h = head;
Node<E> first = h.next;
h.next = h;
head = first;
E x = first.item;
first.item = null;
return x;*/
return null;
}
static class Node<E> {
volatile E item;
public Node(E item, Node<E> next) {
this.item = item;
this.next = new AtomicReference<>(next);
}
AtomicReference<Node<E>> next;
}
}
ConcurrentLinkedQueue 源码分析
ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是
- 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
- 只是这【锁】使用了 cas 来实现
事实上,ConcurrentLinkedQueue 应用还是非常广泛的
例如之前讲的 Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,正是采用了
ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用
类族分析
属性分析
head:头节点
tail:尾节点
内部类分析
static final class Node<E> {
volatile E item;
volatile Node<E> next;
Node(E item) {
ITEM.set(this, item);
}
Node() {}
void appendRelaxed(Node<E> next) {
NEXT.set(this, next);
}
boolean casItem(E cmp, E val) {
return ITEM.compareAndSet(this, cmp, val);
}
}
上面的Node内部类主要做了cas处理
cas属性源码
private static final VarHandle HEAD;
private static final VarHandle TAIL;
static final VarHandle ITEM;
static final VarHandle NEXT;
static {
try {
MethodHandles.Lookup l = MethodHandles.lookup();
HEAD = l.findVarHandle(ConcurrentLinkedQueue.class, "head",
Node.class);
TAIL = l.findVarHandle(ConcurrentLinkedQueue.class, "tail",
Node.class);
ITEM = l.findVarHandle(Node.class, "item", Object.class);
NEXT = l.findVarHandle(Node.class, "next", Node.class);
} catch (ReflectiveOperationException e) {
throw new ExceptionInInitializerError(e);
}
}
构造函数分析
public ConcurrentLinkedQueue() {
head = tail = new Node<E>();
}
head ---> tail ---> Node
增加
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
final Node<E> newNode = new Node<E>(Objects.requireNonNull(e));
// 拿出尾节点
for (Node<E> t = tail, p = t;;) {
// 获得尾节点的下一个节点
Node<E> q = p.next;
// 如果尾节点的.next节点为空,表示它是最后一个节点了
if (q == null) {
// 修改尾节点的下一个节点
if (NEXT.compareAndSet(p, null, newNode)) {
if (p != t)
// 这儿允许设置tail为最新节点的时候失败,因为添加node的时候是根据p.next是不是为null判断的
TAIL.weakCompareAndSet(this, t, newNode);
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// 虽然q是p.next,但是因为是多线程,在offer的同时也在poll,如offer的时候正好p被poll了,那么在poll方法中的updateHead方法会将head指向当前的q,而把p.next指向自己,即:p.next == p
// 这个时候就会造成tail在head的前面,需要重新设置p
// 如果tail已经改变,将p指向tail,但这个时候tail依然可能在head前面
// 如果tail没有改变,直接将p指向head
p = (t != (t = tail)) ? t : head;
else
// tail已经不是最后一个节点,将p指向最后一个节点
p = (p != t && t != (t = tail)) ? t : q;
}
}
CopyOnWriteArrayList
CopyOnWriteArraySet
是它的马甲 底层实现采用了 写入时拷贝
的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。 以新增为例:
public boolean add(E e) {
synchronized (lock) {
// 获取旧的数组
Object[] es = getArray();
int len = es.length;
// 拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)
es = Arrays.copyOf(es, len + 1);
// 添加新元素
es[len] = e;
// 替换旧的数组
setArray(es);
return true;
}
}
其它读操作并未加锁,例如:
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (Object x : getArray()) {
@SuppressWarnings("unchecked") E e = (E) x;
action.accept(e);
}
}
适合『读多写少』的应用场景
get 弱一致性
迭代器弱一致性
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iter = list.iterator();
new Thread(() -> {
list.remove(0);
System.out.println(list);
}).start();
sleep1s();
while (iter.hasNext()) {
System.out.println(iter.next());
}
不要觉得弱一致性就不好
- 数据库的 MVCC 都是弱一致性的表现
- 并发高和一致性是矛盾的,需要权衡
第八章 - JUC的更多相关文章
- Java多线程系列--“JUC锁”03之 公平锁(一)
概要 本章对“公平锁”的获取锁机制进行介绍(本文的公平锁指的是互斥锁的公平锁),内容包括:基本概念ReentrantLock数据结构参考代码获取公平锁(基于JDK1.7.0_40)一. tryAcqu ...
- Java多线程系列--“JUC锁”04之 公平锁(二)
概要 前面一章,我们学习了“公平锁”获取锁的详细流程:这里,我们再来看看“公平锁”释放锁的过程.内容包括:参考代码释放公平锁(基于JDK1.7.0_40) “公平锁”的获取过程请参考“Java多线程系 ...
- 【目录】JUC锁框架目录
JUC锁框架的目录整理如下: 1. [JUC]JUC锁框架综述 2. [JUC]JDK1.8源码分析之LockSupport(一) 3. [JUC]JDK1.8源码分析之AbstractQueuedS ...
- 【目录】JUC集合框架目录
JUC集合框架的目录整理如下: 1. [JUC]JUC集合框架综述 2. [JUC]JDK1.8源码分析之ConcurrentHashMap(一) 3. [JUC]JDK1.8源码分析之Concurr ...
- 精通Web Analytics 2.0 (10) 第八章:竞争情报分析
精通Web Analytics 2.0 : 用户中心科学与在线统计艺术 第八章:竞争情报分析 在现实世界中,收集竞争情报可能意味着雇人在竞争对手的垃圾桶(实际会发生!)翻找. 在虚拟世界中,堆如山的数 ...
- 《Linux内核设计与实现》读书笔记 第十八章 调试
第十八章调试 18.1 准备开始 需要准备的东西: l 一个bug:大部分bug通常都不是行为可靠而且定义明确的 l 一个藏匿bug的内核版本:找出bug首先出现的版本 l 相 ...
- java多线程系类:JUC原子类:01之框架
本系列内容全部来自于http://www.cnblogs.com/skywang12345/p/3514589.html 特在此说明!!!!! 根据修改的数据类型,可以将JUC包中的原子操作类可以分为 ...
- Java多线程系列--“JUC锁”10之 CyclicBarrier原理和示例
概要 本章介绍JUC包中的CyclicBarrier锁.内容包括:CyclicBarrier简介CyclicBarrier数据结构CyclicBarrier源码分析(基于JDK1.7.0_40)Cyc ...
- 《Entity Framework 6 Recipes》中文翻译系列 (42) ------ 第八章 POCO之使用POCO
翻译的初衷以及为什么选择<Entity Framework 6 Recipes>来学习,请看本系列开篇 第八章 POCO 对象不应该知道如何保存它们,加载它们或者过滤它们.这是软件开发中熟 ...
随机推荐
- CentOS7安装vncserver(启动失败及连接黑屏解决办法)
CentOS7安装vncserver(启动失败及连接黑屏解决办法) 转载weixin_34167043 最后发布于2017-11-09 15:11:00 阅读数 42 收藏 展开 AutoSAR入门 ...
- s40 KVM虚拟化企业级实战
1-为何使用虚拟化 02-KVM虚拟化环境准备 yum install libvirt* virt-* qemu-kvm* -y [root@cs7-kvm ~]# systemctl start l ...
- INFJ名言
财富是由什么构成的? 按世俗的观点,就是占有金钱和财宝. 但如果我们用除金钱之外的其他方式来衡量财富, 那么许多在物质上匮乏的人在精神上却是富有的, 许多在物质上富有的人在精神上却是匮乏的. The ...
- 开源月刊《HelloGitHub》第 62 期
兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 分享 GitHub 上有趣.入门级的开源项目. 这里有实战项目.入门教程.黑科技.开源书籍.大厂开源项目等,涵盖多种编程语言 Pyt ...
- CoSky 高性能 服务注册/发现 & 配置中心
CoSky 基于 Redis 的服务治理平台(服务注册/发现 & 配置中心) Consul + Sky = CoSky CoSky 是一个轻量级.低成本的服务注册.服务发现. 配置服务 SDK ...
- 解决Maven资源过滤
<build> <resources> <resource> <directory>src/main/java</directory> &l ...
- Java基础之概述
1. 什么是程序 程序是计算机执行某些操作或解决某个问题而编写的一系列有序指令的集合 2. Java三大版本 Java SE 标准版 Java EE 企业版 Java ME 小型版 3. Java重要 ...
- 分布式调度任务-ElasticJob
一:问题的引出与复现 在一个风和日丽的工作日,公司运营发现系统的任务数据没有推送执行,整个流程因此停住了.我立马远程登陆服务器,查看日志,好家伙,系统在疯狂的打印相同的一段日志:c.d.d.j.i.e ...
- 常用深度学习框——Caffe/ TensorFlow / Keras/ PyTorch/MXNet
常用深度学习框--Caffe/ TensorFlow / Keras/ PyTorch/MXNet 一.概述 近几年来,深度学习的研究和应用的热潮持续高涨,各种开源深度学习框架层出不穷,包括Tenso ...
- MySQL笔记02(黑马)
DDL操作数据库.表 操作数据库:CRUD C(Create):创建 创建数据库: create database 数据库名称; 创建数据库,判断不存在,再创建: create database if ...