学习笔记:Java 并发编程⑥_并发工具_JUC
若文章内容或图片失效,请留言反馈。
部分素材来自网络,若不小心影响到您的利益,请联系博主删除。
- 视频链接:https://www.bilibili.com/video/av81461839
- 配套资料:https://pan.baidu.com/s/1lSDty6-hzCWTXFYuqThRPw( 提取码:5xiu)
写这篇博客旨在制作笔记,方便个人在线阅览,巩固知识。无他用。
博客的内容主要来自上述视频中的内容和其资料中提供的学习笔记。当然,我在此基础之上也增删了一些内容。
参考书籍:《实战 JAVA 高并发程序设计》 葛一鸣 著
参考博客:
- Java 并发之 AQS 详解
- https://javaguide.cn/java/concurrent/aqs.html#AQS_核心思想
- 深入理解 AbstractQueuedSynchronizer 只需 15 张图
推荐阅读博客:
- 史上最全 HashMap 面试总结,51 道附带答案,持续更新中 …
- 面试被问到 ConcurrentHashMap 答不出来,看这一篇就够了 !
- ConcurrentHashMap 源码夺命 15 问,你能坚持到第几问 ?
- 漫画:什么是红黑树?
系列目录
- 学习笔记:Java 并发编程①_基础知识入门
- 学习笔记:Java 并发编程②_共享模型之管程
- 学习笔记:Java 并发编程③_共享模型之内存
- 学习笔记:Java 并发编程④_共享模型之无锁
- 学习笔记:Java 并发编程⑤_共享模型之不可变
- 学习笔记:Java 并发编程⑥_共享模型之并发工具_线程池
- 学习笔记:Java 并发编程⑥_共享模型之并发工具_JUC
本章内容概览
线程池
- ThreadPoolExecutor
- Fork/Join
JUC
- Lock
- Semaphore
- CountdownLatch
- CyclicBarrier
- ConcurrentHashMap
- ConcurrentLinkedQueue
- BlockingQueue
- CopyOnWriteArrayList
参考书籍:《实战 JAVA 高并发程序设计》 葛一鸣 著
JDK 提供了许多的并发容器,它们大部分在java.util.concurrent
包中。
- ConcurrentHashMap:这是一个高效的并发 HashMap。你可以理解为一个线程安全的 HashMap。
- CopyOnWriteArrayList:这是一个 List,从名字看就是和 ArrayList 是一族的。在读多写少的场合,这个 List 的性能非常好,远远好于 Vector。
- ConcurrentLinkedQueue:高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList。
- BlockingQueue:这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
- ConcurrentSkipListMap:跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找。
除了以上并发包中的专有数据结构外,java.util
下的 Vector 是线程安全的(虽然性能和上述专用工具没得比)。
此外,Collections 工具类 可以帮助我们将任意集合包装成线程安全的集合。
1.AQS 原理
1.1.概述
全称是 AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架。
AbstractQueuedSynchronizer,直译的话就是抽象队列同步器。
早期程序员会自己通过一种同步器去实现另一种相近的同步器,例如用可重入锁去实现信号量,或反之。
这显然不够优雅,于是在 JSR166(Java 规范提案)中创建了 AQS,提供了这种通用的同步器机制。
参考书籍:《实战 JAVA 高并发程序设计》 葛一鸣 著
重入锁(ReentrantLock)和信号量(Semaphore)是两个极其重要的并发控制工具。它们都在自己的内部实现了一个叫 AbstractQueuedSynchronizer 的子类。子类的名字都是 Sync,该类正是重入锁和信号量的核心实现。Sync 类中的代码比较少,其核心算法都由 AbstractQueuedSynchronizer 实现。
特点:
- 用 state 属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
- getState:获取 state 状态
- setState:设置 state 状态
- compareAndSetState:CAS 机制设置 state 状态
- 独占模式是只有一个线程能够访问资源,而共享模式可以允许多个线程访问资源
- 提供了基于 FIFO 的等待队列,类似于 Monitor 的 EntryList(不过 EntryList 实际上是非公平竞争)
- 条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet
AbstractQueuedSynchronizer 子类主要实现这样一些方法(默认抛出 UnsupportedOperationException)
- tryAcquire
- tryRelease
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively
获取锁的姿势
// 如果获取锁失败
if (!tryAcquire(arg)) {// 入队, 可以选择阻塞当前线程 // 这里面让当前线程去暂停和恢复运行,用到的机制是 park 和 unpark
}
释放锁的姿势
// 如果释放锁成功
if (tryRelease(arg)) {// 让阻塞线程恢复运行
}
1.2.实现不可重入锁
1.2.1.自定义同步器
MySnc.java
final class MySnc extends AbstractQueuedSynchronizer {@Overrideprotected boolean tryAcquire(int acquires) {if (acquires == 1) {if (compareAndSetState(0, 1)) {setExclusiveOwnerThread(Thread.currentThread());return true;}}return false;}@Overrideprotected boolean tryRelease(int acquires) {if (acquires == 1) {if (getState() == 0) {throw new IllegalMonitorStateException();}setExclusiveOwnerThread(null);setState(0);return true;}return false;}// 是否持有独占锁@Overrideprotected boolean isHeldExclusively() {return getState() == 1;}public Condition newCondition() {// ConditionObject,这就是一个 Condition 对象// 其内部保存着条件变量等待队列// 每个条件变量都有一个等待队列return new ConditionObject();}
}
1.2.2.自定义锁
有了自定义同步器,很容易复用 AQS,实现一个功能完备的自定义锁
MyLock.java
public class MyLock implements Lock {private MySnc sync = new MySnc();// 加锁,加锁失败会进入等待队列@Overridepublic void lock() {sync.acquire(1);}// 加锁,可以打断@Overridepublic void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}// 尝试加锁,只尝试一次@Overridepublic boolean tryLock() {return sync.tryAcquire(1);}// 尝试加锁,带超时时间@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(time));}// 解锁@Overridepublic void unlock() {sync.release(1);}// 创建条件变量@Overridepublic Condition newCondition() {return sync.newCondition();}
}
1.2.3.测试
TestAQS.java
@Slf4j(topic = "c.TestAQS")
public class TestAQS {public static void main(String[] args) {MyLock lock = new MyLock();new Thread(() -> {lock.lock();try {log.debug("locking...");sleep(1);} finally {log.debug("unlocking...");lock.unlock();}}, "t1").start();new Thread(() -> {lock.lock();try {log.debug("locking...");} finally {log.debug("unlocking...");lock.unlock();}}, "t2").start();}
}
这里测试了一下加锁的情况。控制台输出信息符合预期。
17:35:18.065 [t1] DEBUG c.TestAQS - locking...
17:35:19.087 [t1] DEBUG c.TestAQS - unlocking...
17:35:19.087 [t2] DEBUG c.TestAQS - locking...
17:35:19.087 [t2] DEBUG c.TestAQS - unlocking...
如果改为下面代码,会发现自己也会被挡住(只会打印一次 locking)
lock.lock();
log.debug("locking...");
lock.lock();
log.debug("locking...");
控制台输出信息(不可重入测试)
17:38:55.625 [t1] DEBUG c.TestAQS - locking...
1.3.目标
AQS 要实现的功能目标
- 阻塞版本获取锁 acquire 和非阻塞的版本尝试获取锁 tryAcquire
- 获取锁超时机制
- 通过打断取消机制
- 独占机制及共享机制
- 条件不满足时的等待机制
AQS 要实现的性能目标
Instead, the primary performance goal here is scalability: to predictably maintain efficiency even, or especially, when synchronizers are contended.
1.4.内部的数据结构
参考书籍:《实战 JAVA 高并发程序设计》 葛一鸣 著
在 AbstractQueuedSynchroinzer 内部,有一个队列,我们把它叫做 同步等待队列。他的作用是保存等待在这个锁上的线程(由 lock() 操作引起的等待)。此外,为了维护等待在条件变量上的等待线程,AQS 又需要去维护一个 条件等待队列,也就是那些 Condition.await() 方法引起阻塞的线程。由于一个重入锁可以生成多个条件变量对象,因此一个重入锁可以生成多个条件变量对象,一次一个重入锁可能有多个条件变量等待队列。实际上,每个条件变量对象的内部都维护了一个等待队列。其内部逻辑结构和代码层面的具体实现如下面两张图所示。
可以看到,无论是等待队列,还是条件变量等待队列,都使用同一个 Node 类作为链表的节点。
对于同步等待队列,Node 中包括链表的上一个元素 prev、下一个元素 next 和线程对象 thread。
对于条件变量等待队列,还使用了 nextWaiter 表示下一个在条件变量队列中等待的节点。
Node 节点的另一个重要成员是 waitStatus,表示节点在队列中的等待状态。
- CANCELLED:表示线程取消了等待。
如果在获得锁的过程中发生了一些异常,则可能出现取消等待的情况。
比如等待过程中出现了中断异常或者是出现了 timeout。- SIGNAL:表示后续节点需要被唤醒。
- CONDITION:线程在条件变量等待队列中等待。
- PROPAGATE:在共享模式下,无条件传播 releaseShared 状态。
早期的 JDK 版本是没有这个状态的。引入这个状态是为了解决由共享锁并发释放导致线程挂起的 BUG 680120。- 0:初始状态
其中,CANCELLED = 1
,SIGNAL = -1
,CONDITION = -2
,PROPAGATE = -3
。
在具体的实现中,可以简单通过 waitStatus 是否小于或等于 0 来判断是否是 CANCELLED 状态。
1.5.设计
获取锁的逻辑
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 & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
- park 线程还可以通过 interrupt 打断
- 早期的控制线程暂停和恢复的 API 有 suspend 和 resume,但它们是不可用的。
- 队列设计
- 使用了 FIFO 先入先出队列,并不支持优先级队列
- 设计时借鉴了 CLH 队列
队列中有 head 和 tail 两个指针节点,都用 volatile 修饰配合 CAS 使用,每个节点有 state 维护节点状态
入队伪代码(只需要考虑 tail 赋值的原子性)
do {// 原来的 tailNode prev = tail;// 用 cas 在原来 tail 的基础上改为 node
} while(tail.compareAndSet(prev, node))
出队伪代码
// prev 是上一个节点
while((Node prev=node.prev).state != 唤醒状态) {
}// 设置头节点
head = node;
CLH 的好处:无锁,使用自旋;快速,无阻塞
下方图片的来源: https://javaguide.cn/java/concurrent/aqs.html#AQS 核心思想
下方图片的来源:https://www.cnblogs.com/waterystone/p/4920797.html
下方图片的来源:https://blog.csdn.net/m0_37199770/article/details/115755650
AQS 在一些方面改进了 CLH
private Node enq(final Node node) {for (; ; ) {Node t = tail;// 队列中还没有元素 tail 为 nullif (t == null) {// 将 head 从 null -> dummyif (compareAndSetHead(new Node()))tail = head;} else {// 将 node 的 prev 设置为原来的 tailnode.prev = t;// 将 tail 从原来的 tail 设置为 nodeif (compareAndSetTail(t, node)) {// 原来 tail 的 next 设置为 nodet.next = node;return t;}}}
}
主要用到 AQS 的并发工具类
2.ReentrantLock 原理
2.1.非公平锁的实现原理
先从构造器开始看,默认为非公平锁实现
public ReentrantLock() {sync = new NonfairSync();
}
NonfairSync 继承自 AQS
2.1.1.加锁成功流程
没有竞争出现时 |
java/util/concurrent/locks/ReentrantLock.java
final void lock() {if (compareAndSetState(0, 1))// 没有竞争,就直接加锁// state 置为 1,将 owner 线程改为当前线程setExclusiveOwnerThread(Thread.currentThread());else// 有竞争,则调用下面的方法acquire(1);
}
2.1.2.加锁失败流程
第一个竞争出现时 |
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
public final void acquire(int arg) {// 尝试获得许可,arg 为许可的个数。对于重入锁来说,就是每次请求一个。// 即再次尝试一次加锁(这里的逻辑其实蛮像自旋逻辑的)if (!tryAcquire(arg) &&// 若 tryAcquire 失败,则创建一个节点对象 Node,并把它加入等待队列中// 之后都是在 Node 中维护当前线程对象// 然后尝试获得锁acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();
}
参考书籍:《实战 JAVA 高并发程序设计》 葛一鸣 著
tryAcquire 方法:该方法的作用是尝试获得一个许可。
对于 AbstractQueuedSynchronizer 来说,这是一个未实现的抽象方法,具体实现都是在子类中的。
在重入锁、读写锁、信号量等具体类中,都有着各自的实现。
如果 tryAcquire 方法执行成功,则 acquire 直接返回成功。
如果失败,就用 addWaiter() 方法将当前线程加入同步等待队列中。
Thread-1 执行了
- CAS 尝试将 state 由 0 改为 1,结果失败
- 进入 tryAcquire 逻辑,这时 state 已经是1,结果仍然失败
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
private Node addWaiter(Node mode) {// 在 Node 中维护当前的线程对象Node node = new Node(Thread.currentThread(), mode);// 将结点加入队列尾端。这是一个快速的方法,是有可能失败的。// 这种复杂的尝试是为了提神性能(尽管提升不大)Node pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) { //通过 cas 操作把尾节点指向当前节点pred.next = node;return node;}}// 如果快速加入失败,就使用 enq() 方法将 node 加入队列尾端enq(node);return node;
}
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
private Node enq(final Node node) {for (;;) {Node t = tail;// 首次创建 Node 时,会创建两个 Node// 第一个 Node 为哑元,是用来占位的// 第二个 Node 中维护的才是当前线程if (t == null) { // Must initialize// 如果尾节点为空,则创建哨兵节点,通过 cas 把头节点指向哨兵节点if (compareAndSetHead(new Node()))tail = head;} else {// 当前节点的前驱节点设指向之前的尾节点node.prev = t;if (compareAndSetTail(t, node)) { // 通过 cas 把尾结点指向了当前结点t.next = node;return t;}}}
}
接下来当前线程(即 Thread-1)进入 addWaiter 逻辑,构造 Node 队列
- 图中黄色三角表示该 Node 的 waitStatus 状态,其中 0 为默认正常状态
- Node 的创建是懒惰的
- 其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程
这里再说明一下目前的情况:第一个线程是抢占了同步器的。第二个线程初始化了队列:产生了哑元和 Node1 节点。如果还有第三个线程的话,它产生 Node2 节点,并将其插入到 哑元和 Node1 节点之后,之后以此类推 … … 大概就是这么个过程。
此外,这里吐槽一下这个图一些容易引起误解的地方:图中的 head 和 tail 其实都只是头结点和尾结点的引用而已,它们可以指向一个 null,也可以指向同一个结点。当然,这个阶段最终形态是如上图所示的。(这个都是上面的 enq()方法 - 代码块 中做的事情)
如果读者看到这里还是不懂的话,我推荐诸位阅读这篇文章:【深入理解 AbstractQueuedSynchronizer 只需 15 张图】
参考书籍:《实战 JAVA 高并发程序设计》 葛一鸣 著
对已经在队列中的线程请求锁,使用 acquireQueued() 方法,其参数 node 必须是一个已经在队列中等待的节点。
它的功能就是为已经在队列中的节点请求一个许可。
无论是普通的 lock() 还是条件变量中的 await() 都会使用到这个方法。
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
final boolean acquireQueued(final Node node, int arg) {boolean failed = true; // 异常状态,默认是try {boolean interrupted = false; // 该线程是否中断过,默认否for (;;) {// 只有队列中的第二个节点才可以尝试。为什么是第二个?// 队列中第二个节点才是最开始的那个请求者// predecessor 直译就是前任,node.predecessor() 就是获取上一个节点final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {// 把自己设置在头部(请求成功的,会在队列的头部)setHead(node);p.next = null; // help GCfailed = false;return interrupted;}// 请求失败时需要阻塞(block)当前线程吗?由 shouldParkAfterFailedAcquire() 判断// 对于前序节点是 SIGNAL 的,返回 true,然后就需要执行 park 操作// 对于已经取消的节点,就跳过// 对于初始节点和 PROPAGATE 节点,则设置为 SIGNALif (shouldParkAfterFailedAcquire(p, node) &&// 执行 park 操作,挂起当前线程,直到被唤醒// 唤醒后检查当前线程是否被中断,返回该线程中断状态并重置中断状态(返回 false)parkAndCheckInterrupt())// 如果出现了中断,那么中断信息是不可以丢失的,必须要保存起来// 如果是因为 interrupt 被唤醒,返回打断状态为 trueinterrupted = true;}} finally {if (failed)// 尝试获取资源失败并执行异常,取消请求,将当前节点从队列中移除cancelAcquire(node);}}
当前线程进入 acquireQueued 逻辑
- acquireQueued 会在一个死循环中不断尝试获得锁,失败后进入 park 阻塞
- 如果自己是紧邻着 head 指针指向的 Node(null) 的,那么再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
参考博客:【深入理解 AbstractQueuedSynchronizer 只需 15 张图】
shouldParkAfterFailedAcquire 做了三件事情
- 如果前驱节点的等待状态是 SIGNAL,返回 true
- 如果前驱节点的等大状态是 CANCELLED,把 CANCELLED 节点全部移出队列(条件节点)
- 以上两者都不符合,更新前驱节点的等待状态为 SIGNAL,返回 false
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;if (ws == Node.SIGNAL) // SIGNAL = -1/* This node has already set status asking a release to signal it, so it can safely park. */return true;if (ws > 0) { // CANCELED = 1/* Predecessor was cancelled. Skip over predecessors and indicate retry. */do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else { // CONDITION = -2, PROPAGATE = -3, 初始状态 = 0/** waitStatus must be 0 or PROPAGATE. * Indicate that we need a signal, but don't park yet. * Caller will need to retry to make sure it cannot acquire before parking.*/compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;
}
- 进入 shouldParkAfterFailedAcquire 逻辑,将前驱 node,即 head 的 waitStatus 改为 -1,这次返回 false
这里改为 -1 是表明它有责任唤醒后继结点(因为它已经阻塞了,未来是需要被唤醒的,而这个任务就交给了它的前驱节点),这是一种很常见的一种操作:前驱节点唤醒后继结点。
- shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued,再次 tryAcquire 尝试获取锁,当然这时 state 仍为 1,失败
- 当再次进入 shouldParkAfterFailedAcquire 时,这时因为其前驱 node 的 waitStatus 已经是 -1,这次返回 true
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();
}
- 进入 parkAndCheckInterrupt,Thread-1 被 park 了(灰色表示)
再次有多个线程经历上述过程竞争失败,变成这个样子
2.1.3.解锁竞争成功流程
假设:有多个线程经历上述过程竞争失败
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
public final boolean release(int arg) {// tryRelease() 是一个抽象方法,在子类中有具体实现,和 tryAcquire() 一样if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)// 从队列中唤醒一个等待中的线程(直接跳过已经取消的线程)unparkSuccessor(h);return true;}return false;
}
java/util/concurrent/locks/ReentrantLock.java
protected final boolean tryRelease(int releases) {int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c); return free;
}
Thread-0 释放锁,进入 tryRelease 流程,如果成功
- 设置 exclusiveOwnerThread 为 null
state = 0
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
private void unparkSuccessor(Node node) {int ws = node.waitStatus;if (ws < 0)compareAndSetWaitStatus(node, ws, 0);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);}
当前队列不为 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 因为从链表断开,而可被垃圾回收
2.1.4.解锁竞争失败流程
如果这时候有其它线程来竞争(非公平的体现)
例如这时有 Thread-4 来了(这个 Thread-4 不在等待队列里)
如果不巧又被 Thread-4 占了先
- Thread-4 被设置为 exclusiveOwnerThread,
state = 1
- Thread-1 再次进入 acquireQueued 流程,获取锁失败,重新进入 park 阻塞
2.1.5.加锁源码
// Sync 继承自 AQS
static final class NonfairSync extends Sync {private static final long serialVersionUID = 7316153563782823691L;// 加锁实现final void lock() {// 首先用 cas 尝试(仅尝试一次)将 state 从 0 改为 1, 如果成功表示获得了独占锁if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());else// 如果尝试失败,进入 ㈠acquire(1);}// ㈠ AQS 继承过来的方法, 方便阅读, 放在此处public final void acquire(int arg) {// ㈡ tryAcquireif (!tryAcquire(arg) &&// 当 tryAcquire 返回为 false 时, 先调用 addWaiter ㈣, 接着 acquireQueued ㈤acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {selfInterrupt();}}// ㈡ 进入 ㈢protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}// ㈢ Sync 继承过来的方法, 方便阅读, 放在此处final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();// 如果还没有获得锁if (c == 0) {// 尝试用 cas 获得, 这里体现了【非公平性】: 不去检查 AQS 队列if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入else if (current == getExclusiveOwnerThread()) {// state++int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}// 获取失败, 回到调用处return false;}// ㈣ AQS 继承过来的方法, 方便阅读, 放在此处private Node addWaiter(Node mode) {// 将当前线程关联到一个 Node 对象上, 模式为独占模式Node node = new Node(Thread.currentThread(), mode);// 如果 tail 不为 null, cas 尝试将 Node 对象加入 AQS 队列尾部Node pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {// 双向链表pred.next = node;return node;}}// 尝试将 Node 加入 AQS, 进入 ㈥enq(node);return node;}// ㈥ AQS 继承过来的方法, 方便阅读, 放在此处private Node enq(final Node node) {for (; ; ) {Node t = tail;if (t == null) {// 还没有, 设置 head 为哨兵节点(不对应线程,状态为 0)if (compareAndSetHead(new Node())) {tail = head;}} else {// cas 尝试将 Node 对象加入 AQS 队列尾部node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}// ㈤ AQS 继承过来的方法, 方便阅读, 放在此处final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (; ; ) {final Node p = node.predecessor();// 上一个节点是 head, 表示轮到自己(当前线程对应的 node)了, 尝试获取if (p == head && tryAcquire(arg)) {// 获取成功, 设置自己(当前线程对应的 node)为 headsetHead(node);// 上一个节点 help GCp.next = null;failed = false;// 返回中断标记 falsereturn interrupted;}if (// 判断是否应当 park, 进入 ㈦shouldParkAfterFailedAcquire(p, node) &&// park 等待, 此时 Node 的状态被置为 Node.SIGNAL ㈧parkAndCheckInterrupt()) {interrupted = true;}}} finally {if (failed)cancelAcquire(node);}}// ㈦ AQS 继承过来的方法, 方便阅读, 放在此处private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 获取上一个节点的状态int ws = pred.waitStatus;if (ws == Node.SIGNAL) {// 上一个节点都在阻塞, 那么自己也阻塞好了return true;}// > 0 表示取消状态if (ws > 0) {// 上一个节点取消, 那么重构删除前面所有取消的节点, 返回到外层循环重试do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {// 这次还没有阻塞// 但下次如果重试不成功, 则需要阻塞,这时需要设置上一个节点状态为 Node.SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}// ㈧ 阻塞当前线程private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted();}
}
注意:是否需要 unpark 是由当前节点的前驱节点的
waitStatus == Node.SIGNAL
来决定,而不是本节点的 waitStatus 决定
2.1.6.解锁源码
// Sync 继承自 AQS
static final class NonfairSync extends Sync {// 解锁实现public void unlock() {sync.release(1);}// AQS 继承过来的方法, 方便阅读, 放在此处public final boolean release(int arg) {// 尝试释放锁, 进入 ㈠if (tryRelease(arg)) {// 队列头节点 unparkNode h = head;if (// 队列不为 nullh != null &&// waitStatus == Node.SIGNAL 才需要 unparkh.waitStatus != 0) {// unpark AQS 中等待的线程, 进入 ㈡unparkSuccessor(h);}return true;}return false;}// ㈠ Sync 继承过来的方法, 方便阅读, 放在此处protected final boolean tryRelease(int releases) {// state--int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;// 支持锁重入, 只有 state 减为 0, 才释放成功if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}// ㈡ AQS 继承过来的方法, 方便阅读, 放在此处private void unparkSuccessor(Node node) {// 如果状态为 Node.SIGNAL 尝试重置状态为 0// 不成功也可以int ws = node.waitStatus;if (ws < 0) {compareAndSetWaitStatus(node, ws, 0);}// 找到需要 unpark 的节点, 但本节点从 AQS 队列中脱离, 是由唤醒节点完成的Node s = node.next;// 不考虑已取消的节点, 从 AQS 队列从后至前找到队列最前面需要 unpark 的节点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);}
}
2.2.可重入原理
static final class NonfairSync extends Sync {// ...// Sync 继承过来的方法, 方便阅读, 放在此处// 实际地址:java/util/concurrent/locks/ReentrantLock.javafinal boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// 如果已经获得了锁, 线程还是当前线程, 表示发生了锁重入else if (current == getExclusiveOwnerThread()) {// state++int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}// Sync 继承过来的方法, 方便阅读, 放在此处// 实际地址:java/util/concurrent/locks/ReentrantLock.javaprotected final boolean tryRelease(int releases) {// state--int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;// 支持锁重入, 只有 state 减为 0, 才释放成功if (c == 0) {free = true;setExclusiveOwnerThread(null);}setState(c);return free;}}
2.3.可打断原理
回顾知识:interrupt()
、interrupted()
、isInterrupted()
方法名 | static | 功能说明 | 注意 |
---|---|---|---|
interrupt() | 通知目标线程中断,设置线程中断的标志位 | 如果被打断线程正在 sleep,wait,join, 会导致被打断的线程抛出 InterruptedException, 并清除 打断标记; 如果打断的正在运行的进程,则会设置 打断标记; park 的线程被打断,也会设置 打断标记 | |
interrupted() | static | 判断当前线程是否被打断 | 会清除 打断标记 |
isInterrupted() | 判断是否被打断 | 不会清除 打断标记 |
2.3.1.不可打断模式
在此模式下,即使它被打断,仍会驻留在 AQS 队列中,一直要等到获得锁后,才能得知自己被打断了
这里线程是会继续运行的!此处只是打断标记被设置为了 true 而已。
// Sync 继承自 AQS
static final class NonfairSync extends Sync {// ...private final boolean parkAndCheckInterrupt() {// 如果打断标记已经是 true, 则 park 会失效LockSupport.park(this);// interrupted 会清除打断标记return Thread.interrupted();}final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (; ; ) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null;failed = false;// 还是需要获得锁后, 才能返回打断状态return interrupted;}// 进入阻塞的线程可以被其他线程调用它的 interrupt() 方法唤醒if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {// 如果是因为 interrupt 被唤醒, 返回打断状态为 trueinterrupted = true;}}} finally {if (failed)cancelAcquire(node);}}public final void acquire(int arg) {if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {// 如果打断状态为 trueselfInterrupt();}}static void selfInterrupt() {// 设置打断标记// 注意:此处是在获取到锁之后再响应中断,在获取到锁之前不会做出响应。Thread.currentThread().interrupt();}
}
2.3.2.可打断模式
等待锁的过程中,程序可以根据需要,取消对锁的请求。
static final class NonfairSync extends Sync {public final void acquireInterruptibly(int arg) throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();// 如果没有获得到锁, 进入 ㈠if (!tryAcquire(arg))doAcquireInterruptibly(arg);}// ㈠ 可打断的获取锁流程private void doAcquireInterruptibly(int arg) throws InterruptedException {final Node node = addWaiter(Node.EXCLUSIVE);boolean failed = true;try {for (; ; ) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return;}if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {// 在 park 过程中如果被 interrupt 会进入此// 这时候抛出异常, 而不会再次进入 for (;;)throw new InterruptedException();}}} finally {if (failed)cancelAcquire(node);}}
}
2.4.公平锁原理
公平锁与非公平锁主要区别在于 tryAcquire() 方法的实现
在 tryAcquire() 方法里,它有一个对 hasQueuedPredecessors() 方法的判断,用来检查 AQS 队列中是否有前驱节点。
如果 !hasQueuedPredecessors() = true
的话,就相当于队列中有其他的线程,就不会再去竞争了。
static final class FairSync extends Sync {private static final long serialVersionUID = -3000897897090466540L;final void lock() {acquire(1);}// AQS 继承过来的方法, 方便阅读, 放在此处public final void acquire(int arg) {if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {selfInterrupt();}}// 与非公平锁主要区别在于 tryAcquire 方法的实现protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState();if (c == 0) {// 先检查 AQS 队列中是否有前驱节点, 没有才去竞争if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}} else if (current == getExclusiveOwnerThread()) {int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}// ㈠ AQS 继承过来的方法, 方便阅读, 放在此处public final boolean hasQueuedPredecessors() {Node t = tail;Node h = head;Node s;// 主要就是判断三点:// 1.队列中有没有节点// 2.队列中是否存在一个哨兵节点// 3.队列中是否存在多个节点。只有第二个节点才有资格获得锁。// 说人话就是要想得到锁,得有先有队列,然后排队,只有队列中的第二个节点才有资格拿到锁// h != t 时表示队列中有 Nodereturn h != t &&(// (s = h.next) == null 表示队列中还有没有老二(s = h.next) == null ||// 或者队列中老二线程不是此线程s.thread != Thread.currentThread());}
}
2.5.条件变量实现原理
每个条件变量其实就对应着一个等待队列,其实现类是 ConditionObject
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
public class ConditionObject implements Condition, java.io.Serializable { }
2.5.1.await 流程
调用 await() 方法的目的:把同步队列的首节点加到等待队列的尾部并且释放锁。
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
public final void await() throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();// 向条件变量等待队列中加入一个节点Node node = addConditionWaiter();// 在等待之前,一定要释放所有已经持有的许可(不然别的线程怎么工作呢?)int savedState = fullyRelease(node);int interruptMode = 0;// 当前节点是不是已经在同步队列中了?while (!isOnSyncQueue(node)) {// 如果当前节点不在同步队列中,则直接挂起LockSupport.park(this);if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}// 至此,表示线程已经从等待中被唤醒了(signal),并且已经被放到同步等待队列中了。// 所以,既然已经在同步等待队列中了,就可以直接用 acquireQueued() 方法在此请求许可// 从 await() 中唤醒的线程,必须再次获得许可。那么获得几个许可呢?// 之前释放几个,现在就要拿回来几个。不然加锁和解锁的线程数量就对不上。if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;if (node.nextWaiter != null) // clean up if cancelledunlinkCancelledWaiters();if (interruptMode != 0)reportInterruptAfterWait(interruptMode);
}
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
private Node addConditionWaiter() {Node t = lastWaiter;// If lastWaiter is cancelled, clean out.if (t != null && t.waitStatus != Node.CONDITION) {unlinkCancelledWaiters();t = lastWaiter;}Node node = new Node(Thread.currentThread(), Node.CONDITION);if (t == null)firstWaiter = node;elset.nextWaiter = node;lastWaiter = node;return node;
}
开始 Thread-0 持有锁,调用 await(),进入 ConditionObject 的 addConditionWaiter 流程
创建新的 Node 状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
final int fullyRelease(Node node) {boolean failed = true;try {int savedState = getState();if (release(savedState)) { // 把线程所占用的锁全部释放掉failed = false;return savedState;} else {throw new IllegalMonitorStateException();}} finally {if (failed)node.waitStatus = Node.CANCELLED;}
}
接下来进入 AQS 的 fullyRelease 流程,释放同步器上的锁
为什么要完全释放线程所占用的锁呢?
当一个线程获取了对象锁多次(即所重入,计数累加了很多次),这时候调用 await(),当前线程会释放对象锁,必须要释放完;
否则,线程在等待队列,对象锁的 owner 还是这个线程,这就矛盾了。
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
public final boolean release(int arg) {if (tryRelease(arg)) {Node h = head;if (h != null && h.waitStatus != 0)// 唤醒等待队列中的节点unparkSuccessor(h);return true;}return false;
}
unpark AQS 队列中的下一个节点,竞争锁,假设没有其他竞争线程,那么 Thread-1 竞争成功
park 阻塞 Thread-0
2.5.2.signal 流程
当 Condition 对象得到通知时,就会在条件队列中等待,按照 FIFO 原则执行。首先选择第一个节点。
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
public final void signal() {if (!isHeldExclusively())throw new IllegalMonitorStateException();// 从第一个节点开始Node first = firstWaiter;if (first != null)doSignal(first);
}
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
private void doSignal(Node first) {// do{} 中的操作的目的是断开线程与条件变量等待队列的联系do {if ( (firstWaiter = first.nextWaiter) == null)lastWaiter = null;first.nextWaiter = null;// 重点:transferForSignal() 方法把等待队列中的元素移动到同步等待队列中的尾端。// 这样一来,当前面有许可的时候,它就可以自动被唤醒。// 在移动的过程中,如果是一个已经取消的节点,那么它也可以被唤醒。} while (!transferForSignal(first) && (first = firstWaiter) != null);
}
java/util/concurrent/locks/AbstractQueuedSynchronizer.java
final boolean transferForSignal(Node node) {/* If cannot change waitStatus, the node has been cancelled. */if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))return false;/** Splice onto queue and try to set waitStatus of predecessor to indicate that thread is (probably) waiting. * If cancelled or attempt to set waitStatus fails, * wake up to resync (in which case the waitStatus can be transiently and harmlessly wrong).*/Node p = enq(node); // 使用 enq() 方法将 node 加入队列尾端int ws = p.waitStatus;if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))LockSupport.unpark(node.thread);return true;
}
假设 Thread-1 要来唤醒 Thread-0
进入 ConditionObject 的 doSignal 流程,取得等待队列中第一个 Node,即 Thread-0 所在 Node
执行 transferForSignal 流程,将该 Node 加入 AQS 队列尾部,将 Thread-0 的 waitStatus 改为 0,Thread-3 的 waitStatus 改为 -1
Thread-1 释放锁,进入 unlock 流程(这个流程前面已经讲过了,此处不再赘述)
2.5.3.源码
public class ConditionObject implements Condition, java.io.Serializable {private static final long serialVersionUID = 1173984872572414699L;// 第一个等待节点private transient Node firstWaiter;// 最后一个等待节点private transient Node lastWaiter;public ConditionObject() {}// ㈠ 添加一个 Node 至等待队列private Node addConditionWaiter() {Node t = lastWaiter;// 所有已取消的 Node 从队列链表删除, 见 ㈡if (t != null && t.waitStatus != Node.CONDITION) {unlinkCancelledWaiters();t = lastWaiter;}// 创建一个关联当前线程的新 Node, 添加至队列尾部Node node = new Node(Thread.currentThread(), Node.CONDITION);if (t == null)firstWaiter = node;elset.nextWaiter = node;lastWaiter = node;return node;}// 唤醒 - 将没取消的第一个节点转移至 AQS 队列private void doSignal(Node first) {do {// 已经是尾节点了if ((firstWaiter = first.nextWaiter) == null) {lastWaiter = null;}first.nextWaiter = null;} while (// 将等待队列中的 Node 转移至 AQS 队列, 不成功且还有节点则继续循环 ㈢!transferForSignal(first) &&// 队列还有节点(first = firstWaiter) != null);}// 外部类方法, 方便阅读, 放在此处// ㈢ 如果节点状态是取消, 返回 false 表示转移失败, 否则转移成功final boolean transferForSignal(Node node) {// 如果状态已经不是 Node.CONDITION, 说明被取消了if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))return false;// 加入 AQS 队列尾部Node p = enq(node);int ws = p.waitStatus;if (// 上一个节点被取消ws > 0 ||// 上一个节点不能设置状态为 Node.SIGNAL!compareAndSetWaitStatus(p, ws, Node.SIGNAL)) {// unpark 取消阻塞, 让线程重新同步状态LockSupport.unpark(node.thread);}return true;}// 全部唤醒 - 等待队列的所有节点转移至 AQS 队列private void doSignalAll(Node first) {lastWaiter = firstWaiter = null;do {Node next = first.nextWaiter;first.nextWaiter = null;transferForSignal(first);first = next;} while (first != null);}// ㈡private void unlinkCancelledWaiters() {// ...}// 唤醒 - 必须持有锁才能唤醒, 因此 doSignal 内无需考虑加锁public final void signal() {if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)doSignal(first);}// 全部唤醒 - 必须持有锁才能唤醒, 因此 doSignalAll 内无需考虑加锁public final void signalAll() {if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)doSignalAll(first);}// 不可打断等待 - 直到被唤醒public final void awaitUninterruptibly() {// 添加一个 Node 至等待队列, 见 ㈠Node node = addConditionWaiter();// 释放节点持有的锁, 见 ㈣int savedState = fullyRelease(node);boolean interrupted = false;// 如果该节点还没有转移至 AQS 队列, 阻塞while (!isOnSyncQueue(node)) {// park 阻塞LockSupport.park(this);// 如果被打断, 仅设置打断状态if (Thread.interrupted())interrupted = true;}// 唤醒后, 尝试竞争锁, 如果失败进入 AQS 队列if (acquireQueued(node, savedState) || interrupted)selfInterrupt();}// 外部类方法, 方便阅读, 放在此处// ㈣ 因为某线程可能重入,需要将 state 全部释放final int fullyRelease(Node node) {boolean failed = true;try {int savedState = getState();if (release(savedState)) {failed = false;return savedState;} else {throw new IllegalMonitorStateException();}} finally {if (failed)node.waitStatus = Node.CANCELLED;}}// 打断模式 - 在退出等待时重新设置打断状态private static final int REINTERRUPT = 1;// 打断模式 - 在退出等待时抛出异常private static final int THROW_IE = -1;// 判断打断模式private int checkInterruptWhileWaiting(Node node) {return Thread.interrupted() ?(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :0;}// ㈤ 应用打断模式private void reportInterruptAfterWait(int interruptMode)throws InterruptedException {if (interruptMode == THROW_IE)throw new InterruptedException();else if (interruptMode == REINTERRUPT)selfInterrupt();}// 等待 - 直到被唤醒或打断public final void await() throws InterruptedException {if (Thread.interrupted()) {throw new InterruptedException();}// 添加一个 Node 至等待队列, 见 ㈠Node node = addConditionWaiter();// 释放节点持有的锁int savedState = fullyRelease(node);int interruptMode = 0;// 如果该节点还没有转移至 AQS 队列, 阻塞while (!isOnSyncQueue(node)) {// park 阻塞LockSupport.park(this);// 如果被打断, 退出等待队列if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}// 退出等待队列后, 还需要获得 AQS 队列的锁if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;// 所有已取消的 Node 从队列链表删除, 见 ㈡if (node.nextWaiter != null)unlinkCancelledWaiters();// 应用打断模式, 见 ㈤if (interruptMode != 0)reportInterruptAfterWait(interruptMode);}// 等待 - 直到被唤醒或打断或超时public final long awaitNanos(long nanosTimeout) throws InterruptedException {if (Thread.interrupted()) {throw new InterruptedException();}// 添加一个 Node 至等待队列, 见 ㈠Node node = addConditionWaiter();// 释放节点持有的锁int savedState = fullyRelease(node);// 获得最后期限final long deadline = System.nanoTime() + nanosTimeout;int interruptMode = 0;// 如果该节点还没有转移至 AQS 队列, 阻塞while (!isOnSyncQueue(node)) {// 已超时, 退出等待队列if (nanosTimeout <= 0L) {transferAfterCancelledWait(node);break;}// park 阻塞一定时间, spinForTimeoutThreshold 为 1000 nsif (nanosTimeout >= spinForTimeoutThreshold)LockSupport.parkNanos(this, nanosTimeout);// 如果被打断, 退出等待队列if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;nanosTimeout = deadline - System.nanoTime();}// 退出等待队列后, 还需要获得 AQS 队列的锁if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;// 所有已取消的 Node 从队列链表删除, 见 ㈡if (node.nextWaiter != null)unlinkCancelledWaiters();// 应用打断模式, 见 ㈤if (interruptMode != 0)reportInterruptAfterWait(interruptMode);return deadline - System.nanoTime();}// 等待 - 直到被唤醒或打断或超时, 逻辑类似于 awaitNanospublic final boolean awaitUntil(Date deadline) throws InterruptedException {// ...}// 等待 - 直到被唤醒或打断或超时, 逻辑类似于 awaitNanospublic final boolean await(long time, TimeUnit unit) throws InterruptedException {// ...}// 工具方法 省略 ...
}
3.读写锁原理
3.1.ReadWriteLock
参考书籍:《实战 JAVA 高并发程序设计》 葛一鸣 著
ReadWriteLock 是 JDK5 中提供的读写分离锁。读写分离锁可以有效地帮助减少锁竞争,以提升系统性能。
用锁分离的机制来提升性能非常容易理解,比如线程 A1、A2、A3 进行写操作,B1、B2、B3 进行读操作,如果使用重入锁或者内部锁,则理论上说所有读之间、读与写之间、写和写之间都是串行操作。当 B1 进行读取时,B2、B3 则需要等待锁。由于读操作并不对数据的完整性造成破坏,这种等待显然是不合理。因此,读写锁就有了发挥功能的余地。
在这种情况下,读写锁允许多个线程同时读,使得 B1、B2、B3 之间真正并行。但是,考虑到数据完整性,写写操作和读写操作间依然是需要相互等待和持有锁的。总的来说,读写锁的访问约束如下表所示。表:读写锁的访问约束情况
读 写 读 非阻塞 阻塞 写 阻塞 阻塞
- 读-读 不互斥:读读之间不阻塞。
- 读-写 互斥:读阻塞写,写也会阻塞读。
- 写-写 互斥:写写阻塞。
如果在系统中,读操作次数远远大于写操作,则读写锁就可以发挥最大的功效,提升系统的性能。
当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,提高性能。
类似于数据库中的 select ... from ... lock in share mode
示例:提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
DataContainer.java
@Slf4j(topic = "c.DataContainer")
class DataContainer {private Object data;private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();private ReentrantReadWriteLock.ReadLock r = rw.readLock();private ReentrantReadWriteLock.WriteLock w = rw.writeLock();public Object read() {log.debug("获取读锁...");r.lock();try {log.debug("读取");sleep(1);return data;} finally {log.debug("释放读锁...");r.unlock();}}public void write() {log.debug("获取写锁...");w.lock();try {log.debug("写入");sleep(1);} finally {log.debug("释放写锁...");w.unlock();}}
}
- 测试 读锁-读锁 (可以并发)
TestReadWriteLock.java
@Slf4j(topic = "c.TestReadWriteLock")
public class TestReadWriteLock throws InterruptedException {public static void main(String[] args) {DataContainer dataContainer = new DataContainer();new Thread(dataContainer::read, "t1").start();new Thread(dataContainer::read, "t2").start();}
}
控制台输出(从这里可以看到 Thread-0 锁定期间,Thread-1 的读操作不受影响)
15:32:35.597 [t1] DEBUG c.DataContainer - 获取读锁...
15:32:35.597 [t2] DEBUG c.DataContainer - 获取读锁...
15:32:35.609 [t2] DEBUG c.DataContainer - 读取
15:32:35.609 [t1] DEBUG c.DataContainer - 读取
15:32:36.613 [t2] DEBUG c.DataContainer - 释放读锁...
15:32:36.613 [t1] DEBUG c.DataContainer - 释放读锁...
- 测试 读锁-写锁(相互阻塞)
DataContainer dataContainer = new DataContainer();
new Thread(dataContainer::read, "t1").start();
Thread.sleep(100);
new Thread(dataContainer::write, "t2").start();
控制台输出
15:39:10.699 [t1] DEBUG c.DataContainer - 获取读锁...
15:39:10.712 [t1] DEBUG c.DataContainer - 读取
15:39:10.810 [t2] DEBUG c.DataContainer - 获取写锁...
15:39:11.722 [t1] DEBUG c.DataContainer - 释放读锁...
15:39:11.722 [t2] DEBUG c.DataContainer - 写入
15:39:12.734 [t2] DEBUG c.DataContainer - 释放写锁...
- 测试 写锁-写锁(相互阻塞)
DataContainer dataContainer = new DataContainer();
new Thread(dataContainer::write, "t1").start();
Thread.sleep(100);
new Thread(dataContainer::write, "t2").start();
控制台输出
15:44:01.570 [t1] DEBUG c.DataContainer - 获取写锁...
15:44:01.581 [t1] DEBUG c.DataContainer - 写入
15:44:01.674 [t2] DEBUG c.DataContainer - 获取写锁...
15:44:02.592 [t1] DEBUG c.DataContainer - 释放写锁...
15:44:02.592 [t2] DEBUG c.DataContainer - 写入
15:44:03.603 [t2] DEBUG c.DataContainer - 释放写锁...
注意事项
- 读锁不支持条件变量
- 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
- 重入时降级支持:即持有写锁的情况下去获取读锁
重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
r.lock();try {// ...w.lock();try {// ...} finally {w.unlock();}
} finally {r.unlock();
}
重入时降级支持:即持有写锁的情况下去获取读锁
class CachedData {Object data;// 是否有效,如果失效,需要重新计算 datavolatile boolean cacheValid;final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();void processCachedData() {rwl.readLock().lock();if (!cacheValid) {// 获取写锁前必须释放读锁rwl.readLock().unlock();rwl.writeLock().lock();try {// 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新if (!cacheValid) {data = ...cacheValid = true;}// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存rwl.readLock().lock();} finally {rwl.writeLock().unlock();}}// 自己用完数据, 释放读锁try {use(data);} finally {rwl.readLock().unlock();}}
}
3.2.缓存更新策略
更新时,是先清缓存还是先更新数据库?
先清缓存(感叹号出现的地方,即为数据不一致情况的发生地点)
先更新数据库(感叹号出现的地方,即为数据不一致情况的发生地点)
补充一种情况,假设查询线程 A 查询数据时恰好缓存数据由于时间到期失效,或是第一次查询(这种情况的出现几率非常小)
(感叹号出现的地方,即为数据不一致情况的发生地点)
相关视频:ReentrantReadWriteLock 缓存应用示例
使用读写锁实现一个简单的按需加载缓存
示例代码
public class GenericCachedDao<T> {// HashMap 作为缓存非线程安全, 需要保护HashMap<SqlPair, T> map = new HashMap<>();ReentrantReadWriteLock lock = new ReentrantReadWriteLock();GenericDao genericDao = new GenericDao();public int update(String sql, Object... params) {SqlPair key = new SqlPair(sql, params);// 加写锁, 防止其它线程对缓存读取和更改lock.writeLock().lock();try {int rows = genericDao.update(sql, params);map.clear();return rows;} finally {lock.writeLock().unlock();}}public T queryOne(Class<T> beanClass, String sql, Object... params) {SqlPair key = new SqlPair(sql, params);// 加读锁, 防止其它线程对缓存更改lock.readLock().lock();try {T value = map.get(key);if (value != null) {return value;}} finally {lock.readLock().unlock();}// 加写锁, 防止其它线程对缓存读取和更改lock.writeLock().lock();try {// get 方法上面部分是可能多个线程进来的, 可能已经向缓存填充了数据// 为防止重复查询数据库, 再次验证T value = map.get(key);if (value == null) {// 如果没有, 查询数据库value = genericDao.queryOne(beanClass, sql, params);map.put(key, value);}return value;} finally {lock.writeLock().unlock();}}// 作为 key 保证其是不可变的class SqlPair {private String sql;private Object[] params;public SqlPair(String sql, Object[] params) {this.sql = sql;this.params = params;}@Overridepublic boolean equals(Object o) {if (this == o) {return true;}if (o == null || getClass() != o.getClass()) {return false;}SqlPair sqlPair = (SqlPair) o;return sql.equals(sqlPair.sql) &&Arrays.equals(params, sqlPair.params);}@Overridepublic int hashCode() {int result = Objects.hash(sql);result = 31 * result + Arrays.hashCode(params);return result;}}
}
注意事项
- 在上面的代码块中,体现的是读写锁的应用,保证缓存和数据库的一致性,但有下面的问题没有考虑
- 适合读多写少,如果写操作比较频繁,以上实现性能低
- 没有考虑缓存容量
- 没有考虑缓存过期
- 只适合单机
- 并发性还是低,目前只会用一把锁
- 更新方法太过简单粗暴,清空了所有 key(考虑按类型分区或重新设计 key)
- 乐观锁实现:用 CAS 去更新
3.3.图解流程
读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个
3.3.1.【t1】w.lock,【t2】r.lock
【t1】
w.lock
,【t2】r.lock
- t1 成功上锁,流程与 ReentrantLock 加锁相比没有特殊之处
不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位
- t2 执行 r.lock,这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。
如果有写锁占据,那么 tryAcquireShared 返回 -1 表示失败
tryAcquireShared 返回值表示:- -1:表示失败
- 0:表示成功,但后继节点不会继续唤醒
- 正数:表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回 1
- 这时会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点
不同之处在于节点被设置为 Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态
-
t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
-
如果没有成功,在 doAcquireShared 内
for (;;)
循环一次,把前驱节点的 waitStatus 改为 -1,再for (;;)
循环一次尝试 tryAcquireShared(1)。如果还不成功,那么在 parkAndCheckInterrupt() 处 park
3.3.2.【t3】r.lock,【t4】w.lock
【t3】
r.lock
,【t4】w.lock
这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子
3.3.3.【t1】w.unlock
【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 (此处改为 0 是防止改动过程中其他线程的干扰)并唤醒老二,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
这回再来一次 for (;;)
执行 tryAcquireShared 成功则让读锁计数加一
这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点
3.3.4.【t2】r.unlock,【t3】r.unlock
【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) 成功,修改头结点,流程结束
3.4.源码
3.4.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;}
}
3.4.2.写锁释放流程
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;}
}
3.4.3.读锁上锁流程
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 GCif (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// 设置自己为 headsetHead(node);// propagate 表示有共享资源(例如共享读锁或信号量)// 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE// 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATEif (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, 继续 doReleaseSharedunparkSuccessor(h);} else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue; // loop on failed CAS}if (h == head) // loop if head changedbreak;}}
}
3.4.4.读锁释放流程
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 casesunparkSuccessor(h);}// 如果已经是 0 了,改为 -3,用来解决传播性,见后文信号量 bug 分析else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue; // loop on failed CAS}if (h == head) // loop if head changedbreak;}}
}
3.5.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() 方法
@Slf4j(topic = "c.DataContainerStamped")
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 class TestDataContainerStamped {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();}
}
输出结果,可以看到实际没有加读锁
20:18:32.269 [t1] DEBUG c.DataContainerStamped - optimistic read locking...256
20:18:32.773 [t2] DEBUG c.DataContainerStamped - optimistic read locking...256
20:18:32.773 [t2] DEBUG c.DataContainerStamped - read finish...256, data:1
20:18:33.293 [t1] DEBUG c.DataContainerStamped - read finish...256, data:1
测试 读-写 时优化读补加读锁
DataContainerStamped dataContainer = new DataContainerStamped(1);
new Thread(() -> {dataContainer.read(1);
}, "t1").start();sleep(0.5);new Thread(() -> {dataContainer.write(100);
}, "t2").start();
输出结果
20:20:15.532 [t1] DEBUG c.DataContainerStamped - optimistic read locking...256
20:20:16.037 [t2] DEBUG c.DataContainerStamped - write lock 384
20:20:16.550 [t1] DEBUG c.DataContainerStamped - updating to read lock... 256
20:20:18.047 [t2] DEBUG c.DataContainerStamped - write unlock 384
20:20:18.047 [t1] DEBUG c.DataContainerStamped - read lock 513
20:20:19.058 [t1] DEBUG c.DataContainerStamped - read finish...513, data:100
20:20:19.058 [t1] DEBUG c.DataContainerStamped - read unlock 513
注意
- StampedLock 不支持条件变量
- StampedLock 不支持可重入
4.Semaphore
[ˈsɛməˌfɔr] 信号量,用来限制能同时访问共享资源的线程上限。
4.1.基本使用
TestSemaphore_1.java
@Slf4j(topic = "c.TestSemaphore_1")
public class TestSemaphore_1 {public static void main(String[] args) {// 1. 创建 semaphore 对象Semaphore semaphore = new Semaphore(3);// 2. 10 个线程同时运行for (int i = 0; i < 10; i++) {new Thread(() -> {// 3. 获取许可try {semaphore.acquire();} catch (InterruptedException e) {e.printStackTrace();}try {log.debug("running...");sleep(1);log.debug("end...");} finally {// 4. 释放许可semaphore.release();}}).start();}}
}
控制台输出信息
20:27:30.522 [Thread-1] DEBUG c.TestSemaphore_1 - running...
20:27:30.522 [Thread-0] DEBUG c.TestSemaphore_1 - running...
20:27:30.522 [Thread-2] DEBUG c.TestSemaphore_1 - running...
20:27:31.548 [Thread-1] DEBUG c.TestSemaphore_1 - end...
20:27:31.548 [Thread-2] DEBUG c.TestSemaphore_1 - end...
20:27:31.548 [Thread-0] DEBUG c.TestSemaphore_1 - end...
20:27:31.548 [Thread-3] DEBUG c.TestSemaphore_1 - running...
20:27:31.548 [Thread-5] DEBUG c.TestSemaphore_1 - running...
20:27:31.548 [Thread-4] DEBUG c.TestSemaphore_1 - running...
20:27:32.563 [Thread-3] DEBUG c.TestSemaphore_1 - end...
20:27:32.563 [Thread-4] DEBUG c.TestSemaphore_1 - end...
20:27:32.563 [Thread-5] DEBUG c.TestSemaphore_1 - end...
20:27:32.563 [Thread-6] DEBUG c.TestSemaphore_1 - running...
20:27:32.563 [Thread-7] DEBUG c.TestSemaphore_1 - running...
20:27:32.563 [Thread-8] DEBUG c.TestSemaphore_1 - running...
20:27:33.568 [Thread-7] DEBUG c.TestSemaphore_1 - end...
20:27:33.568 [Thread-6] DEBUG c.TestSemaphore_1 - end...
20:27:33.568 [Thread-8] DEBUG c.TestSemaphore_1 - end...
20:27:33.568 [Thread-9] DEBUG c.TestSemaphore_1 - running...
20:27:34.580 [Thread-9] DEBUG c.TestSemaphore_1 - end...
4.2.应用(改进数据库连接池)
视频链接:semaphore_应用_改进数据库连接池
-
使用 Semaphore 限流,在访问高峰期时,让请求线程阻塞,高峰期过去再释放许可。
当然它只适合限制单机线程数量,并且仅是限制线程数,而不是限制资源数(例如连接数,请对比 Tomcat LimitLatch 的实现) -
用 Semaphore 实现简单连接池,对比 享元模式 下的实现(用 wait notify),性能和可读性显然更好,注意下面的实现中线程数和数据库连接数是相等的
之前在这篇文章里提到了 享元模式:学习笔记:Java 并发编程⑤_共享模型之不可变
这里涉及到的代码有些多,故只贴被修改过的代码。(相关代码都在官方提供的资料里)
Pool.java
class Pool {// 1. 连接池大小private final int poolSize;// 2. 连接对象数组private Connection[] connections;// 3. 连接状态数组 0 表示空闲, 1 表示繁忙private AtomicIntegerArray states;private Semaphore semaphore;// 4. 构造方法初始化public Pool(int poolSize) {this.poolSize = poolSize;// 让许可数与资源数一致this.semaphore = new Semaphore(poolSize);this.connections = new Connection[poolSize];this.states = new AtomicIntegerArray(new int[poolSize]);for (int i = 0; i < poolSize; i++) {connections[i] = new MockConnection("连接" + (i + 1));}}// 5. 借连接public Connection borrow() {// t1, t2, t3// 获取许可try {semaphore.acquire(); // 没有许可的线程,在此等待} catch (InterruptedException e) {e.printStackTrace();}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;}// 6. 归还连接public void free(Connection conn) {for (int i = 0; i < poolSize; i++) {if (connections[i] == conn) {states.set(i, 0);log.debug("free {}", conn);semaphore.release();break;}}}
}
4.3.图解流程
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 状态
4.4.源码
static final class NonfairSync extends Sync {private static final long serialVersionUID = -2694183684443567898L;NonfairSync(int permits) {// permits 即 statesuper(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 (// 如果许可已经用完, 返回负数, 表示获取失败, 进入 doAcquireSharedInterruptiblyremaining < 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 GCfailed = 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) // overflowthrow new Error("Maximum permit count exceeded");if (compareAndSetState(current, next))return true;}}
}
5.CountdownLatch
5.1.基本概念
参考书籍:《实战 JAVA 高并发程序设计》 葛一鸣 著
CountDownLatch 是一个非常实用的多线程控制工具类。“Count Down” 在英文中意为倒计数,Latch 为门闩的意思。如果翻译成为倒计数门闩,我想大家都会觉得不知所云吧!因此,这里简单地称之为倒计数器。在这里,门闩的含义是:把门锁起来,不让里面的线程跑出来。因此,这个工具通常用来控制线程等待,它可以让某一个线程等待直到倒计时结束,再开始执行。
对于倒计时器,一种典型的场景就是火箭发射。在火箭发射前,为了保证万无一失,往往还要进行各项设备、仪器的检查。只有等所有的检查完毕后,引擎才能点火。这种场景就非常适合使用 CountDownLatch。它可以使得点火线程等待所有检查线程全部完工后,再执行。
java/util/concurrent/CountDownLatch.java
// 构造方法
public CountDownLatch(int count) {if (count < 0) throw new IllegalArgumentException("count < 0");this.sync = new Sync(count);
}
用来进行线程同步协作,等待所有线程完成倒计时。
其中构造参数用来初始化等待计数值,await() 用来等待计数归零,countDown() 用来让计数减一
java/util/concurrent/CountDownLatch.java
(CountDownLatch 在内部封装了一个同步器)
private static final class Sync extends AbstractQueuedSynchronizer {private static final long serialVersionUID = 4982264981922014374L;Sync(int count) {setState(count);}int getCount() {return getState();}protected int tryAcquireShared(int acquires) {return (getState() == 0) ? 1 : -1;}protected boolean tryReleaseShared(int releases) {// Decrement count; signal when transition to zerofor (;;) {int c = getState();if (c == 0)return false;int nextc = c-1;if (compareAndSetState(c, nextc))return nextc == 0;}}
}
示例代码
@Slf4j(topic = "c.TestCountDownLatch")
public class TestCountDownLatch {public static void main(String[] args) throws InterruptedException, ExecutionException {// test_1();// test_2();}
}
private static void test_1() throws InterruptedException {CountDownLatch latch = new CountDownLatch(3);new Thread(() -> {log.debug("begin...");sleep(1);latch.countDown();log.debug("end...{}", latch.getCount());}).start();new Thread(() -> {log.debug("begin...");sleep(2);latch.countDown();log.debug("end...{}", latch.getCount());}).start();new Thread(() -> {log.debug("begin...");sleep(1.5);latch.countDown();log.debug("end...{}", latch.getCount());}).start();log.debug("waiting...");latch.await();log.debug("wait end...");
}
控制台输出信息(主方法调用 test_1() 方法)
16:53:52.628 [main] DEBUG c.TestCountDownLatch - waiting...
16:53:52.628 [Thread-1] DEBUG c.TestCountDownLatch - begin...
16:53:52.628 [Thread-0] DEBUG c.TestCountDownLatch - begin...
16:53:52.628 [Thread-2] DEBUG c.TestCountDownLatch - begin...
16:53:53.647 [Thread-0] DEBUG c.TestCountDownLatch - end...2
16:53:54.154 [Thread-2] DEBUG c.TestCountDownLatch - end...1
16:53:54.643 [Thread-1] DEBUG c.TestCountDownLatch - end...0
16:53:54.643 [main] DEBUG c.TestCountDownLatch - wait end...
可以配合线程池使用,改进如下
// 配合线程池使用
private static void test_2() throws InterruptedException, ExecutionException {CountDownLatch latch = new CountDownLatch(3);ExecutorService service = Executors.newFixedThreadPool(4);service.submit(() -> {log.debug("begin...");sleep(1);latch.countDown();log.debug("end...{}", latch.getCount());});service.submit(() -> {log.debug("begin...");sleep(1.5);latch.countDown();log.debug("end...{}", latch.getCount());});service.submit(() -> {log.debug("begin...");sleep(2);latch.countDown();log.debug("end...{}", latch.getCount());});service.submit(() -> {try {log.debug("waiting...");latch.await();log.debug("wait end...");} catch (InterruptedException e) {e.printStackTrace();}});}
控制台输出信息(主方法调用 test_2() 方法)
17:01:47.017 [pool-1-thread-4] DEBUG c.TestCountDownLatch - waiting...
17:01:47.017 [pool-1-thread-3] DEBUG c.TestCountDownLatch - begin...
17:01:47.017 [pool-1-thread-2] DEBUG c.TestCountDownLatch - begin...
17:01:47.017 [pool-1-thread-1] DEBUG c.TestCountDownLatch - begin...
17:01:48.044 [pool-1-thread-1] DEBUG c.TestCountDownLatch - end...2
17:01:48.545 [pool-1-thread-2] DEBUG c.TestCountDownLatch - end...1
17:01:49.041 [pool-1-thread-3] DEBUG c.TestCountDownLatch - end...0
17:01:49.041 [pool-1-thread-4] DEBUG c.TestCountDownLatch - wait end...
5.2.应用
5.2.1.等待多线程准备完毕
private static void test_3() throws InterruptedException, ExecutionException {AtomicInteger num = new AtomicInteger(0);ExecutorService service = Executors.newFixedThreadPool(10, (r) -> {return new Thread(r, "t" + num.getAndIncrement());});CountDownLatch latch = new CountDownLatch(10);String[] all = new String[10];Random r = new Random();for (int j = 0; j < 10; j++) {int x = j;service.submit(() -> {for (int i = 0; i <= 100; i++) {try {Thread.sleep(r.nextInt(100));} catch (InterruptedException e) {}all[x] = Thread.currentThread().getName() + "(" + (i + "%") + ")";System.out.print("\r" + Arrays.toString(all));}latch.countDown();});}latch.await();System.out.println("\n游戏开始...");service.shutdown();
}
中间输出
[t0(52%), t1(47%), t2(51%), t3(40%), t4(49%), t5(44%), t6(49%), t7(52%), t8(46%), t9(46%)]
最终的输出结果
[t0(100%), t1(100%), t2(100%), t3(100%), t4(100%), t5(100%), t6(100%), t7(100%), t8(100%), t9(100%)]
游戏开始...
5.2.2.等待多个远程调用结束
@RestController
public class TestCountDownlatchController {@GetMapping("/order/{id}")public Map<String, Object> order(@PathVariable int id) {HashMap<String, Object> map = new HashMap<>();map.put("id", id);map.put("total", "2300.00");sleep(2000);return map;}@GetMapping("/product/{id}")public Map<String, Object> product(@PathVariable int id) {HashMap<String, Object> map = new HashMap<>();if (id == 1) {map.put("name", "小爱音箱");map.put("price", 300);} else if (id == 2) {map.put("name", "小米手机");map.put("price", 2000);}map.put("id", id);sleep(1000);return map;}@GetMapping("/logistics/{id}")public Map<String, Object> logistics(@PathVariable int id) {HashMap<String, Object> map = new HashMap<>();map.put("id", id);map.put("name", "中通快递");sleep(2500);return map;}private void sleep(int millis) {try {Thread.sleep(millis);} catch (InterruptedException e) {e.printStackTrace();}}
}
rest 远程调用
private static void testC() {RestTemplate restTemplate = new RestTemplate();log.debug("begin");ExecutorService service = Executors.newCachedThreadPool();CountDownLatch latch = new CountDownLatch(4);Future<Map<String, Object>> f1 = service.submit(() -> {Map<String, Object> r =restTemplate.getForObject("http://localhost:8080/order/{1}", Map.class, 1);return r;});Future<Map<String, Object>> f2 = service.submit(() -> {Map<String, Object> r =restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 1);return r;});Future<Map<String, Object>> f3 = service.submit(() -> {Map<String, Object> r =restTemplate.getForObject("http://localhost:8080/product/{1}", Map.class, 2);return r;});Future<Map<String, Object>> f4 = service.submit(() -> {Map<String, Object> r =restTemplate.getForObject("http://localhost:8080/logistics/{1}", Map.class, 1);return r;});System.out.println(f1.get());System.out.println(f2.get());System.out.println(f3.get());System.out.println(f4.get());log.debug("执行完毕");service.shutdown();
}
执行结果
19:51:39.711 c.TestCountDownLatch [main] - begin
{total=2300.00, id=1}
{price=300, name=小爱音箱, id=1}
{price=2000, name=小米手机, id=2}
{name=中通快递, id=1}
19:51:42.407 c.TestCountDownLatch [main] - 执行完毕
6.CyclicBarrier
[ˈsaɪklɪk ˈbæriɚ] 循环栅栏,用来进行线程协作,等待线程满足某个计数。构造时设置 计数个数,每个线程执行到某个需要 “同步” 的时刻调用 await() 方法进行等待,当等待的线程数满足 计数个数 时,继续执行。
注意:CyclicBarrier 与 CountDownLatch 的主要区别在于 CyclicBarrier 是可以重用的,CyclicBarrier 可以被比喻为 人满发车
@Slf4j(topic = "c.TestCyclicBarrier")
public class TestCyclicBarrier {public static void main(String[] args) {test_2();}private static void test_2() {ExecutorService executorService = Executors.newFixedThreadPool(2);CyclicBarrier cyclicBarrier = new CyclicBarrier(2, () -> {log.info("task1 & task2 finish ...");});for (int i = 0; i < 3; i++) {executorService.submit(() -> {log.info("task1 begin ...");try {Thread.sleep(1000);cyclicBarrier.await();} catch (InterruptedException | BrokenBarrierException e) {e.printStackTrace();}});executorService.submit(() -> {log.info("task2 begin ...");try {Thread.sleep(2000);cyclicBarrier.await();} catch (InterruptedException | BrokenBarrierException e) {e.printStackTrace();}});}executorService.shutdown();}
}
输出结果
22:01:02.917 [pool-1-thread-1] INFO c.TestCyclicBarrier - task1 begin ...
22:01:02.917 [pool-1-thread-2] INFO c.TestCyclicBarrier - task2 begin ...
22:01:04.935 [pool-1-thread-2] INFO c.TestCyclicBarrier - task1 & task2 finish ...
22:01:04.935 [pool-1-thread-1] INFO c.TestCyclicBarrier - task1 begin ...
22:01:04.935 [pool-1-thread-2] INFO c.TestCyclicBarrier - task2 begin ...
22:01:06.935 [pool-1-thread-2] INFO c.TestCyclicBarrier - task1 & task2 finish ...
22:01:06.935 [pool-1-thread-2] INFO c.TestCyclicBarrier - task1 begin ...
22:01:06.935 [pool-1-thread-1] INFO c.TestCyclicBarrier - task2 begin ...
22:01:08.944 [pool-1-thread-1] INFO c.TestCyclicBarrier - task1 & task2 finish ...
7.线程安全集合类概述
视频链接:线程安全集合类_概述
线程安全集合类可以分为三大类:
- 遗留的线程安全集合,如 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,不再继续遍历
8.ConcurrentHashMap
推荐阅读博客:
- 史上最全 HashMap 面试总结,51 道附带答案,持续更新中 …
- 面试被问到 ConcurrentHashMap 答不出来,看这一篇就够了 !
- ConcurrentHashMap 源码夺命 15 问,你能坚持到第几问 ?
8.1.错误用法
练习:单词计数
生成测试数据
static final String ALPHA = "abcedfghijklmnopqrstuvwxyz";public static void main(String[] args) {int length = ALPHA.length();int count = 200;List<String> list = new ArrayList<>(length * count);for (int i = 0; i < length; i++) {char ch = ALPHA.charAt(i);for (int j = 0; j < count; j++) {list.add(String.valueOf(ch));}}Collections.shuffle(list);for (int i = 0; i < 26; i++) {try (PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream("tmp/" + (i + 1) + ".txt")))) {String collect = list.subList(i * count, (i + 1) * count).stream().collect(Collectors.joining("\n"));out.print(collect);} catch (IOException e) {}}}
模版代码,模版代码中封装了多线程读取文件的代码
private static <V> void demo_1(Supplier<Map<String, V>> supplier, BiConsumer<Map<String, V>, List<String>> consumer) {Map<String, V> counterMap = supplier.get();List<Thread> ts = new ArrayList<>();for (int i = 1; i <= 26; i++) {int idx = i;Thread thread = new Thread(() -> {List<String> words = readFromFile(idx);consumer.accept(counterMap, words);});ts.add(thread);}ts.forEach(Thread::start);ts.forEach(t -> {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}});System.out.println(counterMap);
}
public static List<String> readFromFile(int i) {ArrayList<String> words = new ArrayList<>();try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream("tmp/" + i + ".txt")))) {while (true) {String word = in.readLine();if (word == null) {break;}words.add(word);}return words;} catch (IOException e) {throw new RuntimeException(e);}
}
诸位要做的是实现两个参数:
- 提供一个 map 集合,用来存放每个单词的计数结果,key 为单词,value 为计数
- 提供一组操作,保证计数的安全性,会传递 map 集合以及单词 List
正确结果输出应该是每个单词出现 200 次
{a=200, b=200, c=200, d=200, e=200, f=200, g=200, h=200, i=200, j=200, k=200, l=200, m=200, n=200, o=200, p=200, q=200, r=200, s=200, t=200, u=200, v=200, w=200, x=200, y=200, z=200}
下面的实现为:
demo_0(() -> new HashMap<String, Integer>(),(map, words) -> {for (String word : words) {Integer counter = map.get(word);int newValue = counter == null ? 1 : counter + 1;map.put(word, newValue);}}
);
显然,输出结果是有问题的
{a=199, b=200, c=197, d=193, e=199, f=199, g=197, h=197, i=200, j=200, k=200, l=199, m=199, n=200, o=197, p=199, q=197, r=197, s=199, t=199, u=199, v=198, w=197, x=188, y=199, z=197}
所谓的线程安全集合是指它们内部的每个方法中代码是原子操作的,但是线程安全集合的多个方法的组合并不是原子操作。
所以这里即使使用的是 ConcurrentHashMap,也得不到想要的结果,具体情况如下所示。
{a=191, b=194, c=199, d=198, e=198, f=194, g=196, h=197, i=198, j=194, k=192, l=194, m=197, n=198, o=196, p=194, q=198, r=198, s=195, t=199, u=197, v=198, w=197, x=196, y=199, z=197}
解决方法一
demo_0(() -> new ConcurrentHashMap<String, LongAdder>(),(map, words) -> {for (String word : words) {// 如果缺少一个 key,则计算产生一个值,然后将 key value 放入其中LongAdder value = map.computeIfAbsent(word, (key) -> new LongAdder());// 执行累加value.increment();}}
);
解决方法二
demo_0(() -> new ConcurrentHashMap<String, Integer>(),(map, words) -> {for (String word : words) {// 函数式编程,无需原子变量map.merge(word, 1, Integer::sum);}}
);
8.2.JDK 7 HashMap 并发死链
关于 HashMap 内部的数据结构的介绍,这里推荐观看这个视频:Java 面试八股文宝典( P39 ~ P55)
关于这个视频的 HashMap 我也做了相关笔记:查找和排序 + 集合 + 单例模式
相关视频:HashMap 知识回顾
8.2.1.测试代码
注意事项
- 要在 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() {@Overridepublic void run() {// 放第 13 个元素, 发生扩容map.put(50, null);System.out.println("扩容后大小[Thread-0]:" + map.size());}}.start();new Thread() {@Overridepublic 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);
}
8.2.2.死链复现
调试工具使用 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, 会进入下一个链表的复制, 但死链已经形成了
8.2.3.源码分析
HashMap 的并发死链发生在扩容时
void transfer (Entry[]newTable,boolean rehash){int newCapacity = newTable.length;// e 是是旧数组中的结点for (Entry<K, V> e : table) {while (null != e) {Entry<K, V> next = e.next; // next 是 e 在旧数组中的下一个结点// 1 处if (rehash) {e.hash = null == e.key ? 0 : hash(e.key);}int i = indexFor(e.hash, newCapacity);// 2 处// 将新元素加入 newTable[i], 原 newTable[i] 作为新元素的 nexte.next = newTable[i]; // e 的下一个节点置为新数组的头结点newTable[i] = e; // 将新数组的头结点的置为 ee = next; // 将 e 置为它在旧数组的下一个结点}}
}
假设 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)已经是死链了
8.2.4.小结
究其原因,是因为在多线程环境下使用了非线程安全的 map 集合
JDK 8 虽然将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序)
但仍不意味着能够在多线程环境下能够安全扩容,还会出现其它问题(如扩容丢数据)
8.2.5.补充_图解流程
以下内容取自我之前写的学习笔记:查找和排序 + 集合 + 单例模式【Java 基础_简单复习】
下图中的 线程 2、线程 1,大致等同于上面举的例子中的 线程 1、线程 0
- e 和 next 都是局部变量,用来指向当前节点和下一个节点
- 线程 1(绿色)的临时变量 e 和 next 刚引用了这俩节点,还未来得及移动节点,发生了线程切换,由线程 2(蓝色)完成扩容和迁移
- 线程 2 扩容完成,由于头插法,链表顺序颠倒。
但线程 1 的临时变量 e 和 next 还引用了这俩节点,还要再来一遍迁移
- 第一次循环
- 循环接着线程切换前运行,注意此时 e 指向的是节点 a,next 指向的是节点 b
- e 头插 a 节点,注意图中画了两份 a 节点,但事实上只有一个(为了不让箭头特别乱画了两份)
- 当循环结束时,e 会指向 next,也就是 b 节点
- 第二次循环
- next 指向了节点 a
- e 头插节点 b
- 当循环结束时,e 指向 next 也就是节点 a
- 第三次循环
- next 指向了 null
- e 头插节点 a,a 的 next 指向了 b(之前 a.next 一直是 null),b 的 next 指向 a,死链已成
- 当循环结束时,e 指向 next 也就是 null,因此第四次循环时会正常退出
8.3.JDK 8 ConcurrentHashMap
8.3.1.重要属性和内部类
参考书籍:《实战 JAVA 高并发程序设计》 葛一鸣 著
ConcurrentHashMap 内部的数据结构
int sizeCtl
:这是一个多功能字段,可以用来记录参与 Map 扩展的线程数量,也可以用来记录新的 table 的扩容阈值counterCells
:用来记录元素的个数。这是一个数组,使用数组来记录是为了避免多线程竞争时可能产生的冲突。使用了数组,在多个线程同时修改数据时,极有可能实际上操作的是数组上的不同的单元。从而减少竞争。Node<K,V>[] table
:实际上存放 Map 内容的地方。一个 Map 实际上就是一个 Node 数组,每个 Node 里都包含了 key 和 value 的信息Node<K,V>[] nextTable
:当 table 需要扩充时,会把新数据填充到 nextTable 中,也就是说 nextTable 就是扩充后的 Map。
ConcurrentHashMap 中最核心的元素是 Node。Map 中的 Node 不一定是 Node 对象,也可能是 TreeBin 或者 ForwardingNode。
在绝大部分的情况下,使用的是 Node。从 Node 的结构不难看出,Node 其实是链表。
可以看到,Node 数组中的每一个元素实际上是链表的头部,这样,当元素的位置发生冲突的时候,不同的元素就可以存放在 Node 数组中的同一个槽位中。
当数组槽位对应的是链表的时候,在链表中查找 key 只能使用简单的遍历,这在数据不多的时候,还是可以接受的。当数据冲突比较多的时候,这种简单的遍历就有点慢了。故在具体实现中,当链表的长度大于且等于 8 的时候,会将链表数化,也就是变成一颗红黑树。如下图所示,其中的一个槽位就变成了一棵树。这就是 TreeBin(在 TreeBin 中使用 TreeNode 构造整棵树)
当数组快满的时候,即超过 75% 的容量的时候,数组还需要进行扩容。在扩容过程中,如果老的数组已经完成了复制,那么就会将老的数组中的元素使用 ForwardingNode 对象代替,表示当前槽位的数据已经处理过了,不需要再处理了。这样,当有多个线程同时参加扩容的时候,就不会发生冲突了。
推荐阅读博客:漫画:什么是红黑树?
// 默认为 0
// 当初始化时, 为 -1
// 当扩容时, 为 -(1 + 扩容线程数)
// 当初始化或扩容完成后,为 下一次的扩容的阈值大小
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> {}
8.3.2.重要方法
// 获取 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)
8.3.3.构造器分析
可以看到实现了 懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建
// initialCapacity:初始容量、loadFactor:负载因子,扩容阈值、concurrencyLevel:并发度
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 binsinitialCapacity = concurrencyLevel; // as estimated threadslong 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;
}
8.3.4.get 流程
参考书籍:《实战 JAVA 高并发程序设计》 葛一鸣 著
get() 方法的工作步骤
- 根据 hash 值得到对应的槽位 (n-1) & h
- 如果当前槽位的第一个元素的 key 就和请求的一样,直接返回
- 否则就调用 Node 的 find() 方法进行查找
- 对于 ForwardingNode 使用的是 ForwardingNode.find() 方法
- 对于红黑树使用的是 TreeBin.find() 方法
- 对于链表型的槽位,依次顺序查找到对应的 key
public V get(Object key) {Node<K, V>[] tab;Node<K, V> e, p;int n, eh;K ek;// spread 方法能确保返回结果是正数int h = spread(key.hashCode());if ((tab = table) != null && (n = tab.length) > 0 &&(e = tabAt(tab, (n - 1) & h)) != null) {// 如果头结点已经是要查找的 keyif ((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;
}
8.3.5.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;// 要创建 tableif (tab == null || (n = tab.length) == 0)// 初始化 table 使用了 cas, 无需 synchronized 创建成功, 进入下一轮循环tab = initTable();// 要创建链表头节点else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// 添加链表头使用了 cas, 无需 synchronizedif (casTabAt(tab, i, null,new Node<K, V>(hash, key, value, null)))break;}// 帮忙扩容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;// 找到相同的 keyif (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 是否已经在树中, 是, 则返回对应的 TreeNodeif ((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;
}
private final Node<K, V>[] initTable() {Node<K, V>[] tab;int sc;while ((tab = table) == null || tab.length == 0) {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;
}
// 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 (// 还没有 counterCellsas == 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();}}
}
8.3.6.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;
}
8.3.7.小结
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[] 当中。最后统计数量时累加即可
源码分析:http://www.importnew.com/28263.html
其它实现:https://github.com/boundary/high-scale-lib
8.4.JDK 7 ConcurrentHashMap
它维护了一个 segment 数组,每个 segment 对应一把锁
- 优点:如果多个线程访问不同的 segment,实际是没有冲突的,这与 JDK 8 中是类似的
- 缺点:Segments 数组默认大小为 16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化
8.4.1.构造器分析
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 = 28this.segmentShift = 32 - sshift;// segmentMask 默认是 15 即 0000 0000 0000 1111this.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;
}
构造完成,如下图所示
(下图的 Segments 数组是 0~15,图太长了,我就省略了后面的情况了)
JDK 7 版本的 ConcurrentHashMap 没有实现懒惰初始化,空间占用不友好
其中 this.segmentShift 和 this.segmentMask 的作用是决定将 key 的 hash 结果匹配到哪个 segment
例如,根据某一 hash 值求 segment 位置,先将高位向低位移动 this.segmentShift 位
结果再与 this.segmentMask 做位于运算,最终得到 1010 即下标为 10 的 segment
8.4.2.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, 是则创建该 segmentif ((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) 创建新 nodenode = 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;
}
8.4.3.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 listnewTable[idx] = e;else { // Reuse consecutive sequence at same slotHashEntry<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 nodenode.setNext(newTable[nodeIndex]);newTable[nodeIndex] = node;// 替换为新的 HashEntry tabletable = 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 扩容为 8map.put(341, "value");map.put(484, "value");map.put(545, "value"); // 8 扩容为 16map.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;
}
8.4.4.get 流程
get 时并未加锁,用了 UNSAFE 方法保证了可见性,扩容过程中,get 先发生就从旧表取内容,get 后发生就从新表取内容
public V get(Object key) {Segment<K, V> s; // manually integrate access methods to reduce overheadHashEntry<K, V>[] tab;int h = hash(key);// u 为 segment 对象在数组中的偏移量long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;// s 即为 segmentif ((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;
}
8.4.5.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 bitslong sum; // sum of modCountslong last = 0L; // previous sumint retries = -1; // first iteration isn't retrytry {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;
}
9.LinkedBlockingQueue
9.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;
9.2.出队操作
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;
9.3.加锁分析
高明之处 在于用了两把锁和 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();
9.4.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) {// 倒过来读就好: 等待 notFullnotFull.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();
}
由 put 唤醒 put 是为了避免信号不足
9.5.take 操作
public E take() throws InterruptedException {E x;int c = -1;final AtomicInteger count = this.count;final ReentrantLock takeLock = this.takeLock;takeLock.lockInterruptibly();try {while (count.get() == 0) {notEmpty.await();}x = dequeue();c = count.getAndDecrement();if (c > 1)notEmpty.signal(); // 消费者自己唤醒其他的消费者} finally {takeLock.unlock();}// 如果队列中只有一个空位时, 叫醒 put 线程// 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacityif (c == capacity)// 这里调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争signalNotFull()return x;
}
9.6.性能比较
这里主要列举的是 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较
- Linked 支持有界,Array 强制有界
- Linked 实现是链表,Array 实现是数组
- Linked 是懒惰的,而 Array 需要提前初始化 Node 数组
- Linked 每次入队会生成新 Node,而 Array 的 Node 是提前创建好的
- Linked 两把锁,Array 一把锁
10.ConcurrentLinkedQueue
ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是
- 两把【锁】,同一时刻,可以允许两个线程同时(一个生产者与一个消费者)执行
- dummy 节点的引入让两把【锁】将来锁住的是不同对象,避免竞争
- 只是这【锁】使用了 CAS 来实现
事实上,ConcurrentLinkedQueue 应用还是非常广泛的
例如之前讲的 Tomcat 的 Connector 结构时
Acceptor 作为生产者向 Poller 消费者传递事件信息时
正是采用了 ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用
11.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;}
}
上面代码块的源码版本是 Java 11,下面代码块的源码版本是 Java 1.8
Java 1.8 中使用的是可重入锁而不是 synchronized。
public boolean add(E e) {final ReentrantLock lock = this.lock;lock.lock();try {Object[] elements = getArray();int len = elements.length;Object[] newElements = Arrays.copyOf(elements, len + 1);newElements[len] = e;setArray(newElements);return true;} finally {lock.unlock();}
}
其它 读操作 并未加锁,例如
public void forEach(Consumer<? super E> action) {Objects.requireNonNull(action);for (Object x : getArray()) {@SuppressWarnings("unchecked") E e = (E) x;action.accept(e);}
}
适合 『读多写少』 的应用场景
get 弱一致性
时间点 | 操作 1 |
---|---|
1 | Thread-0 getArray() |
2 | Thread-1 getArray() |
3 | Thread-1 setArray(arrayCopy) |
4 | Thread-0 array[index] |
不容易测试,但问题确实存在
迭代器弱一致性
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 都是弱一致性的表现
- 并发高和一致性是矛盾的,需要权衡
相关文章:
学习笔记:Java 并发编程⑥_并发工具_JUC
若文章内容或图片失效,请留言反馈。 部分素材来自网络,若不小心影响到您的利益,请联系博主删除。 视频链接:https://www.bilibili.com/video/av81461839配套资料:https://pan.baidu.com/s/1lSDty6-hzCWTXFYuqThRPw&am…...
Linux文件隐藏属性(修改与显示):chattr和lsattr
文件除了基本的九个权限以外还有隐藏属性存在,这些隐藏属性对于系统有很大的帮助,尤其是系统安全(Security)上 chattr(配置文件隐藏属性) chattr 【-】【ASacdistu】文件或目录名称 选项与参数:…...
广东省基层就业补贴
基层就业补贴链接:https://www.gdzwfw.gov.cn/portal/v2/guide/11440309MB2D27065K4440511108001 一.申请条件: 1、劳动者到中小微企业、个体工商户、社会组织等就业,或到乡镇(街道)、村居社会管理和公共服务岗位就业…...
高压放大器在超声导波钢轨传播中的应用
实验名称:高压放大器在超声导波钢轨传播中的应用研究方向:无损检测测试目的:超声导波具有传播距离远、检测距离长的特点,在钢轨无损检测领域受到越来越多的关注。本文使用有限元仿真方法和现场实验方法,对钢轨各模态超…...
Java字符串常见拼接方式
目录 最常见的方式 StringBuilder.append()和StringBuffer.append() String类下的cocat()方法 String类下的join()方法 StringUtils.join 项目中使用 不建议在 for 循环中使用 “” 进行字符串拼接 通过字符串连接,可以将两个或多个字符串、字符、整数和浮点…...
商城业务:购物车
人生在世如身处荆棘之中,心不动,人不妄动,不动则不伤;如心动则人妄动,伤其身痛其骨,于是体会到世间诸般痛苦。 1、购物车需求 1)、需求描述: - 用户可以在登录状态下将商品添加到购…...
计算机网络学习笔记(一)
网络是由若干接点和连接这些结点的链路组成。 多个网络通过路由器互联起来构成覆盖范围更大的互联网。 普通用户通过ISP接入因特网。 基于ISP的三层结构因特网 相隔较远的两台主机间通信可能需要经过多个ISP。 有电路交换,报文交换,分组交换三种交换方…...
【单目标优化算法】烟花优化算法(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
微服务项目【秒杀商品展示及商品秒杀】
登录方式调整 第1步:从zmall-common的pom.xml中移除spring-session-data-redis依赖 注意: 1)本次不采用spring-session方式,改用redis直接存储用户登录信息,主要是为了方便之后的jmeter压测; 2)…...
DIDL3_模型选择、复杂度、过欠拟合的相关概念
模型选择、复杂度、过欠拟合的概念模型选择训练误差和泛化误差验证数据集和测试数据集K-则交叉验证(没有足够多数据时使用)过拟合和欠拟合模型容量模型容量的影响估计模型容量控制模型容量数据复杂度处理过拟合的方法(1)ÿ…...
Android 9.0 去除锁屏界面及SystemUI无sim卡拨打紧急电话控件显示功能实现
1.1概述 在9.0的系统rom定制化开发中,关于SystemUI的定制化功能也是比较多的,在SystemUI的锁屏页面和状态栏提示无sim卡拨打紧急电话控件显示等相关提示 的功能中,在有些systemui的定制中是不需要这些功能的,所以需要从systemui中去掉这些功能提示的,这就需要从systemui中…...
AntDB-M设计之内存结构
亚信科技专注通信行业多年,AntDB数据库从诞生开始,就面对通信级的大数据量应用场景挑战,在性能、稳定性、规模化等方面获得了超过10年的通信核心业务系统验证,性能峰值达到每秒百万的通信核心交易量。AntDB-M(AntDB内存…...
互联网舆情监测公司监测哪些内容,TOOM北京舆情监测公司
互联网舆情监测公司是一种提供舆情监测、分析和管理服务的公司,其业务主要涉及互联网舆情监测、数据分析、报告撰写、危机处理等方面。这些公司通过使用各种技术和工具,帮助客户监测他们在互联网上的声誉和品牌形象,并提供相应的建议和解决方…...
一篇文章带你熟练使用Ansible中的playbook
目录 一、Playbook的功能 二、YAML 1、简介 2、特点 3、语法简介 4、YAML 列表 5、YAML的字典 三、playbook执行命令 四、 Playbook的核心组件 五、vim 设定技巧 练习 一、Playbook的功能 playbook 是由一个或多个play组成的列表 Playboot 文件使用YAML来写的 二、…...
HashedWheelTimer
序言这种算法是一种轮询算法的优化升级,能够以只有一个Timer的情况下处理大量的定时任务.Begin结合HashedWheelTimer的思想根据自然时间1分钟为例,来做大批量的定时任务触发首先定一个长度为60的数组,数组中存放的是Set集合,集合里面是任务详情.当有定时任务刚来的时候判断是否…...
OPenCV库移植到ARM开发板子上面配置过程
步骤一 1,环境准备去下载opencv官方的源码。 我这里用的是opencv-4.5.5版本的 2,还需要交叉编译工具一般,你交叉编译的工具板子厂家会提供工具,最好还是用板子厂家提供的交叉编译工具,因为我之前编译试过其他的交叉…...
Jenkins实现CI/CD
Jenkins是一个开源的持续集成和持续交付(CI/CD)解决方案,它可以自动执行构建、测试和部署等任务,从而简化了开发工作流程。本文将详细介绍如何使用Jenkins实现CI/CD。 首先,您需要安装Jenkins并启动它。您可以通过以下…...
如何给img标签里的请求添加自定义header
是这样的需求,有一个web页面,里面图片的上传和预览来自于一个独立的文件服务器,对http的请求需要进行访问权限的设置,就是在请求的header里加一个Authorization的字段。上传好说我用的Axios直接添加一个header就行了,但…...
Linux系统基本概念操作,用户和文件权限管理
常用快捷键和通配符常用快捷键按键作用Ctrld键盘输入结束或退出终端Ctrls暂停当前程序,暂停后按下任意键恢复运行Ctrlz将当前程序放到后台运行,恢复到前台为命令fgCtrla将光标移至输入行头,相当于Home键Ctrle将光标移至输入行末,相…...
数据库中的单表查询和多表查询
一、单表查询素材: 表名:worker-- 表中字段均为中文,比如 部门号 工资 职工号 参加工作 等 CREATE TABLE worker (部门号 int(11) NOT NULL,职工号 int(11) NOT NULL,工作时间 date NOT NULL,工资 float(8,2) NOT NULL,政治面貌 varchar(10) …...
全网详解MyBatis-Plus LambdaQueryWrapper的使用说明以及LambdaQueryWrapper和QueryWapper的区别
文章目录1. 文章引言2. 代码演示3. 分析LambdaQueryWrapper3.1 引入LambdaQueryWrapper的原因3.2 LambdaQueryWrapper和QueryWapper的区别4. 重要总结1. 文章引言 今天在公司写代码时,发现同事使用LambdaQueryWrapper来查询数据,而我一直习惯使用QueryW…...
暴力破解(new)
数据来源 本文仅用于信息安全的学习,请遵守相关法律法规,严禁用于非法途径。若观众因此作出任何危害网络安全的行为,后果自负,与本人无关。 01 暴力破解介绍及应用场景 》暴力破解介绍 》暴力破解字典 GitHub - k8gege/Passwor…...
Android12之apex调试
1.问题在调试libtinyalsa.so中添加log后,但是发现push so后,却没有log打印,why?2.分析以下为libtinyalsa.so的位置/system/lib64/libtinyalsa.so /system/lib/libtinyalsa.so /apex/com.android.vndk.v31/lib64/libtinyalsa.so /a…...
Python - 数字(Number)数据类型常用操作
目录数字运算类型转换数学函数数学库math、cmathmath 模块常量math 模块方法随机函数库 randomrandom 模块方法保留小数到指定位数三角函数数字运算 :用于给变量赋值type(x):查看数据所属类型isinstance(x, A_tuple):判断数据是否为预期类型…...
QT(51)-动态链接库-windows
1.qt- 调用win32 DLL 2.qt- 调用MFC DLL 0概述: 01.扩展DLL: 必须有一个DllMain()函数,且调用AfxInitExtensionModule()函数。 CRuntimeClass类-初始化函数CDynLinkLibrary。02.windows定位DLL文件: 1)…...
[Vivado那些事儿]将自定义 IP (HDL)添加到 Vivado 模块设计(Block Design)
绪论使用Vivado Block Design设计解决了项目继承性问题,但是还有个问题,不知道大家有没有遇到,就是新设计的自定义 RTL 文件无法快速的添加到Block Design中,一种方式是通过自定义IP,但是一旦设计的文件有问题就需要重…...
开学必备数码清单,大学生开学必备数码好物分享
还有很多小伙伴不知道开学应该准备什么,在学校当中需要用到的数码产品,在宿舍娱乐使用的音箱、学习当中使用到的笔记本,这些都是我们可以准备的,还有一个小众的好物,能够让我们在学校当中提升生活的幸福感,…...
【面试题】常见前端基础面试题(HTML,CSS,JS)
大厂面试题分享 面试题库后端面试题库 (面试必备) 推荐:★★★★★地址:前端面试题库html语义化的理解代码结构: 使页面在没有css的情况下,也能够呈现出好的内容结构有利于SEO: 爬虫根据标签来分配关键字的权重,因此可以和搜索引擎…...
Vue (4)
文章目录1. 绑定样式1.1 绑定 class 样式1.2 绑定 style 样式2. 条件渲染2.1 v-show2.2 v-if3. 列表渲染3.1 v-for3.2 key 的作用与原理3.3 列表过滤3.4 列表排序1. 绑定样式 说 绑定样式 前,先准备好 以下几个 样式 : <style>.basic {width: 400px;height: 1…...
静态库和动态库的制作
一、什么叫做库: 库:二进制的程序,能被操作系统载入内存中执行 二、Linux下的库有两种:静态库和共享库(动态库),二者的不同点在于代码载入的时刻不同。 A、静态库在程序编译的时候并会被连接到目标代码中,程…...
做网络主播网站违法吗/专业关键词优化平台
为什么80%的码农都做不了架构师?>>> 今天mac异常的卡,不小心直接把盘给抹了,反正有小米路由器每天备份么。 但是commandR进入恢复模式就傻眼了,备份的东西不知道去哪里了。想想应该是,MAC在恢复模式根本就…...
嘉兴教育网站建设/pc网站建设和推广
你和你的朋友正在玩棋子跳格子的游戏,而棋盘是一个由n个格子组成的长条,你们两人轮流移动一颗棋子,每次可以选择让棋子跳1-3格,先将棋子移出棋盘的人获得胜利。我们知道你们两人都会采取最优策略,现在已知格子数目&…...
iapp做网站/百度关键词多少钱一个月
原型模式 转载于:https://www.cnblogs.com/zhang-wenbin/p/11445063.html...
wordpress 页脚改颜色/优化营商环境
原标题:一起玩转Android项目中的字节码(Transform篇)作者:Quinn Chenhttp://quinnchen.me/2018/09/13/2018-09-13-asm-transform/作为Android开发,日常写Java代码之余,是否想过,玩玩class文件?直接对class文…...
苏州seo建站/qq群排名优化
一、HTML5的data 属性,本质就是:一个用于保存数据的自定义属性。它们总是以 data- 作为前缀,后面跟随着描述性的(只允许小写字母和连接字符-hyphens)。 一个元素可以有任意数量的 data 属性。 <li data-id"1…...
金融行业网站制作/卢松松外链工具
return [// 默认使用的数据库连接配置default > env(database.driver, mysql),// 自定义时间查询规则time_query_rule > [],// 自动写入时间戳字段// true为自动识别类型 false关闭// 字符串则明确指定时间字段类型 支持 int timestamp datetime dateauto_timestamp >…...