当前位置: 首页 > news >正文

美工网站模板戴尔电脑网站建设方案范文

美工网站模板,戴尔电脑网站建设方案范文,网站个人备案流程,html静态网站模板简单单页若文章内容或图片失效#xff0c;请留言反馈。 部分素材来自网络#xff0c;若不小心影响到您的利益#xff0c;请联系博主删除。 视频链接#xff1a;https://www.bilibili.com/video/av81461839配套资料#xff1a;https://pan.baidu.com/s/1lSDty6-hzCWTXFYuqThRPw请留言反馈。 部分素材来自网络若不小心影响到您的利益请联系博主删除。 视频链接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 本章内容概览 线程池 ThreadPoolExecutorFork/Join JUC LockSemaphoreCountdownLatchCyclicBarrierConcurrentHashMapConcurrentLinkedQueueBlockingQueueCopyOnWriteArrayList 参考书籍《实战 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直译的话就是抽象队列同步器。 早期程序员会自己通过一种同步器去实现另一种相近的同步器例如用可重入锁去实现信号量或反之。 这显然不够优雅于是在 JSR166Java 规范提案中创建了 AQS提供了这种通用的同步器机制。 参考书籍《实战 JAVA 高并发程序设计》 葛一鸣 著 重入锁ReentrantLock和信号量Semaphore是两个极其重要的并发控制工具。它们都在自己的内部实现了一个叫 AbstractQueuedSynchronizer 的子类。子类的名字都是 Sync该类正是重入锁和信号量的核心实现。Sync 类中的代码比较少其核心算法都由 AbstractQueuedSynchronizer 实现。 特点 用 state 属性来表示资源的状态分独占模式和共享模式子类需要定义如何维护这个状态控制如何获取锁和释放锁 getState获取 state 状态setState设置 state 状态compareAndSetStateCAS 机制设置 state 状态独占模式是只有一个线程能够访问资源而共享模式可以允许多个线程访问资源 提供了基于 FIFO 的等待队列类似于 Monitor 的 EntryList不过 EntryList 实际上是非公平竞争条件变量来实现等待、唤醒机制支持多个条件变量类似于 Monitor 的 WaitSet AbstractQueuedSynchronizer 子类主要实现这样一些方法默认抛出 UnsupportedOperationException tryAcquiretryReleasetryAcquireSharedtryReleaseSharedisHeldExclusively 获取锁的姿势 // 如果获取锁失败 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() 方法引起阻塞的线程。由于一个重入锁可以生成多个条件变量对象因此一个重入锁可以生成多个条件变量对象一次一个重入锁可能有多个条件变量等待队列。实际上每个条件变量对象的内部都维护了一个等待队列。其内部逻辑结构和代码层面的具体实现如下面两张图所示。 AQS 内部逻辑结构图AQS 实现类图可以看到无论是等待队列还是条件变量等待队列都使用同一个 Node 类作为链表的节点。 对于同步等待队列Node 中包括链表的上一个元素 prev、下一个元素 next 和线程对象 thread。 对于条件变量等待队列还使用了 nextWaiter 表示下一个在条件变量队列中等待的节点。 Node 节点的另一个重要成员是 waitStatus表示节点在队列中的等待状态。 CANCELLED表示线程取消了等待。 如果在获得锁的过程中发生了一些异常则可能出现取消等待的情况。 比如等待过程中出现了中断异常或者是出现了 timeout。SIGNAL表示后续节点需要被唤醒。CONDITION线程在条件变量等待队列中等待。PROPAGATE在共享模式下无条件传播 releaseShared 状态。 早期的 JDK 版本是没有这个状态的。引入这个状态是为了解决由共享锁并发释放导致线程挂起的 BUG 680120。0初始状态 其中CANCELLED 1SIGNAL -1CONDITION -2PROPAGATE -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 打断 队列设计 使用了 FIFO 先入先出队列并不支持优先级队列设计时借鉴了 CLH 队列 队列中有 head 和 tail 两个指针节点都用 volatile 修饰配合 CAS 使用每个节点有 state 维护节点状态 入队伪代码只需要考虑 tail 赋值的原子性 do {// 原来的 tailNode prev tail;// 用 cas 在原来 tail 的基础上改为 node } while(tail.compareAndSet(prev, node))出队伪代码 // prev 是上一个节点 while((Node prevnode.prev).state ! 唤醒状态) { }// 设置头节点 head node;CLH 的好处无锁使用自旋快速无阻塞 下方图片的来源 https://javaguide.cn/java/concurrent/aqs.html#AQS 核心思想 CLH 队列结构图下方图片的来源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 操作挂起当前线程直到被唤醒// 唤醒后检查当前线程是否被中断返回该线程中断状态并重置中断状态返回 falseparkAndCheckInterrupt())// 如果出现了中断那么中断信息是不可以丢失的必须要保存起来// 如果是因为 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 dont 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(); }进入 parkAndCheckInterruptThread-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 为 nullstate 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-1state 1head 指向刚刚 Thread-1 所在的 Node该 Node 清空 Thread原本的 head 因为从链表断开而可被垃圾回收 2.1.4.解锁竞争失败流程 如果这时候有其它线程来竞争非公平的体现 例如这时有 Thread-4 来了这个 Thread-4 不在等待队列里 如果不巧又被 Thread-4 占了先 Thread-4 被设置为 exclusiveOwnerThreadstate 1Thread-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()) {// stateint 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 为哨兵节点不对应线程状态为 0if (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()) {// stateint 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()通知目标线程中断设置线程中断的标志位如果被打断线程正在 sleepwaitjoin    会导致被打断的线程抛出 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 状态为 -2Node.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 改为 0Thread-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 GenericCachedDaoT {// HashMap 作为缓存非线程安全, 需要保护HashMapSqlPair, 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(ClassT 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.SIGNALshouldParkAfterFailedAcquire(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 就像是获得了停车位然后停车场显示空余车位减一 刚开始permitsstate为 3这时 5 个线程来获取资源 假设其中 Thread-1Thread-2Thread-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.javaCountDownLatch 在内部封装了一个同步器 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 MapString, Object order(PathVariable int id) {HashMapString, Object map new HashMap();map.put(id, id);map.put(total, 2300.00);sleep(2000);return map;}GetMapping(/product/{id})public MapString, Object product(PathVariable int id) {HashMapString, 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 MapString, Object logistics(PathVariable int id) {HashMapString, 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);FutureMapString, Object f1 service.submit(() - {MapString, Object r restTemplate.getForObject(http://localhost:8080/order/{1}, Map.class, 1);return r;});FutureMapString, Object f2 service.submit(() - {MapString, Object r restTemplate.getForObject(http://localhost:8080/product/{1}, Map.class, 1);return r;});FutureMapString, Object f3 service.submit(() - {MapString, Object r restTemplate.getForObject(http://localhost:8080/product/{1}, Map.class, 2);return r;});FutureMapString, Object f4 service.submit(() - {MapString, 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 {total2300.00, id1} {price300, name小爱音箱, id1} {price2000, name小米手机, id2} {name中通快递, id1} 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.线程安全集合类概述 视频链接线程安全集合类_概述 线程安全集合类可以分为三大类 遗留的线程安全集合如 HashtableVector使用 Collections 装饰的线程安全集合如 Collections.synchronizedCollectionCollections.synchronizedListCollections.synchronizedMapCollections.synchronizedSetCollections.synchronizedNavigableMapCollections.synchronizedNavigableSetCollections.synchronizedSortedMapCollections.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;ListString 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(SupplierMapString, V supplier, BiConsumerMapString, V, ListString consumer) {MapString, V counterMap supplier.get();ListThread ts new ArrayList();for (int i 1; i 26; i) {int idx i;Thread thread new Thread(() - {ListString 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 ListString readFromFile(int i) {ArrayListString 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 次 {a200, b200, c200, d200, e200, f200, g200, h200, i200, j200, k200, l200, m200, n200, o200, p200, q200, r200, s200, t200, u200, v200, w200, x200, y200, z200} 下面的实现为 demo_0(() - new HashMapString, Integer(),(map, words) - {for (String word : words) {Integer counter map.get(word);int newValue counter null ? 1 : counter 1;map.put(word, newValue);}} );显然输出结果是有问题的 {a199, b200, c197, d193, e199, f199, g197, h197, i200, j200, k200, l199, m199, n200, o197, p199, q197, r197, s199, t199, u199, v198, w197, x188, y199, z197}所谓的线程安全集合是指它们内部的每个方法中代码是原子操作的但是线程安全集合的多个方法的组合并不是原子操作。 所以这里即使使用的是 ConcurrentHashMap也得不到想要的结果具体情况如下所示。 {a191, b194, c199, d198, e198, f194, g196, h197, i198, j194, k192, l194, m197, n198, o196, p194, q198, r198, s195, t199, u197, v198, w197, x196, y199, z197}解决方法一 demo_0(() - new ConcurrentHashMapString, LongAdder(),(map, words) - {for (String word : words) {// 如果缺少一个 key则计算产生一个值然后将 key value 放入其中LongAdder value map.computeIfAbsent(word, (key) - new LongAdder());// 执行累加value.increment();}} );解决方法二 demo_0(() - new ConcurrentHashMapString, 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 HashMapInteger, Integer map new HashMapInteger, 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.length32 (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 行加断点 EntryK,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 (EntryK, V e : table) {while (null ! e) {EntryK, 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 指向的是节点 anext 指向的是节点 be 头插 a 节点注意图中画了两份 a 节点但事实上只有一个为了不让箭头特别乱画了两份当循环结束时e 会指向 next也就是 b 节点 第二次循环 next 指向了节点 ae 头插节点 b当循环结束时e 指向 next 也就是节点 a 第三次循环 next 指向了 nulle 头插节点 aa 的 next 指向了 b之前 a.next 一直是 nullb 的 next 指向 a死链已成当循环结束时e 指向 next 也就是 null因此第四次循环时会正常退出 8.3.JDK 8 ConcurrentHashMap 8.3.1.重要属性和内部类 参考书籍《实战 JAVA 高并发程序设计》 葛一鸣 著 ConcurrentHashMap 内部的数据结构 int sizeCtl这是一个多功能字段可以用来记录参与 Map 扩展的线程数量也可以用来记录新的 table 的扩容阈值counterCells用来记录元素的个数。这是一个数组使用数组来记录是为了避免多线程竞争时可能产生的冲突。使用了数组在多个线程同时修改数据时极有可能实际上操作的是数组上的不同的单元。从而减少竞争。NodeK,V[] table实际上存放 Map 内容的地方。一个 Map 实际上就是一个 Node 数组每个 Node 里都包含了 key 和 value 的信息NodeK,V[] nextTable当 table 需要扩充时会把新数据填充到 nextTable 中也就是说 nextTable 就是扩充后的 Map。 ConcurrentHashMap 中最核心的元素是 Node。Map 中的 Node 不一定是 Node 对象也可能是 TreeBin 或者 ForwardingNode。 ConcurrentHashMap 的内部数据结构在绝大部分的情况下使用的是 Node。从 Node 的结构不难看出Node 其实是链表。 ConcurrentHashMap 内部的 Node 数组结构可以看到Node 数组中的每一个元素实际上是链表的头部这样当元素的位置发生冲突的时候不同的元素就可以存放在 Node 数组中的同一个槽位中。 当数组槽位对应的是链表的时候在链表中查找 key 只能使用简单的遍历这在数据不多的时候还是可以接受的。当数据冲突比较多的时候这种简单的遍历就有点慢了。故在具体实现中当链表的长度大于且等于 8 的时候会将链表数化也就是变成一颗红黑树。如下图所示其中的一个槽位就变成了一棵树。这就是 TreeBin在 TreeBin 中使用 TreeNode 构造整棵树 当冲突元素较多时可以用红黑树来存储元素可加速查找当数组快满的时候即超过 75% 的容量的时候数组还需要进行扩容。在扩容过程中如果老的数组已经完成了复制那么就会将老的数组中的元素使用 ForwardingNode 对象代替表示当前槽位的数据已经处理过了不需要再处理了。这样当有多个线程同时参加扩容的时候就不会发生冲突了。 推荐阅读博客漫画什么是红黑树 // 默认为 0 // 当初始化时, 为 -1 // 当扩容时, 为 -(1 扩容线程数) // 当初始化或扩容完成后为 下一次的扩容的阈值大小 private transient volatile int sizeCtl;// 整个 ConcurrentHashMap 就是一个 Node[] static class NodeK,V implements Map.EntryK,V {}// hash 表 transient volatile NodeK,V[] table;// 扩容时的 新 hash 表 private transient volatile NodeK,V[] nextTable;// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点 static final class ForwardingNodeK,V extends NodeK,V {}// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node static final class ReservationNodeK,V extends NodeK,V {}// 作为 treebin 的头节点, 存储 root 和 first static final class TreeBinK,V extends NodeK,V {}// 作为 treebin 的节点, 存储 parent, left, right static final class TreeNodeK,V extends NodeK,V {}8.3.2.重要方法 // 获取 Node[] 中第 i 个 Node static final K,V NodeK,V tabAt(NodeK,V[] tab, int i)// cas 修改 Node[] 中第 i 个 Node 的值, c 为旧值, v 为新值 static final K,V boolean casTabAt(NodeK,V[] tab, int i, NodeK,V c, NodeK,V v)// 直接修改 Node[] 中第 i 个 Node 的值, v 为新值 static final K,V void setTabAt(NodeK,V[] tab, int i, NodeK,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) {NodeK, V[] tab;NodeK, 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 (NodeK, V[] tab table; ; ) {// f 是链表头节点// fh 是链表头结点的 hash// i 是链表在 table 中的下标NodeK, 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 NodeK, 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 (NodeK, 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;}NodeK, V pred e;// 已经是最后的节点了, 新增 Node, 追加至链表尾if ((e e.next) null) {pred.next new NodeK, V(hash, key, value, null);break;}}}// 红黑树else if (f instanceof TreeBin) {NodeK, V p;binCount 2;// putTreeVal 会看 key 是否已经在树中, 是, 则返回对应的 TreeNodeif ((p ((TreeBinK, 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 NodeK, V[] initTable() {NodeK, V[] tab;int sc;while ((tab table) null || tab.length 0) {if ((sc sizeCtl) 0)Thread.yield();// 尝试将 sizeCtl 设置为 -1表示初始化 tableelse 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;NodeK, V[] nt (NodeK, 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) {NodeK, 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 计算实际发生在 putremove 改变集合元素的操作之中 没有竞争发生向 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]SegmentK, V s0 new SegmentK, V(loadFactor, (int) (cap * loadFactor),(HashEntryK, V[]) new HashEntry[cap]);SegmentK, V[] ss (SegmentK, 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) {SegmentK, V s;if (value null)throw new NullPointerException();int hash hash(key);// 计算出 segment 下标int j (hash segmentShift) segmentMask;// 获得 segment 对象, 判断是否为 null, 是则创建该 segmentif ((s (SegmentK, 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) {// 尝试加锁HashEntryK, V node tryLock() ? null :// 如果不成功, 进入 scanAndLockForPut 流程// 如果是多核 cpu 最多 tryLock 64 次, 进入 lock 流程// 在尝试期间, 还可以顺便看该节点在链表中有没有, 如果没有顺便创建出来scanAndLockForPut(key, hash, value);// 执行到这里 segment 已经被成功加锁, 可以安全执行V oldValue;try {HashEntryK, V[] tab table;int index (tab.length - 1) hash;HashEntryK, V first entryAt(tab, index);for (HashEntryK, 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 HashEntryK, 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(HashEntryK, V node) {HashEntryK, V[] oldTable table;int oldCapacity oldTable.length;int newCapacity oldCapacity 1;threshold (int) (newCapacity * loadFactor);HashEntryK, V[] newTable (HashEntryK, V[]) new HashEntry[newCapacity];int sizeMask newCapacity - 1;for (int i 0; i oldCapacity; i) {HashEntryK, V e oldTable[i];if (e ! null) {HashEntryK, V next e.next;int idx e.hash sizeMask;if (next null) // Single node on listnewTable[idx] e;else { // Reuse consecutive sequence at same slotHashEntryK, V lastRun e;int lastIdx idx;// 过一遍链表, 尽可能把 rehash 后 idx 不变的节点重用for (HashEntryK, V last next;last ! null;last last.next) {int k last.hash sizeMask;if (k ! lastIdx) {lastIdx k;lastRun last;}}newTable[lastIdx] lastRun;// 剩余节点需要新建for (HashEntryK, V p e; p ! lastRun; p p.next) {V v p.value;int h p.hash;int k h sizeMask;HashEntryK, V n newTable[k];newTable[k] new HashEntryK, 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) {ConcurrentHashMapInteger, 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) {SegmentK, V s; // manually integrate access methods to reduce overheadHashEntryK, V[] tab;int h hash(key);// u 为 segment 对象在数组中的偏移量long u (((h segmentShift) segmentMask) SSHIFT) SBASE;// s 即为 segmentif ((s (SegmentK, V) UNSAFE.getObjectVolatile(segments, u)) ! null (tab s.table) ! null) {for (HashEntryK, V e (HashEntryK, 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 SegmentK, 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 isnt 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) {SegmentK, 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 LinkedBlockingQueueE extends AbstractQueueEimplements BlockingQueueE, java.io.Serializable {static class NodeE {E item;/*** 下列三种情况之一* - 真正的后继节点* - 自己, 发生在出队时* - null, 表示是没有后继节点, 是最后了*/NodeE next;Node(E x) {item x;}} }初始化链表 last head new NodeE(null); Dummy 节点用来占位item 为 null 当一个节点入队 last last.next node; 再来一个节点入队 last last.next node; 9.2.出队操作 NodeE h head; NodeE 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;NodeE node new NodeE(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 弱一致性 时间点操作 11Thread-0 getArray()2Thread-1 getArray()3Thread-1 setArray(arrayCopy)4Thread-0 array[index]不容易测试但问题确实存在 迭代器弱一致性 CopyOnWriteArrayListInteger list new CopyOnWriteArrayList(); list.add(1); list.add(2); list.add(3); IteratorInteger iter list.iterator(); new Thread(() - {list.remove(0);System.out.println(list); }).start(); sleep1s(); while (iter.hasNext()) {System.out.println(iter.next()); }不要觉得弱一致性就不好 数据库的 MVCC 都是弱一致性的表现并发高和一致性是矛盾的需要权衡
http://www.w-s-a.com/news/838375/

相关文章:

  • 网站建设与管理网络推广的优点
  • 美食网站的设计与制作做网站的电销话术
  • 中国档案网站建设现状研究陕西建设厅执业资格注册中心网站
  • 网站建设的内容管理怎么用ps切片在dw里做网站
  • 建设婚恋网站用什么搭建涿州网站开发
  • 做知识内容的网站与app哈尔滨哪里有做网站的
  • 青岛企业网站建站模板百度网站建设推广
  • 做360网站中保存的图片存在哪里个人建立网站要多少钱
  • 网站安装部署无锡做网站的公司
  • 怎么将网站做成小程序安装wordpress到服务器
  • 企业网站建设的四大因素沈阳网站建设招标公司
  • wordpress仿站开发公司网站策划宣传
  • 金乡县网站开发网站开发三个流程
  • qq空间网站是多少纺织网站建设方案
  • 建设微网站项目报告网站优化难吗
  • 做网站需要自己上传产品吗企业网站系统设计
  • wordpress个人中心济南网站建设和优化
  • 网站pc端网址和手机端网址建设牡丹江 网站建设
  • 苏州新区城乡建设网站人才招聘网站开发
  • 一般网站是怎么做的威远移动网站建设
  • 赣州网站开发公司怎么才能设计好一个网站
  • 个人网站建设分几个步走培训网站开发哪个好
  • 智能网站价格河北城乡建设网站
  • 做动画在线观看网站网上花店 网站源代码
  • 做网站项目体会商业信息
  • 深圳的设计网站谷歌浏览器下载手机版官网
  • 苏州网站建设都找全网天下外贸响应式网站设计
  • 揭阳专业做网站网站迁移教材
  • 手机上怎么上传网站吗工程信息网站建设
  • 用手机建网站微信手机网站流程