├── 01.并发编程的优缺点 └── 并发编程的优缺点.md ├── 02.线程的状态转换以及基本操作 ├── futureTask接口实现关系.png ├── 中断线程方法.png ├── 线程状态.png ├── 线程状态转换以及基本操作.md └── 线程状态转换关系.png ├── 03.java内存模型以及happens-before规则 ├── JMM内存结构抽象结构示意图.png ├── JMM层级图.png ├── JMM设计示意图.png ├── Java内存模型以及happens-before.md ├── happens-before与JMM的关系.png └── 从源码到最终执行的指令序列的示意图.png ├── 04.彻底理解synchronized ├── Mark Word存储结构.png ├── Mark Word状态变化.png ├── java关键字---synchronized.md ├── synchronizedDemo.class.png ├── synchronized的happens-before关系.png ├── synchronized的使用场景.png ├── 偏向锁撤销流程.png ├── 偏向锁获取和撤销流程.png ├── 各种锁的对比.png ├── 对象,对象监视器,同步队列和线程状态的关系.png ├── 线程A写共享变量.png ├── 线程B读共享变量.png └── 轻量级锁加锁解锁以及锁膨胀.png ├── 05.彻底理解volatile ├── VolatileExample的happens-before关系推导.png ├── java关键字---volatile.md ├── volatile写插入内存屏障示意图.png ├── volatile读插入内存屏障示意图.png ├── volatile重排序规则表.png ├── 内存屏障分类表.png ├── 线程A执行volatile写后的内存状态图.png └── 线程B读volatile后的内存状态图.png ├── 06.你以为你真的了解final吗? ├── final修饰局部变量.png ├── final修饰成员变量.png ├── final域写可能的存在的执行时序.png ├── final域引用可能的执行时序.png ├── final域读可能存在的执行时序.png ├── final方法不能重写.png ├── final类不能继承.png ├── java关键字--final.md └── 写final修饰引用类型数据可能的执行时序.png ├── 07.三大性质总结:原子性、可见性以及有序性 ├── 三大性质总结:原子性、可见性以及有序性.md └── 不加volatile可能的执行时序.png ├── 08.初识Lock与AbstractQueuedSynchronizer(AQS) ├── AQS可重写的方法.png ├── AQS提供的模板方法.png ├── concurrent包实现整体示意图.png ├── concurrent目录结构.png ├── mutex的执行情况.png └── 初识Lock与AbstractQueuedSynchronizer(AQS).md ├── 09.深入理解AbstractQueuedSynchronizer(AQS) ├── LockDemo debug下.png ├── banner.jpg ├── 当前节点引用线程获取锁,当前节点设置为队列头结点.png ├── 深入理解AbstractQueuedSynchronizer(AQS).md ├── 独占式锁获取(acquire()方法)流程图.png ├── 自旋获取锁整体示意图.png ├── 超时等待式获取锁(doAcquireNanos()方法).png └── 队列示意图.png ├── 10.彻底理解ReentrantLock └── 彻底理解ReentrantLock.md ├── 11.深入理解读写锁ReentrantReadWriteLock ├── 深入理解读写锁ReentrantReadWriteLock.md └── 读写锁的读写状态设计.png ├── 12.详解Condition的await和signal等待通知机制 ├── AQS持有多个Condition.png ├── await方法示意图.png ├── condition下的等待通知机制.png ├── debug模式下情景图.png ├── signal执行示意图.png ├── 等待队列的示意图.png └── 详解Condition的await和signal等待通知机制.md ├── 13.LockSupport工具 └── LockSupport工具.md ├── 14.并发容器之ConcurrentHashMap(JDK 1.8版本) ├── ConcurrentHashMap扩容示意图.png ├── ConcurrentHashMap散列桶数组结构示意图.png └── 并发容器之ConcurrentHashMap(JDK 1.8版本).md ├── 15.并发容器之ConcurrentLinkedQueue ├── ConcurrentLinkedQueue初始化状态.png ├── Node2从队列中出队后的状态.png ├── offer 1后队列的状态.png ├── offer和poll相互影响分析时队列初始状态.png ├── 并发容器之ConcurrentLinkedQueue.md ├── 线程A和线程B可能存在的执行时序.png ├── 线程A和线程B有可能的执行时序.png ├── 线程B进行poll后队列的状态图.png ├── 经过一次循环后的状态.png ├── 队列offer 2后的状态.png ├── 队列出队操作后的状态.png ├── 队列初始状态.png └── 队列进行入队操作后casTail失败后的状态图.png ├── 16.并发容器之CopyOnWriteArrayList ├── 并发容器之CopyOnWriteArrayList.md └── 最终一致性的分析.png ├── 17.并发容器之ThreadLocal ├── ThreadLocal各引用间的关系.png ├── 分离链表法示意图.gif ├── 并发容器之ThreadLocal.md ├── 开放定址法示意图.jpg └── 理想散列表的一个示意图.png ├── 18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题 ├── 1.2情况示意图.png ├── cleanSomeSlots执行情景图.png ├── cleanSomeSlots示意图 (2).png ├── cleanSomeSlots示意图.png ├── threadLocal引用示意图.png ├── 一篇文章,从源码深入详解ThreadLocal内存泄漏问题.md ├── 前向未搜索到脏entry,后向环形搜索到可覆盖的entry.png ├── 前向环形搜索到脏entry,向后环形未搜索可覆盖entry.png ├── 前向环形未搜索到脏entry,后向环形查找未查找到可覆盖的entry.png └── 向前环形搜索到脏entry,向后环形查找到可覆盖的entry的情况.png ├── 18362989059.pdf ├── 19.并发容器之BlockingQueue ├── BlockingQueue和BlockingDeque的区别.png ├── BlockingQueue基本操作.png ├── LinkedBlockingDeque的基本操作.png └── 并发容器之BlockingQueue.md ├── 20.并发容器之ArrayBlockingQueue和LinkedBlockingQueue实现原理详解 └── 并发容器之ArrayBlockingQueue和LinkedBlockingQueue实现原理详解.md ├── 21.线程池ThreadPoolExecutor实现原理 ├── execute执行过程示意图.jpg ├── 线程池ThreadPoolExecutor实现原理.md └── 线程池执行流程图.jpg ├── 22.线程池之ScheduledThreadPoolExecutor ├── ScheduledThreadPoolExecutor类的UML图.png └── 线程池之ScheduledThreadPoolExecutor.md ├── 23.FutureTask基本操作总结 ├── FutureTask基本操作总结.md ├── FutureTask状态迁移图.jpg └── FutureTask的get和cancel的执行示意图.jpg ├── 24.Java中atomic包中的原子操作类总结 └── Java中atomic包中的原子操作类总结.md ├── 25.大白话说java并发工具类-CountDownLatch,CyclicBarrier ├── CyclicBarrier执行示意图.jpg └── 大白话说java并发工具类-CountDownLatch,CyclicBarrier.md ├── 26.大白话说java并发工具类-Semaphore,Exchanger └── 大白话说java并发工具类-Semaphore,Exchanger.md ├── 27.一篇文章,让你彻底弄懂生产者--消费者问题 └── 一篇文章,让你彻底弄懂生产者--消费者问题.md ├── Java并发知识图谱.png ├── README.md └── 高级测试工程师-18362989059.pdf /01.并发编程的优缺点/并发编程的优缺点.md: -------------------------------------------------------------------------------- 1 | 一直以来并发编程对于刚入行的小白来说总是觉得高深莫测,于是乎,就诞生了想写点东西记录下,以提升理解和堆并发编程的认知。为什么需要用的并发?凡事总有好坏两面,之间的trade-off是什么,也就是说并发编程具有哪些缺点?以及在进行并发编程时应该了解和掌握的概念是什么?这篇文章主要以这三个问题来谈一谈。 2 | # 1. 为什么要用到并发 # 3 | 一直以来,硬件的发展极其迅速,也有一个很著名的"摩尔定律",可能会奇怪明明讨论的是并发编程为什么会扯到了硬件的发展,这其中的关系应该是多核CPU的发展为并发编程提供的硬件基础。摩尔定律并不是一种自然法则或者是物理定律,它只是基于认为观测数据后,对未来的一种预测。按照所预测的速度,我们的计算能力会按照指数级别的速度增长,不久以后会拥有超强的计算能力,正是在畅想未来的时候,2004年,Intel宣布4GHz芯片的计划推迟到2005年,然后在2004年秋季,Intel宣布彻底取消4GHz的计划,也就是说摩尔定律的有效性超过了半个世纪戛然而止。但是,聪明的硬件工程师并没有停止研发的脚步,他们为了进一步提升计算速度,而不是再追求单独的计算单元,而是将多个计算单元整合到了一起,也就是形成了多核CPU。短短十几年的时间,家用型CPU,比如Intel i7就可以达到4核心甚至8核心。而专业服务器则通常可以达到几个独立的CPU,每一个CPU甚至拥有多达8个以上的内核。因此,摩尔定律似乎在CPU核心扩展上继续得到体验。因此,多核的CPU的背景下,催生了并发编程的趋势,通过**并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升**。 4 | 5 | 顶级计算机科学家Donald Ervin Knuth如此评价这种情况:在我看来,这种现象(并发)或多或少是由于硬件设计者无计可施了导致的,他们将摩尔定律的责任推给了软件开发者。 6 | 7 | 另外,在特殊的业务场景下先天的就适合于并发编程。比如在图像处理领域,一张1024X768像素的图片,包含达到78万6千多个像素。即时将所有的像素遍历一边都需要很长的时间,面对如此复杂的计算量就需要充分利用多核的计算的能力。又比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。**面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分** 。正是因为这些优点,使得多线程技术能够得到重视,也是一名CS学习者应该掌握的: 8 | 9 | - 充分利用多核CPU的计算能力; 10 | - 方便进行业务拆分,提升应用性能 11 | # 2. 并发编程有哪些缺点 # 12 | 多线程技术有这么多的好处,难道就没有一点缺点么,就在任何场景下就一定适用么?很显然不是。 13 | 14 | 15 | ## 2.1 频繁的上下文切换 ## 16 | 17 | 时间片是CPU分配给各个线程的时间,因为时间非常短,所以CPU不断通过切换线程,让我们觉得多个线程是同时执行的,时间片一般是几十毫秒。而每次切换时,需要保存当前的状态起来,以便能够进行恢复先前状态,而这个切换时非常损耗性能,过于频繁反而无法发挥出多线程编程的优势。通常减少上下文切换可以采用无锁并发编程,CAS算法,使用最少的线程和使用协程。 18 | 19 | - 无锁并发编程:可以参照concurrentHashMap锁分段的思想,不同的线程处理不同段的数据,这样在多线程竞争的条件下,可以减少上下文切换的时间。 20 | 21 | - CAS算法,利用Atomic下使用CAS算法来更新数据,使用了乐观锁,可以有效的减少一部分不必要的锁竞争带来的上下文切换 22 | - 使用最少线程:避免创建不需要的线程,比如任务很少,但是创建了很多的线程,这样会造成大量的线程都处于等待状态 23 | 24 | - 协程:在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换 25 | 26 | 由于上下文切换也是个相对比较耗时的操作,所以在"java并发编程的艺术"一书中有过一个实验,并发累加未必会比串行累加速度要快。 可以使用**Lmbench3测量上下文切换的时长** **vmstat测量上下文切换次数** 27 | 28 | 29 | ## 2.2 线程安全 ## 30 | 31 | 多线程编程中最难以把握的就是临界区线程安全问题,稍微不注意就会出现死锁的情况,一旦产生死锁就会造成系统功能不可用。 32 | 33 | 34 | public class DeadLockDemo { 35 | private static String resource_a = "A"; 36 | private static String resource_b = "B"; 37 | 38 | public static void main(String[] args) { 39 | deadLock(); 40 | } 41 | 42 | public static void deadLock() { 43 | Thread threadA = new Thread(new Runnable() { 44 | @Override 45 | public void run() { 46 | synchronized (resource_a) { 47 | System.out.println("get resource a"); 48 | try { 49 | Thread.sleep(3000); 50 | synchronized (resource_b) { 51 | System.out.println("get resource b"); 52 | } 53 | } catch (InterruptedException e) { 54 | e.printStackTrace(); 55 | } 56 | } 57 | } 58 | }); 59 | Thread threadB = new Thread(new Runnable() { 60 | @Override 61 | public void run() { 62 | synchronized (resource_b) { 63 | System.out.println("get resource b"); 64 | synchronized (resource_a) { 65 | System.out.println("get resource a"); 66 | } 67 | } 68 | } 69 | }); 70 | threadA.start(); 71 | threadB.start(); 72 | 73 | } 74 | } 75 | 76 | 在上面的这个demo中,开启了两个线程threadA, threadB,其中threadA占用了resource_a, 并等待被threadB释放的resource _b。threadB占用了resource _b正在等待被threadA释放的resource _a。因此threadA,threadB出现线程安全的问题,形成死锁。同样可以通过jps,jstack证明这种推论: 77 | 78 | "Thread-1": 79 | waiting to lock monitor 0x000000000b695360 (object 0x00000007d5ff53a8, a java.lang.String), 80 | which is held by "Thread-0" 81 | "Thread-0": 82 | waiting to lock monitor 0x000000000b697c10 (object 0x00000007d5ff53d8, a java.lang.String), 83 | which is held by "Thread-1" 84 | 85 | Java stack information for the threads listed above: 86 | =================================================== 87 | "Thread-1": 88 | at learn.DeadLockDemo$2.run(DeadLockDemo.java:34) 89 | - waiting to lock <0x00000007d5ff53a8(a java.lang.String) 90 | - locked <0x00000007d5ff53d8(a java.lang.String) 91 | at java.lang.Thread.run(Thread.java:722) 92 | "Thread-0": 93 | at learn.DeadLockDemo$1.run(DeadLockDemo.java:20) 94 | - waiting to lock <0x00000007d5ff53d8(a java.lang.String) 95 | - locked <0x00000007d5ff53a8(a java.lang.String) 96 | at java.lang.Thread.run(Thread.java:722) 97 | 98 | Found 1 deadlock. 99 | 100 | 101 | 如上所述,完全可以看出当前死锁的情况。 102 | 103 | 那么,通常可以用如下方式避免死锁的情况: 104 | 105 | 1. 避免一个线程同时获得多个锁; 106 | 2. 避免一个线程在锁内部占有多个资源,尽量保证每个锁只占用一个资源; 107 | 3. 尝试使用定时锁,使用lock.tryLock(timeOut),当超时等待时当前线程不会阻塞; 108 | 4. 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况 109 | 110 | 所以,如何正确的使用多线程编程技术有很大的学问,比如如何保证线程安全,如何正确理解由于JMM内存模型在原子性,有序性,可见性带来的问题,比如数据脏读,DCL等这些问题(在后续篇幅会讲述)。而在学习多线程编程技术的过程中也会让你收获颇丰。 111 | 112 | 113 | # 3. 应该了解的概念 # 114 | ## 3.1 同步VS异步 ## 115 | 同步和异步通常用来形容一次方法调用。同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。比如,在超时购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知去取就好。 116 | 117 | ## 3.2 并发与并行 ## 118 | 并发和并行是十分容易混淆的概念。并发指的是多个任务交替进行,而并行则是指真正意义上的“同时进行”。实际上,如果系统内只有一个CPU,而使用多线程时,那么真实系统环境下不能并行,只能通过切换时间片的方式交替进行,而成为并发执行任务。真正的并行也只能出现在拥有多个CPU的系统中。 119 | 120 | ## 3.3 阻塞和非阻塞 ## 121 | 阻塞和非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。 122 | 123 | ## 3.4 临界区 ## 124 | 临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。 -------------------------------------------------------------------------------- /02.线程的状态转换以及基本操作/futureTask接口实现关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/02.线程的状态转换以及基本操作/futureTask接口实现关系.png -------------------------------------------------------------------------------- /02.线程的状态转换以及基本操作/中断线程方法.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/02.线程的状态转换以及基本操作/中断线程方法.png -------------------------------------------------------------------------------- /02.线程的状态转换以及基本操作/线程状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/02.线程的状态转换以及基本操作/线程状态.png -------------------------------------------------------------------------------- /02.线程的状态转换以及基本操作/线程状态转换以及基本操作.md: -------------------------------------------------------------------------------- 1 | 在上一篇文章中[并发编程的优缺点](https://github.com/CL0610/Java-concurrency/blob/master/1.并发编程的优缺点/并发编程的优缺点.md)谈到了为什么花功夫去学习并发编程的技术,也就是说我们必须了解到并发编程的优缺点,我们在什么情况下可以去考虑开启多个线程去实现我们的业务,当然使用多线程我们应该着重注意一些什么,在上一篇文章中会有一些讨论。那么,说了这么多,无论是针对面试还是实际工作中作为一名软件开发人员都应该具备这样的技能。万事开头难,接下来就应该了解如何新建一个线程?线程状态是怎样转换的?关于线程状态的操作是怎样的?这篇文章就主要围绕这三个方面来聊一聊。 2 | # 1. 新建线程 # 3 | 一个java程序从main()方法开始执行,然后按照既定的代码逻辑执行,看似没有其他线程参与,但实际上java程序天生就是一个多线程程序,包含了:(1)分发处理发送给给JVM信号的线程;(2)调用对象的finalize方法的线程;(3)清除Reference的线程;(4)main线程,用户程序的入口。那么,如何在用户程序中新建一个线程了,只要有三种方式: 4 | 5 | 1. 通过继承Thread类,重写run方法; 6 | 2. 通过实现runable接口; 7 | 3. 通过实现callable接口这三种方式,下面看具体demo。 8 | 9 | public class CreateThreadDemo { 10 | 11 | public static void main(String[] args) { 12 | //1.继承Thread 13 | Thread thread = new Thread() { 14 | @Override 15 | public void run() { 16 | System.out.println("继承Thread"); 17 | super.run(); 18 | } 19 | }; 20 | thread.start(); 21 | //2.实现runable接口 22 | Thread thread1 = new Thread(new Runnable() { 23 | @Override 24 | public void run() { 25 | System.out.println("实现runable接口"); 26 | } 27 | }); 28 | thread1.start(); 29 | //3.实现callable接口 30 | ExecutorService service = Executors.newSingleThreadExecutor(); 31 | Future future = service.submit(new Callable() { 32 | @Override 33 | public String call() throws Exception { 34 | return "通过实现Callable接口"; 35 | } 36 | }); 37 | try { 38 | String result = future.get(); 39 | System.out.println(result); 40 | } catch (InterruptedException e) { 41 | e.printStackTrace(); 42 | } catch (ExecutionException e) { 43 | e.printStackTrace(); 44 | } 45 | } 46 | 47 | } 48 | 49 | 三种新建线程的方式具体看以上注释,需要主要的是: 50 | 51 | 52 | - 由于java不能多继承可以实现多个接口,因此,在创建线程的时候尽量多考虑采用实现接口的形式; 53 | - 实现callable接口,提交给ExecutorService返回的是异步执行的结果,另外,通常也可以利用FutureTask(Callable callable)将callable进行包装然后FeatureTask提交给ExecutorsService。如图, 54 | 55 | ![FutureTask接口实现关系](https://github.com/CL0610/Java-concurrency/blob/master/2.%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E4%BB%A5%E5%8F%8A%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/futureTask%E6%8E%A5%E5%8F%A3%E5%AE%9E%E7%8E%B0%E5%85%B3%E7%B3%BB.png) 56 | 57 | 58 | 另外由于FeatureTask也实现了Runable接口也可以利用上面第二种方式(实现Runable接口)来新建线程; 59 | - 可以通过Executors将Runable转换成Callable,具体方法是:Callable callable(Runnable task, T result), Callable callable(Runnable task)。 60 | 61 | # 2. 线程状态转换 # 62 | 63 | 64 | ![线程状态转换图](https://github.com/CL0610/Java-concurrency/blob/master/2.%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E4%BB%A5%E5%8F%8A%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E5%85%B3%E7%B3%BB.png) 65 | 66 | 67 | 68 | 此图来源于《JAVA并发编程的艺术》一书中,线程是会在不同的状态间进行转换的,java线程线程转换图如上图所示。线程创建之后调用start()方法开始运行,当调用wait(),join(),LockSupport.lock()方法线程会进入到**WAITING**状态,而同样的wait(long timeout),sleep(long),join(long),LockSupport.parkNanos(),LockSupport.parkUtil()增加了超时等待的功能,也就是调用这些方法后线程会进入**TIMED_WAITING**状态,当超时等待时间到达后,线程会切换到Runable的状态,另外当WAITING和TIMED _WAITING状态时可以通过Object.notify(),Object.notifyAll()方法使线程转换到Runable状态。当线程出现资源竞争时,即等待获取锁的时候,线程会进入到**BLOCKED**阻塞状态,当线程获取锁时,线程进入到Runable状态。线程运行结束后,线程进入到**TERMINATED**状态,状态转换可以说是线程的生命周期。另外需要注意的是: 69 | 70 | - 当线程进入到synchronized方法或者synchronized代码块时,线程切换到的是BLOCKED状态,而使用java.util.concurrent.locks下lock进行加锁的时候线程切换的是WAITING或者TIMED_WAITING状态,因为lock会调用LockSupport的方法。 71 | 72 | 用一个表格将上面六种状态进行一个总结归纳。 73 | 74 | ![JAVA线程的状态](https://github.com/CL0610/Java-concurrency/blob/master/2.%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E4%BB%A5%E5%8F%8A%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/%E7%BA%BF%E7%A8%8B%E7%8A%B6%E6%80%81.png) 75 | 76 | 77 | # 3. 线程状态的基本操作 # 78 | 除了新建一个线程外,线程在生命周期内还有需要基本操作,而这些操作会成为线程间一种通信方式,比如使用中断(interrupted)方式通知实现线程间的交互等等,下面就将具体说说这些操作。 79 | 80 | ## 3.1. interrupted ## 81 | 82 | 中断可以理解为线程的一个标志位,它表示了一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了一个招呼。其他线程可以调用该线程的interrupt()方法对其进行中断操作,同时该线程可以调用 83 | isInterrupted()来感知其他线程对其自身的中断操作,从而做出响应。另外,同样可以调用Thread的静态方法 84 | interrupted()对当前线程进行中断操作,该方法会清除中断标志位。**需要注意的是,当抛出InterruptedException时候,会清除中断标志位,也就是说在调用isInterrupted会返回false。** 85 | 86 | ![线程中断的方法](https://github.com/CL0610/Java-concurrency/blob/master/2.%E7%BA%BF%E7%A8%8B%E7%9A%84%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2%E4%BB%A5%E5%8F%8A%E5%9F%BA%E6%9C%AC%E6%93%8D%E4%BD%9C/%E4%B8%AD%E6%96%AD%E7%BA%BF%E7%A8%8B%E6%96%B9%E6%B3%95.png) 87 | 88 | 89 | 90 | 91 | 下面结合具体的实例来看一看 92 | 93 | public class InterruptDemo { 94 | public static void main(String[] args) throws InterruptedException { 95 | //sleepThread睡眠1000ms 96 | final Thread sleepThread = new Thread() { 97 | @Override 98 | public void run() { 99 | try { 100 | Thread.sleep(1000); 101 | } catch (InterruptedException e) { 102 | e.printStackTrace(); 103 | } 104 | super.run(); 105 | } 106 | }; 107 | //busyThread一直执行死循环 108 | Thread busyThread = new Thread() { 109 | @Override 110 | public void run() { 111 | while (true) ; 112 | } 113 | }; 114 | sleepThread.start(); 115 | busyThread.start(); 116 | sleepThread.interrupt(); 117 | busyThread.interrupt(); 118 | while (sleepThread.isInterrupted()) ; 119 | System.out.println("sleepThread isInterrupted: " + sleepThread.isInterrupted()); 120 | System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted()); 121 | } 122 | } 123 | 124 | 输出结果 125 | > sleepThread isInterrupted: false 126 | > busyThread isInterrupted: true 127 | 128 | 开启了两个线程分别为sleepThread和BusyThread, sleepThread睡眠1s,BusyThread执行死循环。然后分别对着两个线程进行中断操作,可以看出sleepThread抛出InterruptedException后清除标志位,而busyThread就不会清除标志位。 129 | 130 | 另外,同样可以通过中断的方式实现线程间的简单交互, while (sleepThread.isInterrupted()) 表示在Main中会持续监测sleepThread,一旦sleepThread的中断标志位清零,即sleepThread.isInterrupted()返回为false时才会继续Main线程才会继续往下执行。因此,中断操作可以看做线程间一种简便的交互方式。一般在**结束线程时通过中断标志位或者标志位的方式可以有机会去清理资源,相对于武断而直接的结束线程,这种方式要优雅和安全。** 131 | 132 | ## 3.2. join ## 133 | 134 | join方法可以看做是线程间协作的一种方式,很多时候,一个线程的输入可能非常依赖于另一个线程的输出,这就像两个好基友,一个基友先走在前面突然看见另一个基友落在后面了,这个时候他就会在原处等一等这个基友,等基友赶上来后,就两人携手并进。其实线程间的这种协作方式也符合现实生活。在软件开发的过程中,从客户那里获取需求后,需要经过需求分析师进行需求分解后,这个时候产品,开发才会继续跟进。如果一个线程实例A执行了threadB.join(),其含义是:当前线程A会等待threadB线程终止后threadA才会继续执行。关于join方法一共提供如下这些方法: 135 | > public final synchronized void join(long millis) 136 | > public final synchronized void join(long millis, int nanos) 137 | > public final void join() throws InterruptedException 138 | 139 | Thread类除了提供join()方法外,另外还提供了超时等待的方法,如果线程threadB在等待的时间内还没有结束的话,threadA会在超时之后继续执行。join方法源码关键是: 140 | 141 | while (isAlive()) { 142 | wait(0); 143 | } 144 | 145 | 可以看出来当前等待对象threadA会一直阻塞,直到被等待对象threadB结束后即isAlive()返回false的时候才会结束while循环,当threadB退出时会调用notifyAll()方法通知所有的等待线程。下面用一个具体的例子来说说join方法的使用: 146 | 147 | public class JoinDemo { 148 | public static void main(String[] args) { 149 | Thread previousThread = Thread.currentThread(); 150 | for (int i = 1; i <= 10; i++) { 151 | Thread curThread = new JoinThread(previousThread); 152 | curThread.start(); 153 | previousThread = curThread; 154 | } 155 | } 156 | 157 | static class JoinThread extends Thread { 158 | private Thread thread; 159 | 160 | public JoinThread(Thread thread) { 161 | this.thread = thread; 162 | } 163 | 164 | @Override 165 | public void run() { 166 | try { 167 | thread.join(); 168 | System.out.println(thread.getName() + " terminated."); 169 | } catch (InterruptedException e) { 170 | e.printStackTrace(); 171 | } 172 | } 173 | } 174 | } 175 | 176 | 输出结果为: 177 | 178 | > main terminated. 179 | > Thread-0 terminated. 180 | > Thread-1 terminated. 181 | > Thread-2 terminated. 182 | > Thread-3 terminated. 183 | > Thread-4 terminated. 184 | > Thread-5 terminated. 185 | > Thread-6 terminated. 186 | > Thread-7 terminated. 187 | > Thread-8 terminated. 188 | 189 | 在上面的例子中一个创建了10个线程,每个线程都会等待前一个线程结束才会继续运行。可以通俗的理解成接力,前一个线程将接力棒传给下一个线程,然后又传给下一个线程...... 190 | 191 | ## 3.3 sleep ## 192 | public static native void sleep(long millis)方法显然是Thread的静态方法,很显然它是让当前线程按照指定的时间休眠,其休眠时间的精度取决于处理器的计时器和调度器。需要注意的是如果当前线程获得了锁,sleep方法并不会失去锁。sleep方法经常拿来与Object.wait()方法进行比价,这也是面试经常被问的地方。 193 | > **sleep() VS wait()** 194 | 195 | 两者主要的区别: 196 | 197 | 1. sleep()方法是Thread的静态方法,而wait是Object实例方法 198 | 2. wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁; 199 | 3. sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。 200 | 201 | 202 | ## 3.4 yield ## 203 | public static native void yield();这是一个静态方法,一旦执行,它会是当前线程让出CPU,但是,需要注意的是,让出的CPU并不是代表当前线程不再运行了,如果在下一次竞争中,又获得了CPU时间片当前线程依然会继续运行。另外,让出的时间片只会分配**给当前线程相同优先级**的线程。什么是线程优先级了?下面就来具体聊一聊。 204 | 205 | 现代操作系统基本采用时分的形式调度运行的线程,操作系统会分出一个个时间片,线程会分配到若干时间片,当前时间片用完后就会发生线程调度,并等待这下次分配。线程分配到的时间多少也就决定了线程使用处理器资源的多少,而线程优先级就是决定线程需要或多或少分配一些处理器资源的线程属性。 206 | 207 | 在Java程序中,通过一个**整型成员变量Priority**来控制优先级,优先级的范围从1~10.在构建线程的时候可以通过**setPriority(int)**方法进行设置,默认优先级为5,优先级高的线程相较于优先级低的线程优先获得处理器时间片。需要注意的是在不同JVM以及操作系统上,线程规划存在差异,有些操作系统甚至会忽略线程优先级的设定。 208 | 209 | 另外需要注意的是,sleep()和yield()方法,同样都是当前线程会交出处理器资源,而它们不同的是,sleep()交出来的时间片其他线程都可以去竞争,也就是说都有机会获得当前线程让出的时间片。而yield()方法只允许与当前线程具有相同优先级的线程能够获得释放出来的CPU时间片。 210 | 211 | # 4.守护线程Daemon # 212 | 守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT线程就可以理解守护线程。与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。当一个Java应用,只有守护线程的时候,虚拟机就会自然退出。下面以一个简单的例子来表述Daemon线程的使用。 213 | 214 | public class DaemonDemo { 215 | public static void main(String[] args) { 216 | Thread daemonThread = new Thread(new Runnable() { 217 | @Override 218 | public void run() { 219 | while (true) { 220 | try { 221 | System.out.println("i am alive"); 222 | Thread.sleep(500); 223 | } catch (InterruptedException e) { 224 | e.printStackTrace(); 225 | } finally { 226 | System.out.println("finally block"); 227 | } 228 | } 229 | } 230 | }); 231 | daemonThread.setDaemon(true); 232 | daemonThread.start(); 233 | //确保main线程结束前能给daemonThread能够分到时间片 234 | try { 235 | Thread.sleep(800); 236 | } catch (InterruptedException e) { 237 | e.printStackTrace(); 238 | } 239 | } 240 | } 241 | 242 | 输出结果为: 243 | 244 | > i am alive 245 | > finally block 246 | > i am alive 247 | 248 | 上面的例子中daemodThread run方法中是一个while死循环,会一直打印,但是当main线程结束后daemonThread就会退出所以不会出现死循环的情况。main线程先睡眠800ms保证daemonThread能够拥有一次时间片的机会,也就是说可以正常执行一次打印“i am alive”操作和一次finally块中"finally block"操作。紧接着main 线程结束后,daemonThread退出,这个时候只打印了"i am alive"并没有打印finnal块中的。因此,这里需要注意的是**守护线程在退出的时候并不会执行finnaly块中的代码,所以将释放资源等操作不要放在finnaly块中执行,这种操作是不安全的** 249 | 250 | 线程可以通过setDaemon(true)的方法将线程设置为守护线程。并且需要注意的是设置守护线程要先于start()方法,否则会报 251 | > Exception in thread "main" java.lang.IllegalThreadStateException 252 | > at java.lang.Thread.setDaemon(Thread.java:1365) 253 | > at learn.DaemonDemo.main(DaemonDemo.java:19) 254 | 255 | 这样的异常,但是该线程还是会执行,只不过会当做正常的用户线程执行。 256 | -------------------------------------------------------------------------------- /02.线程的状态转换以及基本操作/线程状态转换关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/02.线程的状态转换以及基本操作/线程状态转换关系.png -------------------------------------------------------------------------------- /03.java内存模型以及happens-before规则/JMM内存结构抽象结构示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/03.java内存模型以及happens-before规则/JMM内存结构抽象结构示意图.png -------------------------------------------------------------------------------- /03.java内存模型以及happens-before规则/JMM层级图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/03.java内存模型以及happens-before规则/JMM层级图.png -------------------------------------------------------------------------------- /03.java内存模型以及happens-before规则/JMM设计示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/03.java内存模型以及happens-before规则/JMM设计示意图.png -------------------------------------------------------------------------------- /03.java内存模型以及happens-before规则/Java内存模型以及happens-before.md: -------------------------------------------------------------------------------- 1 | # 1. JMM的介绍 # 2 | 在上一篇文章中总结了[线程的状态转换和一些基本操作](http://www.jianshu.com/p/f65ea68a4a7f),对多线程已经有一点基本的认识了,如果多线程编程只有这么简单,那我们就不必费劲周折的去学习它了。在多线程中稍微不注意就会出现线程安全问题,那么什么是线程安全问题?我的认识是,在多线程下代码执行的结果与预期正确的结果不一致,该代码就是线程不安全的,否则则是线程安全的。虽然这种回答似乎不能获取什么内容,可以google下。在<<深入理解Java虚拟机>>中看到的定义。原文如下: 3 | 当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替运行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获取正确的结果,那这个对象是线程安全的。 4 | 5 | 关于定义的理解这是一个仁者见仁智者见智的事情。出现线程安全的问题一般是因为**主内存和工作内存数据不一致性**和**重排序**导致的,而解决线程安全的问题最重要的就是理解这两种问题是怎么来的,那么,理解它们的核心在于理解java内存模型(JMM)。 6 | 7 | 在多线程条件下,多个线程肯定会相互协作完成一件事情,一般来说就会涉及到**多个线程间相互通信告知彼此的状态以及当前的执行结果**等,另外,为了性能优化,还会**涉及到编译器指令重排序和处理器指令重排序**。下面会一一来聊聊这些知识。 8 | 9 | #2. 内存模型抽象结构 10 | 线程间协作通信可以类比人与人之间的协作的方式,在现实生活中,之前网上有个流行语“你妈喊你回家吃饭了”,就以这个生活场景为例,小明在外面玩耍,小明妈妈在家里做饭,做晚饭后准备叫小明回家吃饭,那么就存在两种方式: 11 | 12 | 小明妈妈要去上班了十分紧急这个时候手机又没有电了,于是就在桌子上贴了一张纸条“饭做好了,放在...”小明回家后看到纸条如愿吃到妈妈做的饭菜,那么,如果将小明妈妈和小明作为两个线程,那么这张纸条就是这两个线程间通信的共享变量,通过读写共享变量实现两个线程间协作; 13 | 14 | 还有一种方式就是,妈妈的手机还有电,妈妈在赶去坐公交的路上给小明打了个电话,这种方式就是通知机制来完成协作。同样,可以引申到线程间通信机制。 15 | 16 | 通过上面这个例子,应该有些认识。在并发编程中主要需要解决两个问题:**1. 线程之间如何通信;2.线程之间如何完成同步**(这里的线程指的是并发执行的活动实体)。通信是指线程之间以何种机制来交换信息,主要有两种:共享内存和消息传递。这里,可以分别类比上面的两个举例。java内存模型是**共享内存的并发模型**,线程之间主要通过读-写共享变量来完成隐式通信。如果程序员不能理解Java的共享内存模型在编写并发程序时一定会遇到各种各样关于内存可见性的问题。 17 | 18 | > 1.哪些是共享变量 19 | 20 | 在java程序中所有**实例域,静态域和数组元素**都是放在堆内存中(所有线程均可访问到,是可以共享的),而局部变量,方法定义参数和异常处理器参数不会在线程间共享。共享数据会出现线程安全的问题,而非共享数据不会出现线程安全的问题。关于JVM运行时内存区域在后面的文章会讲到。 21 | 22 | > 2.JMM抽象结构模型 23 | 24 | 我们知道CPU的处理速度和主存的读写速度不是一个量级的,为了平衡这种巨大的差距,每个CPU都会有缓存。因此,共享变量会先放在主存中,每个线程都有属于自己的工作内存,并且会把位于主存中的共享变量拷贝到自己的工作内存,之后的读写操作均使用位于工作内存的变量副本,并在某个时刻将工作内存的变量副本写回到主存中去。JMM就从抽象层次定义了这种方式,并且JMM决定了一个线程对共享变量的写入何时对其他线程是可见的。 25 | 26 | ![JMM内存模型的抽象结构示意图](http://upload-images.jianshu.io/upload_images/2615789-8c0b960a27af28db.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) 27 | 28 | 如图为JMM抽象示意图,线程A和线程B之间要完成通信的话,要经历如下两步: 29 | 1. 线程A从主内存中将共享变量读入线程A的工作内存后并进行操作,之后将数据重新写回到主内存中; 30 | 2. 线程B从主存中读取最新的共享变量 31 | 32 | 从横向去看看,线程A和线程B就好像通过共享变量在进行隐式通信。这其中有很有意思的问题,如果线程A更新后数据并没有及时写回到主存,而此时线程B读到的是过期的数据,这就出现了“脏读”现象。可以通过同步机制(控制不同线程间操作发生的相对顺序)来解决或者通过volatile关键字使得每次volatile变量都能够强制刷新到主存,从而对每个线程都是可见的。 33 | 34 | # 3. 重排序 # 35 | 一个好的内存模型实际上会放松对处理器和编译器规则的束缚,也就是说软件技术和硬件技术都为同一个目标而进行奋斗:在不改变程序执行结果的前提下,尽可能提高并行度。JMM对底层尽量减少约束,使其能够发挥自身优势。因此,在执行程序时,**为了提高性能,编译器和处理器常常会对指令进行重排序**。一般重排序可以分为如下三种: 36 | 37 | ![从源码到最终执行的指令序列的示意图](http://upload-images.jianshu.io/upload_images/2615789-4a1ae3e7c7906823.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) 38 | 1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序; 39 | 2. 指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果**不存在数据依赖性**,处理器可以改变语句对应机器指令的执行顺序; 40 | 3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。 41 | 42 | 43 | 如图,1属于编译器重排序,而2和3统称为处理器重排序。这些重排序会导致线程安全的问题,一个很经典的例子就是DCL问题,这个在以后的文章中会具体去聊。**针对编译器重排序**,JMM的编译器重排序规则会禁止一些**特定类型的编译器重排序**;**针对处理器重排序**,编译器在生成指令序列的时候会通过**插入内存屏障指令来禁止某些特殊的处理器重排序**。 44 | 45 | 那么什么情况下,不能进行重排序了?下面就来说说数据依赖性。有如下代码: 46 | 47 | > double pi = 3.14 //A 48 | > 49 | > double r = 1.0 //B 50 | > 51 | > double area = pi * r * r //C 52 | 53 | 这是一个计算圆面积的代码,由于A,B之间没有任何关系,对最终结果也不会存在关系,它们之间执行顺序可以重排序。因此可以执行顺序可以是A->B->C或者B->A->C执行最终结果都是3.14,即A和B之间没有数据依赖性。具体的定义为:**如果两个操作访问同一个变量,且这两个操作有一个为写操作,此时这两个操作就存在数据依赖性**这里就存在三种情况:1. 读后写;2.写后写;3. 写后读,者三种操作都是存在数据依赖性的,如果重排序会对最终执行结果会存在影响。**编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖性关系的两个操作的执行顺序** 54 | 55 | 另外,还有一个比较有意思的就是as-if-serial语义。 56 | 57 | > **as-if-serial** 58 | 59 | as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提供并行度),(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。as-if-serial语义把单线程程序保护了起来,**遵守as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的**。比如上面计算圆面积的代码,在单线程中,会让人感觉代码是一行一行顺序执行上,实际上A,B两行不存在数据依赖性可能会进行重排序,即A,B不是顺序执行的。as-if-serial语义使程序员不必担心单线程中重排序的问题干扰他们,也无需担心内存可见性问题。 60 | 61 | # 4. happens-before规则 # 62 | 上面的内容讲述了重排序原则,一会是编译器重排序一会是处理器重排序,如果让程序员再去了解这些底层的实现以及具体规则,那么程序员的负担就太重了,严重影响了并发编程的效率。因此,JMM为程序员在上层提供了六条规则,这样我们就可以根据规则去推论跨线程的内存可见性问题,而不用再去理解底层重排序的规则。下面以两个方面来说。 63 | 64 | ## 4.1 happens-before定义 ## 65 | happens-before的概念最初由Leslie Lamport在其一篇影响深远的论文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出,有兴趣的可以google一下。JSR-133使用happens-before的概念来指定两个操作之间的执行顺序。由于这两个操作可以在一个线程之内,也可以是在不同线程之间。因此,**JMM可以通过happens-before关系向程序员提供跨线程的内存可见性保证**(如果A线程的写操作a与B线程的读操作b之间存在happens-before关系,尽管a操作和b操作在不同的线程中执行,但JMM向程序员保证a操作将对b操作可见)。具体的定义为: 66 | 67 | 1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。 68 | 69 | 2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。 70 | 71 | 上面的**1)是JMM对程序员的承诺**。从程序员的角度来说,可以这样理解happens-before关系:如果A happens-before B,那么Java内存模型将向程序员保证——A操作的结果将对B可见,且A的执行顺序排在B之前。注意,这只是Java内存模型向程序员做出的保证! 72 | 73 | 上面的**2)是JMM对编译器和处理器重排序的约束原则**。正如前面所言,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。JMM这么做的原因是:程序员对于这两个操作是否真的被重排序并不关心,程序员关心的是程序执行时的语义不能被改变(即执行结果不能被改变)。因此,happens-before关系本质上和as-if-serial语义是一回事。 74 | 75 | 下面来比较一下as-if-serial和happens-before: 76 | 77 | > **as-if-serial VS happens-before** 78 | 79 | 1. as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。 80 | 2. as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的。 81 | 3. as-if-serial语义和happens-before这么做的目的,都是为了在不改变程序执行结果的前提下,尽可能地提高程序执行的并行度。 82 | 83 | ## 4.2 具体规则 ## 84 | 具体的一共有六项规则: 85 | 86 | 1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。 87 | 2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。 88 | 3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。 89 | 4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。 90 | 5. start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。 91 | 6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。 92 | 7. 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。 93 | 7. 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。 94 | 95 | 下面以一个**具体的例子来讲下如何使用这些规则进行推论**: 96 | 97 | 依旧以上面计算圆面积的进行描述。利用程序顺序规则(规则1)存在三个happens-before关系:1. A happens-before B;2. B happens-before C;3. A happens-before C。这里的第三个关系是利用传递性进行推论的。A happens-before B,定义1要求A执行结果对B可见,并且A操作的执行顺序在B操作之前,但与此同时利用定义中的第二条,A,B操作彼此不存在数据依赖性,两个操作的执行顺序对最终结果都不会产生影响,在不改变最终结果的前提下,允许A,B两个操作重排序,即happens-before关系并不代表了最终的执行顺序。 98 | 99 | # 5. 总结 # 100 | 上面已经聊了关于JMM的两个方面:1. JMM的抽象结构(主内存和线程工作内存);2. 重排序以及happens-before规则。接下来,我们来做一个总结。从两个方面进行考虑。1. 如果让我们设计JMM应该从哪些方面考虑,也就是说JMM承担哪些功能;2. happens-before与JMM的关系;3. 由于JMM,多线程情况下可能会出现哪些问题? 101 | 102 | ## 5.1 JMM的设计 ## 103 | 104 | ![JMM层级图](http://upload-images.jianshu.io/upload_images/2615789-b96f4b4edada03a6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) 105 | 106 | JMM是语言级的内存模型,在我的理解中JMM处于中间层,包含了两个方面:(1)内存模型;(2)重排序以及happens-before规则。同时,为了禁止特定类型的重排序会对编译器和处理器指令序列加以控制。而上层会有基于JMM的关键字和J.U.C包下的一些具体类用来方便程序员能够迅速高效率的进行并发编程。站在JMM设计者的角度,在设计JMM时需要考虑两个关键因素: 107 | 108 | 1. **程序员对内存模型的使用** 109 | 程序员希望内存模型易于理解、易于编程。程序员希望基于一个强内存模型来编写代码。 110 | 2. **编译器和处理器对内存模型的实现** 111 | 编译器和处理器希望内存模型对它们的束缚越少越好,这样它们就可以做尽可能多的优化来提高性能。编译器和处理器希望实现一个弱内存模型。 112 | 113 | 另外还要一个特别有意思的事情就是关于重排序问题,更简单的说,重排序可以分为两类: 114 | 115 | 1. 会改变程序执行结果的重排序。 116 | 2. 不会改变程序执行结果的重排序。 117 | 118 | JMM对这两种不同性质的重排序,采取了不同的策略,如下。 119 | 120 | 1. 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。 121 | 2. 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种 122 | 重排序) 123 | 124 | JMM的设计图为: 125 | 126 | ![JMM设计示意图](http://upload-images.jianshu.io/upload_images/2615789-b288451befb6a441.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) 127 | 从图可以看出: 128 | 129 | 1. JMM向程序员提供的happens-before规则能满足程序员的需求。JMM的happens-before规则不但简单易懂,而且也向程序员提供了足够强的内存可见性保证(有些内存可见性保证其实并不一定真实存在,比如上面的A happens-before B)。 130 | 2. JMM对编译器和处理器的束缚已经尽可能少。从上面的分析可以看出,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。例如,如果编译器经过细致的分析后,认定一个锁只会被单个线程访问,那么这个锁可以被消除。再如,如果编译器经过细致的分析后,认定一个volatile变量只会被单个线程访问,那么编译器可以把这个volatile变量当作一个普通变量来对待。这些优化既不会改变程序的执行结果,又能提高程序的执行效率。 131 | ## 5.2 happens-before与JMM的关系 ## 132 | 133 | 134 | ![happens-before与JMM的关系](http://upload-images.jianshu.io/upload_images/2615789-dd96af34a8df5c49.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) 135 | 136 | 一个happens-before规则对应于一个或多个编译器和处理器重排序规则。对于Java程序员来说,happens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法 137 | 138 | 139 | ## 5.3 今后可能需要关注的问题 ## 140 | 从上面内存抽象结构来说,可能出在数据“脏读”的现象,这就是**数据可见性**的问题,另外,重排序在多线程中不注意的话也容易存在一些问题,比如一个很经典的问题就是DCL(双重检验锁),这就是需要**禁止重排序**,另外,在多线程下原子操作例如i++不加以注意的也容易出现线程安全的问题。但总的来说,在多线程开发时需要从**原子性,有序性,可见性**三个方面进行考虑。J.U.C包下的并发工具类和并发容器也是需要花时间去掌握的,这些东西在以后得文章中多会一一进行讨论。 141 | 142 | 143 | > 参考文献 144 | 145 | 《java并发编程的艺术》 -------------------------------------------------------------------------------- /03.java内存模型以及happens-before规则/happens-before与JMM的关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/03.java内存模型以及happens-before规则/happens-before与JMM的关系.png -------------------------------------------------------------------------------- /03.java内存模型以及happens-before规则/从源码到最终执行的指令序列的示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/03.java内存模型以及happens-before规则/从源码到最终执行的指令序列的示意图.png -------------------------------------------------------------------------------- /04.彻底理解synchronized/Mark Word存储结构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/04.彻底理解synchronized/Mark Word存储结构.png -------------------------------------------------------------------------------- /04.彻底理解synchronized/Mark Word状态变化.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/04.彻底理解synchronized/Mark Word状态变化.png -------------------------------------------------------------------------------- /04.彻底理解synchronized/java关键字---synchronized.md: -------------------------------------------------------------------------------- 1 | # 1. synchronized简介 # 2 | 3 | 在学习知识前,我们先来看一个现象: 4 | 5 | public class SynchronizedDemo implements Runnable { 6 | private static int count = 0; 7 | 8 | public static void main(String[] args) { 9 | for (int i = 0; i < 10; i++) { 10 | Thread thread = new Thread(new SynchronizedDemo()); 11 | thread.start(); 12 | } 13 | try { 14 | Thread.sleep(500); 15 | } catch (InterruptedException e) { 16 | e.printStackTrace(); 17 | } 18 | System.out.println("result: " + count); 19 | } 20 | 21 | @Override 22 | public void run() { 23 | for (int i = 0; i < 1000000; i++) 24 | count++; 25 | } 26 | } 27 | 28 | 开启了10个线程,每个线程都累加了1000000次,如果结果正确的话自然而然总数就应该是10 * 1000000 = 10000000。可就运行多次结果都不是这个数,而且每次运行结果都不一样。这是为什么了?有什么解决方案了?这就是我们今天要聊的事情。 29 | 30 | 在上一篇博文中我们已经了解了[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)的一些知识,并且已经知道出现线程安全的主要来源于JMM的设计,主要集中在主内存和线程的工作内存而导致的**内存可见性问题**,以及**重排序导致的问题**,进一步知道了**happens-before规则**。线程运行时拥有自己的栈空间,会在自己的栈空间运行,如果多线程间没有共享的数据也就是说多线程间并没有协作完成一件事情,那么,多线程就不能发挥优势,不能带来巨大的价值。那么共享数据的线程安全问题怎样处理?很自然而然的想法就是每一个线程依次去读写这个共享变量,这样就不会有任何数据安全的问题,因为每个线程所操作的都是当前最新的版本数据。那么,在java关键字synchronized就具有使每个线程依次排队操作共享变量的功能。很显然,这种同步机制效率很低,但synchronized是其他并发容器实现的基础,对它的理解也会大大提升对并发编程的感觉,从功利的角度来说,这也是面试高频的考点。好了,下面,就来具体说说这个关键字。 31 | # 2. synchronized实现原理 # 32 | 在java代码中使用synchronized可是使用在代码块和方法中,根据Synchronized用的位置可以有这些使用场景: 33 | 34 | 35 | ![Synchronized的使用场景](https://user-gold-cdn.xitu.io/2018/4/30/16315cc79aaac173?w=700&h=413&f=png&s=33838) 36 | 37 | 38 | 如图,synchronized可以用在**方法**上也可以使用在**代码块**中,其中方法是实例方法和静态方法分别锁的是该类的实例对象和该类的对象。而使用在代码块中也可以分为三种,具体的可以看上面的表格。这里的需要注意的是:**如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系**。 39 | 40 | 现在我们已经知道了怎样synchronized了,看起来很简单,拥有了这个关键字就真的可以在并发编程中得心应手了吗?爱学的你,就真的不想知道synchronized底层是怎样实现了吗? 41 | ## 2.1 对象锁(monitor)机制 ## 42 | 现在我们来看看synchronized的具体底层实现。先写一个简单的demo: 43 | 44 | public class SynchronizedDemo { 45 | public static void main(String[] args) { 46 | synchronized (SynchronizedDemo.class) { 47 | } 48 | method(); 49 | } 50 | 51 | private static void method() { 52 | } 53 | } 54 | 上面的代码中有一个同步代码块,锁住的是类对象,并且还有一个同步静态方法,锁住的依然是该类的类对象。编译之后,切换到SynchronizedDemo.class的同级目录之后,然后用**javap -v SynchronizedDemo.class**查看字节码文件: 55 | 56 | 57 | 58 | ![SynchronizedDemo.class](https://user-gold-cdn.xitu.io/2018/4/30/16315cce259af0d2?w=700&h=330&f=png&s=68919) 59 | 60 | 61 | 如图,上面用黄色高亮的部分就是需要注意的部分了,这也是添Synchronized关键字之后独有的。执行同步代码块后首先要先执行**monitorenter**指令,退出的时候**monitorexit**指令。通过分析之后可以看出,使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是**互斥**的,即同一时刻只有一个线程能够获取到monitor。上面的demo中在执行完同步代码块之后紧接着再会去执行一个静态同步方法,而这个方法锁的对象依然就这个类对象,那么这个正在执行的线程还需要获取该锁吗?答案是不必的,从上图中就可以看出来,执行静态同步方法的时候就只有一条monitorexit指令,并没有monitorenter获取锁的指令。这就是**锁的重入性**,即在同一锁程中,线程不需要再次获取同一把锁。Synchronized先天具有重入性。**每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一**。 62 | 63 | 任意一个对象都拥有自己的监视器,当这个对象由同步块或者这个对象的同步方法调用时,执行方法的线程必须先获取该对象的监视器才能进入同步块和同步方法,如果没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入到BLOCKED状态(关于线程的状态可以看[这篇文章](https://juejin.im/post/5ae6cf7a518825670960fcc2) 64 | 65 | 下图表现了对象,对象监视器,同步队列以及执行线程状态之间的关系: 66 | 67 | ![对象,对象监视器,同步队列和线程状态的关系](https://user-gold-cdn.xitu.io/2018/4/30/16315cd5fa7cf91c?w=700&h=261&f=png&s=54962) 68 | 69 | 该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。 70 | 71 | 72 | ## 2.2 synchronized的happens-before关系 ## 73 | 在上一篇文章中讨论过[happens-before](https://juejin.im/post/5ae6d309518825673123fd0e)规则,抱着学以致用的原则我们现在来看一看Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。继续来看代码: 74 | 75 | public class MonitorDemo { 76 | private int a = 0; 77 | 78 | public synchronized void writer() { // 1 79 | a++; // 2 80 | } // 3 81 | 82 | public synchronized void reader() { // 4 83 | int i = a; // 5 84 | } // 6 85 | } 86 | 87 | 该代码的happens-before关系如图所示: 88 | 89 | 90 | 91 | ![synchronized的happens-before关系](https://user-gold-cdn.xitu.io/2018/4/30/16315ce6ea84f240?w=650&h=629&f=png&s=61572) 92 | 93 | 94 | 在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:**线程A释放锁happens-before线程B加锁**,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。现在我们来重点关注2 happens-before 5,通过这个关系我们可以得出什么? 95 | 96 | 根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B。线程A先对共享变量A进行加一,由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1。 97 | 98 | ## 2.3 锁获取和锁释放的内存语义 ## 99 | 在上一篇文章提到过JMM核心为两个部分:happens-before规则以及内存抽象模型。我们分析完Synchronized的happens-before关系后,还是不太完整的,我们接下来看看基于java内存抽象模型的Synchronized的内存语义。 100 | 101 | 废话不多说依旧先上图。 102 | 103 | ![线程A写共享变量](https://user-gold-cdn.xitu.io/2018/4/30/16315cef21fd3ad8?w=557&h=440&f=png&s=10816) 104 | 105 | 106 | 从上图可以看出,线程A会首先先从主内存中读取共享变量a=0的值然后将该变量拷贝到自己的本地内存,进行加一操作后,再将该值刷新到主内存,整个过程即为线程A 加锁-->执行临界区代码-->释放锁相对应的内存语义。 107 | 108 | 109 | 110 | ![线程B读共享变量](https://user-gold-cdn.xitu.io/2018/4/30/16315cf41661491e?w=564&h=458&f=png&s=14468) 111 | 112 | 线程B获取锁的时候同样会从主内存中共享变量a的值,这个时候就是最新的值1,然后将该值拷贝到线程B的工作内存中去,释放锁的时候同样会重写到主内存中。 113 | 114 | 从整体上来看,线程A的执行结果(a=1)对线程B是可见的,实现原理为:释放锁的时候会将值刷新到主内存中,其他线程获取锁时会强制从主内存中获取最新的值。另外也验证了2 happens-before 5,2的执行结果对5是可见的。 115 | 116 | 从横向来看,这就像线程A通过主内存中的共享变量和线程B进行通信,A 告诉 B 我们俩的共享数据现在为1啦,这种线程间的通信机制正好吻合java的内存模型正好是共享内存的并发模型结构。 117 | 118 | # 3. synchronized优化 # 119 | 通过上面的讨论现在我们对Synchronized应该有所印象了,它最大的特征就是在同一时刻只有一个线程能够获得对象的监视器(monitor),从而进入到同步代码块或者同步方法之中,即表现为**互斥性(排它性)**。这种方式肯定效率低下,每次只能通过一个线程,既然每次只能通过一个,这种形式不能改变的话,那么我们能不能让每次通过的速度变快一点了。打个比方,去收银台付款,之前的方式是,大家都去排队,然后去纸币付款收银员找零,有的时候付款的时候在包里拿出钱包再去拿出钱,这个过程是比较耗时的,然后,支付宝解放了大家去钱包找钱的过程,现在只需要扫描下就可以完成付款了,也省去了收银员跟你找零的时间的了。同样是需要排队,但整个付款的时间大大缩短,是不是整体的效率变高速率变快了?这种优化方式同样可以引申到锁优化上,缩短获取锁的时间,伟大的科学家们也是这样做的,令人钦佩,毕竟java是这么优秀的语言(微笑脸)。 120 | 121 | 在聊到锁的优化也就是锁的几种状态前,有两个知识点需要先关注:(1)CAS操作 (2)Java对象头,这是理解下面知识的前提条件。 122 | 123 | ## 3.1 CAS操作 ## 124 | ### 3.1.1 什么是CAS? ### 125 | 使用锁时,线程获取锁是一种**悲观锁策略**,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种**乐观锁策略**,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用**CAS(compare and swap)**又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。 126 | ### 3.1.2 CAS的操作过程 ### 127 | CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:**V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值**。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程 128 | 129 | CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的**CMPXCHG**指令实现。 130 | 131 | > Synchronized VS CAS 132 | 133 | 元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。 134 | 135 | ### 3.1.3 CAS的应用场景 ### 136 | 137 | 在J.U.C包中利用CAS实现类有很多,可以说是支撑起整个concurrency包的实现,在Lock实现中会有CAS改变state变量,在atomic包中的实现类也几乎都是用CAS实现,关于这些具体的实现场景在之后会详细聊聊,现在有个印象就好了(微笑脸)。 138 | 139 | ### 3.1.4 CAS的问题 ### 140 | 141 | **1. ABA问题** 142 | 因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。java这么优秀的语言,当然在java 1.5后的atomic包中提供了AtomicStampedReference来解决ABA问题,解决思路就是这样的。 143 | 144 | **2. 自旋时间过长** 145 | 146 | 使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。 147 | 148 | **3. 只能保证一个共享变量的原子操作** 149 | 150 | 当对一个共享变量执行操作时CAS能保证其原子性,如果对多个共享变量进行操作,CAS就不能保证其原子性。有一个解决方案是利用对象整合多个共享变量,即一个类中的成员变量就是这几个共享变量。然后将这个对象做CAS操作就可以保证其原子性。atomic中提供了AtomicReference来保证引用对象之间的原子性。 151 | 152 | ## 3.2 Java对象头 ## 153 | 154 | 在同步的时候是获取对象的monitor,即获取到对象的锁。那么对象的锁怎么理解?无非就是类似对对象的一个标志,那么这个标志就是存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。32为JVM Mark Word默认存储结构为(注:java对象头以及下面的锁状态变化摘自《java并发编程的艺术》一书,该书我认为写的足够好,就没在自己组织语言班门弄斧了): 155 | 156 | ![Mark Word存储结构](https://user-gold-cdn.xitu.io/2018/4/30/16315cff10307a29?w=700&h=71&f=png&s=23717) 157 | 158 | 如图在Mark Word会默认存放hasdcode,年龄值以及锁标志位等信息。 159 | 160 | Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:**无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态**,这几个状态会随着竞争情况逐渐升级。**锁可以升级但不能降级**,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。对象的MarkWord变化为下图: 161 | 162 | ![Mark Word状态变化](https://user-gold-cdn.xitu.io/2018/4/30/16315d056598e4c2?w=700&h=151&f=png&s=47968) 163 | 164 | 165 | 166 | ## 3.2 偏向锁 ## 167 | 168 | HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。 169 | 170 | > **偏向锁的获取** 171 | 172 | 当一个线程访问同步块并获取锁时,会在**对象头**和**栈帧中的锁记录**里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程 173 | 174 | 175 | > **偏向锁的撤销** 176 | 177 | 偏向锁使用了一种**等到竞争出现才释放锁**的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。 178 | 179 | 180 | ![偏向锁撤销流程](https://user-gold-cdn.xitu.io/2018/4/30/16315d0b13b37da4?w=567&h=736&f=png&s=72325) 181 | 182 | 183 | 如图,偏向锁的撤销,需要等待**全局安全点**(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word**要么**重新偏向于其他线程,**要么**恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。 184 | 185 | 下图线程1展示了偏向锁获取的过程,线程2展示了偏向锁撤销的过程。 186 | 187 | ![偏向锁获取和撤销流程](https://user-gold-cdn.xitu.io/2018/4/30/16315cb9175365f5?w=630&h=703&f=png&s=160223) 188 | 189 | > **如何关闭偏向锁** 190 | 191 | 偏向锁在Java 6和Java 7里是默认启用的,但是它在应用程序启动几秒钟之后才激活,如有必要可以使用JVM参数来关闭延迟:**-XX:BiasedLockingStartupDelay=0**。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:**-XX:-UseBiasedLocking=false**,那么程序默认会进入轻量级锁状态 192 | 193 | ## 3.3 轻量级锁 ## 194 | > **加锁** 195 | 196 | 线程在执行同步块之前,JVM会先在当前线程的栈桢中**创建用于存储锁记录的空间**,并将对象头中的Mark Word复制到锁记录中,官方称为**Displaced Mark Word**。然后线程尝试使用CAS**将对象头中的Mark Word替换为指向锁记录的指针**。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 197 | 198 | 199 | > **解锁** 200 | 201 | 轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。 202 | 203 | 204 | ![轻量级锁加锁解锁以及锁膨胀](https://user-gold-cdn.xitu.io/2018/4/30/16315cb9193719c2?w=794&h=772&f=png&s=287958) 205 | 206 | 因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。 207 | ## 3.5 各种锁的比较 ## 208 | 209 | ![各种锁的对比](https://user-gold-cdn.xitu.io/2018/4/30/16315cb91da523d9?w=800&h=193&f=png&s=116058) 210 | 211 | # 4. 一个例子 # 212 | 经过上面的理解,我们现在应该知道了该怎样解决了。更正后的代码为: 213 | 214 | public class SynchronizedDemo implements Runnable { 215 | private static int count = 0; 216 | 217 | public static void main(String[] args) { 218 | for (int i = 0; i < 10; i++) { 219 | Thread thread = new Thread(new SynchronizedDemo()); 220 | thread.start(); 221 | } 222 | try { 223 | Thread.sleep(500); 224 | } catch (InterruptedException e) { 225 | e.printStackTrace(); 226 | } 227 | System.out.println("result: " + count); 228 | } 229 | 230 | @Override 231 | public void run() { 232 | synchronized (SynchronizedDemo.class) { 233 | for (int i = 0; i < 1000000; i++) 234 | count++; 235 | } 236 | } 237 | } 238 | 239 | 开启十个线程,每个线程在原值上累加1000000次,最终正确的结果为10X1000000=10000000,这里能够计算出正确的结果是因为在做累加操作时使用了同步代码块,这样就能保证每个线程所获得共享变量的值都是当前最新的值,如果不使用同步的话,就可能会出现A线程累加后,而B线程做累加操作有可能是使用原来的就值,即“脏值”。这样,就导致最终的计算结果不是正确的。而使用Syncnized就可能保证内存可见性,保证每个线程都是操作的最新值。这里只是一个示例性的demo,聪明的你,还有其他办法吗? 240 | 241 | > 参考文献 242 | 243 | 《java并发编程的艺术》 244 | -------------------------------------------------------------------------------- /04.彻底理解synchronized/synchronizedDemo.class.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/04.彻底理解synchronized/synchronizedDemo.class.png -------------------------------------------------------------------------------- /04.彻底理解synchronized/synchronized的happens-before关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/04.彻底理解synchronized/synchronized的happens-before关系.png -------------------------------------------------------------------------------- /04.彻底理解synchronized/synchronized的使用场景.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/04.彻底理解synchronized/synchronized的使用场景.png -------------------------------------------------------------------------------- /04.彻底理解synchronized/偏向锁撤销流程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/04.彻底理解synchronized/偏向锁撤销流程.png -------------------------------------------------------------------------------- /04.彻底理解synchronized/偏向锁获取和撤销流程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/04.彻底理解synchronized/偏向锁获取和撤销流程.png -------------------------------------------------------------------------------- /04.彻底理解synchronized/各种锁的对比.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/04.彻底理解synchronized/各种锁的对比.png -------------------------------------------------------------------------------- /04.彻底理解synchronized/对象,对象监视器,同步队列和线程状态的关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/04.彻底理解synchronized/对象,对象监视器,同步队列和线程状态的关系.png -------------------------------------------------------------------------------- /04.彻底理解synchronized/线程A写共享变量.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/04.彻底理解synchronized/线程A写共享变量.png -------------------------------------------------------------------------------- /04.彻底理解synchronized/线程B读共享变量.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/04.彻底理解synchronized/线程B读共享变量.png -------------------------------------------------------------------------------- /04.彻底理解synchronized/轻量级锁加锁解锁以及锁膨胀.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/04.彻底理解synchronized/轻量级锁加锁解锁以及锁膨胀.png -------------------------------------------------------------------------------- /05.彻底理解volatile/VolatileExample的happens-before关系推导.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/05.彻底理解volatile/VolatileExample的happens-before关系推导.png -------------------------------------------------------------------------------- /05.彻底理解volatile/java关键字---volatile.md: -------------------------------------------------------------------------------- 1 | # 1. volatile简介 # 2 | 3 | 在上一篇文章中我们深入理解了java关键字[synchronized](https://juejin.im/post/5ae6dc04f265da0ba351d3ff),我们知道在java中还有一大神器就是关键volatile,可以说是和synchronized各领风骚,其中奥妙,我们来共同探讨下。 4 | 5 | 通过上一篇的文章我们了解到synchronized是阻塞式同步,在线程竞争激烈的情况下会升级为重量级锁。而volatile就可以说是java虚拟机提供的最轻量级的同步机制。但它同时不容易被正确理解,也至于在并发编程中很多程序员遇到线程安全的问题就会使用synchronized。[Java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。线程在工作内存进行操作后何时会写到主内存中?这个时机对普通变量是没有规定的,而针对volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。 6 | 7 | 现在我们有了一个大概的印象就是:**被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。** 8 | 9 | # 2. volatile实现原理 # 10 | volatile是怎样实现了?比如一个很简单的Java代码: 11 | > instance = new Instancce() //instance是volatile变量 12 | 13 | 在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出**Lock前缀的指令**(具体的大家可以使用一些工具去看一下,这里我就只把结果说出来)。我们想这个**Lock**指令肯定有神奇的地方,那么Lock前缀的指令在多核处理器下会发现什么事情了?主要有这两个方面的影响: 14 | 15 | 1. 将当前处理器缓存行的数据写回系统内存; 16 | 2. 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效 17 | 18 | 19 | 为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现**缓存一致性**协议,**每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期**了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。因此,经过分析我们可以得出如下结论: 20 | 21 | 1. Lock前缀的指令会引起处理器缓存写回内存; 22 | 2. 一个处理器的缓存回写到内存会导致其他处理器的缓存失效; 23 | 3. 当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。 24 | 25 | 这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。 26 | 27 | # 3. volatile的happens-before关系 # 28 | 29 | 经过上面的分析,我们已经知道了volatile变量可以通过**缓存一致性协议**保证每个线程都能获得最新值,即满足数据的“可见性”。我们继续延续上一篇分析问题的方式(我一直认为思考问题的方式是属于自己,也才是最重要的,也在不断培养这方面的能力),我一直将并发分析的切入点分为**两个核心,三大性质**。两大核心:JMM内存模型(主内存和工作内存)以及happens-before;三条性质:原子性,可见性,有序性(关于三大性质的总结在以后得文章会和大家共同探讨)。废话不多说,先来看两个核心之一:volatile的happens-before关系。 30 | 31 | 在六条[happens-before规则](https://juejin.im/post/5ae6d309518825673123fd0e)中有一条是:**volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。**下面我们结合具体的代码,我们利用这条规则推导下: 32 | 33 | public class VolatileExample { 34 | private int a = 0; 35 | private volatile boolean flag = false; 36 | public void writer(){ 37 | a = 1; //1 38 | flag = true; //2 39 | } 40 | public void reader(){ 41 | if(flag){ //3 42 | int i = a; //4 43 | } 44 | } 45 | } 46 | 47 | 上面的实例代码对应的happens-before关系如下图所示: 48 | 49 | ![VolatileExample的happens-before关系推导](http://upload-images.jianshu.io/upload_images/2615789-c9c291d6c0b3e0f1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 50 | 51 | 52 | 加锁线程A先执行writer方法,然后线程B执行reader方法图中每一个箭头两个节点就代码一个happens-before关系,黑色的代表根据**程序顺序规则**推导出来,红色的是根据**volatile变量的写happens-before 于任意后续对volatile变量的读**,而蓝色的就是根据传递性规则推导出来的。这里的2 happen-before 3,同样根据happens-before规则定义:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B的执行顺序,我们可以知道操作2执行结果对操作3来说是可见的,也就是说当线程A将volatile变量 flag更改为true后线程B就能够迅速感知。 53 | # 4. volatile的内存语义 # 54 | 还是按照**两个核心**的分析方式,分析完happens-before关系后我们现在就来进一步分析volatile的内存语义(按照这种方式去学习,会不会让大家对知识能够把握的更深,而不至于不知所措,如果大家认同我的这种方式,不妨给个赞,小弟在此谢过,对我是个鼓励)。还是以上面的代码为例,假设线程A先执行writer方法,线程B随后执行reader方法,初始时线程的本地内存中flag和a都是初始状态,下图是线程A执行volatile写后的状态图。 55 | 56 | ![线程A执行volatile写后的内存状态图](http://upload-images.jianshu.io/upload_images/2615789-9e5098f09d5ad065.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 57 | 58 | 59 | 当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。下图就展示了线程B读取同一个volatile变量的内存变化示意图。 60 | 61 | ![线程B读volatile后的内存状态图](http://upload-images.jianshu.io/upload_images/2615789-606771789255958f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 62 | 63 | 64 | 从横向来看,线程A和线程B之间进行了一次通信,线程A在写volatile变量时,实际上就像是给B发送了一个消息告诉线程B你现在的值都是旧的了,然后线程B读这个volatile变量时就像是接收了线程A刚刚发送的消息。既然是旧的了,那线程B该怎么办了?自然而然就只能去主内存去取啦。 65 | 66 | 好的,我们现在**两个核心**:happens-before以及内存语义现在已经都了解清楚了。是不是还不过瘾,突然发现原来自己会这么爱学习(微笑脸),那我们下面就再来一点干货----volatile内存语义的实现。 67 | 68 | ## 4.1 volatile的内存语义实现 ## 69 | 我们都知道,为了性能优化,JMM在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序,那如果想阻止重排序要怎么办了?答案是可以添加内存屏障。 70 | 71 | > **内存屏障** 72 | 73 | JMM内存屏障分为四类见下图, 74 | 75 | ![内存屏障分类表](http://upload-images.jianshu.io/upload_images/2615789-27cf04634cbdf284.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/680) 76 | 77 | 78 | java编译器会在生成指令系列时在适当的位置会插入内存屏障指令来禁止特定类型的处理器重排序。为了实现volatile的内存语义,JMM会限制特定类型的编译器和处理器重排序,JMM会针对编译器制定volatile重排序规则表: 79 | 80 | 81 | ![volatile重排序规则表](http://upload-images.jianshu.io/upload_images/2615789-fa62c72e7ec4ccb0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/680) 82 | 83 | 84 | "NO"表示禁止重排序。为了实现volatile内存语义时,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的**处理器重排序**。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎是不可能的,为此,JMM采取了保守策略: 85 | 86 | 1. 在每个volatile写操作的**前面**插入一个StoreStore屏障; 87 | 2. 在每个volatile写操作的**后面**插入一个StoreLoad屏障; 88 | 3. 在每个volatile读操作的**后面**插入一个LoadLoad屏障; 89 | 4. 在每个volatile读操作的**后面**插入一个LoadStore屏障。 90 | 91 | 需要注意的是:volatile写是在前面和后面**分别插入内存屏障**,而volatile读操作是在**后面插入两个内存屏障** 92 | 93 | **StoreStore屏障**:禁止上面的普通写和下面的volatile写重排序; 94 | 95 | **StoreLoad屏障**:防止上面的volatile写与下面可能有的volatile读/写重排序 96 | 97 | **LoadLoad屏障**:禁止下面所有的普通读操作和上面的volatile读重排序 98 | 99 | **LoadStore屏障**:禁止下面所有的普通写操作和上面的volatile读重排序 100 | 101 | 下面以两个示意图进行理解,图片摘自相当好的一本书《java并发编程的艺术》。 102 | 103 | ![volatile写插入内存屏障示意图](http://upload-images.jianshu.io/upload_images/2615789-a31dbae587e8a946.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/620) 104 | 105 | 106 | ![volatile读插入内存屏障示意图](http://upload-images.jianshu.io/upload_images/2615789-dc628461898a66a6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/620) 107 | # 5. 一个示例 # 108 | 我们现在已经理解volatile的精华了,文章开头的那个问题我想现在我们都能给出答案了。更正后的代码为: 109 | 110 | public class VolatileDemo { 111 | private static volatile boolean isOver = false; 112 | 113 | public static void main(String[] args) { 114 | Thread thread = new Thread(new Runnable() { 115 | @Override 116 | public void run() { 117 | while (!isOver) ; 118 | } 119 | }); 120 | thread.start(); 121 | try { 122 | Thread.sleep(500); 123 | } catch (InterruptedException e) { 124 | e.printStackTrace(); 125 | } 126 | isOver = true; 127 | } 128 | } 129 | 130 | 注意不同点,现在已经**将isOver设置成了volatile变量**,这样在main线程中将isOver改为了true后,thread的工作内存该变量值就会失效,从而需要再次从主内存中读取该值,现在能够读出isOver最新值为true从而能够结束在thread里的死循环,从而能够顺利停止掉thread线程。现在问题也解决了,知识也学到了:)。(如果觉得还不错,请点赞,是对我的一个鼓励。) 131 | 132 | > 参考文献 133 | 134 | 《java并发编程的艺术》 -------------------------------------------------------------------------------- /05.彻底理解volatile/volatile写插入内存屏障示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/05.彻底理解volatile/volatile写插入内存屏障示意图.png -------------------------------------------------------------------------------- /05.彻底理解volatile/volatile读插入内存屏障示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/05.彻底理解volatile/volatile读插入内存屏障示意图.png -------------------------------------------------------------------------------- /05.彻底理解volatile/volatile重排序规则表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/05.彻底理解volatile/volatile重排序规则表.png -------------------------------------------------------------------------------- /05.彻底理解volatile/内存屏障分类表.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/05.彻底理解volatile/内存屏障分类表.png -------------------------------------------------------------------------------- /05.彻底理解volatile/线程A执行volatile写后的内存状态图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/05.彻底理解volatile/线程A执行volatile写后的内存状态图.png -------------------------------------------------------------------------------- /05.彻底理解volatile/线程B读volatile后的内存状态图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/05.彻底理解volatile/线程B读volatile后的内存状态图.png -------------------------------------------------------------------------------- /06.你以为你真的了解final吗?/final修饰局部变量.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/06.你以为你真的了解final吗?/final修饰局部变量.png -------------------------------------------------------------------------------- /06.你以为你真的了解final吗?/final修饰成员变量.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/06.你以为你真的了解final吗?/final修饰成员变量.png -------------------------------------------------------------------------------- /06.你以为你真的了解final吗?/final域写可能的存在的执行时序.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/06.你以为你真的了解final吗?/final域写可能的存在的执行时序.png -------------------------------------------------------------------------------- /06.你以为你真的了解final吗?/final域引用可能的执行时序.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/06.你以为你真的了解final吗?/final域引用可能的执行时序.png -------------------------------------------------------------------------------- /06.你以为你真的了解final吗?/final域读可能存在的执行时序.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/06.你以为你真的了解final吗?/final域读可能存在的执行时序.png -------------------------------------------------------------------------------- /06.你以为你真的了解final吗?/final方法不能重写.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/06.你以为你真的了解final吗?/final方法不能重写.png -------------------------------------------------------------------------------- /06.你以为你真的了解final吗?/final类不能继承.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/06.你以为你真的了解final吗?/final类不能继承.png -------------------------------------------------------------------------------- /06.你以为你真的了解final吗?/java关键字--final.md: -------------------------------------------------------------------------------- 1 | # 1. final的简介 # 2 | final可以修饰**变量,方法和类**,用于表示所修饰的内容一旦赋值之后就不会再被改变,比如String类就是一个final类型的类。即使能够知道final具体的使用方法,我想对**final在多线程中存在的重排序问题**也很容易忽略,希望能够一起做下探讨。 3 | # 2. final的具体使用场景 # 4 | final能够修饰变量,方法和类,也就是final使用范围基本涵盖了java每个地方,下面就分别以锁修饰的位置:变量,方法和类分别来说一说。 5 | ## 2.1 变量 ## 6 | 在java中变量,可以分为**成员变量**以及方法**局部变量**。因此也是按照这种方式依次来说,以避免漏掉任何一个死角。 7 | ### 2.1.1 final成员变量 ### 8 | 通常每个类中的成员变量可以分为**类变量(static修饰的变量)以及实例变量**。针对这两种类型的变量赋初值的时机是不同的,类变量可以在声明变量的时候直接赋初值或者在静态代码块中给类变量赋初值。而实例变量可以在声明变量的时候给实例变量赋初值,在非静态初始化块中以及构造器中赋初值。类变量有**两个时机赋初值**,而实例变量则可以有**三个时机赋初值**。当final变量未初始化时系统不会进行隐式初始化,会出现报错。这样说起来还是比较抽象,下面用具体的代码来演示。(代码涵盖了final修饰变量所有的可能情况,耐心看下去会有收获的:) ) 9 | 10 | 11 | ![final修饰成员变量](http://upload-images.jianshu.io/upload_images/2615789-768017317b5fab78.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 12 | 13 | 看上面的图片已经将每种情况整理出来了,这里用截图的方式也是觉得在IDE出现红色出错的标记更能清晰的说明情况。现在我们来将这几种情况归纳整理一下: 14 | 15 | 1. **类变量**:必须要在**静态初始化块**中指定初始值或者**声明该类变量时**指定初始值,而且只能在这**两个地方**之一进行指定; 16 | 2. **实例变量**:必要要在**非静态初始化块**,**声明该实例变量**或者在**构造器中**指定初始值,而且只能在这**三个地方**进行指定。 17 | ### 2.2.2 final局部变量 ### 18 | final局部变量由程序员进行显式初始化,如果final局部变量已经进行了初始化则后面就不能再次进行更改,如果final变量未进行初始化,可以进行赋值,**当且仅有一次**赋值,一旦赋值之后再次赋值就会出错。下面用具体的代码演示final局部变量的情况: 19 | 20 | ![final修饰局部变量](http://upload-images.jianshu.io/upload_images/2615789-7077bdb169d4d1c3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 21 | 22 | 现在我们来换一个角度进行考虑,final修饰的是基本数据类型和引用类型有区别吗? 23 | 24 | > **final基本数据类型 VS final引用数据类型** 25 | 26 | 通过上面的例子我们已经看出来,如果final修饰的是一个基本数据类型的数据,一旦赋值后就不能再次更改,那么,如果final是引用数据类型了?这个引用的对象能够改变吗?我们同样来看一段代码。 27 | 28 | public class FinalExample { 29 | //在声明final实例成员变量时进行赋值 30 | private final static Person person = new Person(24, 170); 31 | public static void main(String[] args) { 32 | //对final引用数据类型person进行更改 33 | person.age = 22; 34 | System.out.println(person.toString()); 35 | } 36 | static class Person { 37 | private int age; 38 | private int height; 39 | 40 | public Person(int age, int height) { 41 | this.age = age; 42 | this.height = height; 43 | } 44 | @Override 45 | public String toString() { 46 | return "Person{" + 47 | "age=" + age + 48 | ", height=" + height + 49 | '}'; 50 | } 51 | } 52 | } 53 | 54 | 当我们对final修饰的引用数据类型变量person的属性改成22,是可以成功操作的。通过这个实验我们就可以看出来**当final修饰基本数据类型变量时,不能对基本数据类型变量重新赋值,因此基本数据类型变量不能被改变。而对于引用类型变量而言,它仅仅保存的是一个引用,final只保证这个引用类型变量所引用的地址不会发生改变,即一直引用这个对象,但这个对象属性是可以改变的**。 55 | 56 | > **宏变量** 57 | 58 | 利用final变量的不可更改性,在满足一下三个条件时,该变量就会成为一个“宏变量”,即是一个常量。 59 | 60 | 1. 使用final修饰符修饰; 61 | 2. 在定义该final变量时就指定了初始值; 62 | 3. 该初始值在编译时就能够唯一指定。 63 | 64 | 注意:当程序中其他地方使用该宏变量的地方,编译器会直接替换成该变量的值 65 | 66 | ## 2.2 方法 ## 67 | > **重写?** 68 | 69 | 当父类的方法被final修饰的时候,子类不能重写父类的该方法,比如在Object中,getClass()方法就是final的,我们就不能重写该方法,但是hashCode()方法就不是被final所修饰的,我们就可以重写hashCode()方法。我们还是来写一个例子来加深一下理解: 70 | 先定义一个父类,里面有final修饰的方法test(); 71 | 72 | public class FinalExampleParent { 73 | public final void test() { 74 | } 75 | } 76 | 77 | 然后FinalExample继承该父类,当重写test()方法时出现报错,如下图: 78 | 79 | ![final方法不能重写](http://upload-images.jianshu.io/upload_images/2615789-5d831da449f512e9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 80 | 81 | 通过这个现象我们就可以看出来**被final修饰的方法不能够被子类所重写**。 82 | 83 | > **重载?** 84 | 85 | public class FinalExampleParent { 86 | public final void test() { 87 | } 88 | 89 | public final void test(String str) { 90 | } 91 | } 92 | 93 | 可以看出被final修饰的方法是可以重载的。经过我们的分析可以得出如下结论: 94 | 95 | **1. 父类的final方法是不能够被子类重写的** 96 | 97 | **2. final方法是可以被重载的** 98 | ## 2.3 类 ## 99 | **当一个类被final修饰时,表名该类是不能被子类继承的**。子类继承往往可以重写父类的方法和改变父类属性,会带来一定的安全隐患,因此,当一个类不希望被继承时就可以使用final修饰。还是来写一个小例子: 100 | 101 | public final class FinalExampleParent { 102 | public final void test() { 103 | } 104 | } 105 | 106 | 父类会被final修饰,当子类继承该父类的时候,就会报错,如下图: 107 | 108 | ![final类不能继承](http://upload-images.jianshu.io/upload_images/2615789-835b66d960e21e2e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 109 | 110 | # 3. final的例子 # 111 | final经常会被用作不变类上,利用final的不可更改性。我们先来看看什么是不变类。 112 | > 不变类 113 | 114 | 不变类的意思是创建该类的实例后,该实例的实例变量是不可改变的。满足以下条件则可以成为不可变类: 115 | 116 | 1. 使用private和final修饰符来修饰该类的成员变量 117 | 2. 提供带参的构造器用于初始化类的成员变量; 118 | 3. 仅为该类的成员变量提供getter方法,不提供setter方法,因为普通方法无法修改fina修饰的成员变量; 119 | 4. 如果有必要就重写Object类 的hashCode()和equals()方法,应该保证用equals()判断相同的两个对象其Hashcode值也是相等的。 120 | 121 | JDK中提供的八个包装类和String类都是不可变类,我们来看看String的实现。 122 | 123 | /** The value is used for character storage. */ 124 | private final char value[]; 125 | 可以看出String的value就是final修饰的,上述其他几条性质也是吻合的。 126 | 127 | # 4. 多线程中你真的了解final吗? # 128 | 上面我们聊的final使用,应该属于**Java基础层面**的,当理解这些后我们就真的算是掌握了final吗?有考虑过final在多线程并发的情况吗?在[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)中我们知道java内存模型为了能让处理器和编译器底层发挥他们的最大优势,对底层的约束就很少,也就是说针对底层来说java内存模型就是一弱内存数据模型。同时,处理器和编译为了性能优化会对指令序列有**编译器和处理器重排序**。那么,在多线程情况下,final会进行怎样的重排序?会导致线程安全的问题吗?下面,就来看看final的重排序。 129 | 130 | ## 4.1 final域重排序规则 ## 131 | 132 | ### 4.1.1 final域为基本类型 ### 133 | 先看一段示例性的代码: 134 | 135 | public class FinalDemo { 136 | private int a; //普通域 137 | private final int b; //final域 138 | private static FinalDemo finalDemo; 139 | 140 | public FinalDemo() { 141 | a = 1; // 1. 写普通域 142 | b = 2; // 2. 写final域 143 | } 144 | 145 | public static void writer() { 146 | finalDemo = new FinalDemo(); 147 | } 148 | 149 | public static void reader() { 150 | FinalDemo demo = finalDemo; // 3.读对象引用 151 | int a = demo.a; //4.读普通域 152 | int b = demo.b; //5.读final域 153 | } 154 | } 155 | 156 | 假设线程A在执行writer()方法,线程B执行reader()方法。 157 | 158 | > **写final域重排序规则** 159 | 160 | 161 | 写final域的重排序规则**禁止对final域的写重排序到构造函数之外**,这个规则的实现主要包含了两个方面: 162 | 163 | 1. JMM禁止编译器把final域的写重排序到构造函数之外; 164 | 2. 编译器会在final域写之后,构造函数return之前,插入一个storestore屏障(关于内存屏障可以看[这篇文章](https://juejin.im/post/5ae6d309518825673123fd0e))。这个屏障可以禁止处理器把final域的写重排序到构造函数之外。 165 | 166 | 我们再来分析writer方法,虽然只有一行代码,但实际上做了两件事情: 167 | 168 | 1. 构造了一个FinalDemo对象; 169 | 2. 把这个对象赋值给成员变量finalDemo。 170 | 171 | 我们来画下存在的一种可能执行时序图,如下: 172 | 173 | ![final域写可能的存在的执行时序](http://upload-images.jianshu.io/upload_images/2615789-9e3937df955a9862.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) 174 | 175 | 由于a,b之间没有数据依赖性,普通域(普通变量)a可能会被重排序到构造函数之外,线程B就有可能读到的是普通变量a初始化之前的值(零值),这样就可能出现错误。而final域变量b,根据重排序规则,会禁止final修饰的变量b重排序到构造函数之外,从而b能够正确赋值,线程B就能够读到final变量初始化后的值。 176 | 177 | 因此,写final域的重排序规则可以确保:**在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域就不具有这个保障**。比如在上例,线程B有可能就是一个未正确初始化的对象finalDemo。 178 | 179 | 180 | >**读final域重排序规则** 181 | 182 | 读final域重排序规则为:**在一个线程中,初次读对象引用和初次读该对象包含的final域,JMM会禁止这两个操作的重排序。**(注意,这个规则仅仅是针对处理器),处理器会在读final域操作的前面插入一个LoadLoad屏障。实际上,读对象的引用和读该对象的final域存在间接依赖性,一般处理器不会重排序这两个操作。但是有一些处理器会重排序,因此,这条禁止重排序规则就是针对这些处理器而设定的。 183 | 184 | read()方法主要包含了三个操作: 185 | 186 | 1. 初次读引用变量finalDemo; 187 | 2. 初次读引用变量finalDemo的普通域a; 188 | 3. 初次读引用变量finalDemo的final与b; 189 | 190 | 假设线程A写过程没有重排序,那么线程A和线程B有一种的可能执行时序为下图: 191 | 192 | 193 | ![final域读可能存在的执行时序](http://upload-images.jianshu.io/upload_images/2615789-2a93b67948d7fc64.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) 194 | 195 | 读对象的普通域被重排序到了读对象引用的前面就会出现线程B还未读到对象引用就在读取该对象的普通域变量,这显然是错误的操作。而final域的读操作就“限定”了在读final域变量前已经读到了该对象的引用,从而就可以避免这种情况。 196 | 197 | 读final域的重排序规则可以确保:**在读一个对象的final域之前,一定会先读这个包含这个final域的对象的引用。** 198 | 199 | 200 | 201 | ### 4.1.2 final域为引用类型 ### 202 | 我们已经知道了final域是基本数据类型的时候重排序规则是怎么的了?如果是**引用数据类型**了?我们接着继续来探讨。 203 | 204 | > **对final修饰的对象的成员域写操作** 205 | 206 | 针对引用数据类型,final域写针对编译器和处理器重排序**增加了这样的约束**:在构造函数内对**一个final修饰的对象的成员域的写入,与随后在构造函数之外把这个被构造的对象的引用赋给一个引用变量**,这两个操作是不能被重排序的。注意这里的是“增加”也就说前面对final基本数据类型的重排序规则在这里还是使用。这句话是比较拗口的,下面结合实例来看。 207 | 208 | 209 | public class FinalReferenceDemo { 210 | final int[] arrays; 211 | private FinalReferenceDemo finalReferenceDemo; 212 | 213 | public FinalReferenceDemo() { 214 | arrays = new int[1]; //1 215 | arrays[0] = 1; //2 216 | } 217 | 218 | public void writerOne() { 219 | finalReferenceDemo = new FinalReferenceDemo(); //3 220 | } 221 | 222 | public void writerTwo() { 223 | arrays[0] = 2; //4 224 | } 225 | 226 | public void reader() { 227 | if (finalReferenceDemo != null) { //5 228 | int temp = finalReferenceDemo.arrays[0]; //6 229 | } 230 | } 231 | } 232 | 233 | 234 | 针对上面的实例程序,线程线程A执行wirterOne方法,执行完后线程B执行writerTwo方法,然后线程C执行reader方法。下图就以这种执行时序出现的一种情况来讨论(耐心看完才有收获)。 235 | 236 | 237 | ![写final修饰引用类型数据可能的执行时序](http://upload-images.jianshu.io/upload_images/2615789-1f5f0a39a3f6977e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/800) 238 | 239 | 由于对final域的写禁止重排序到构造方法外,因此1和3不能被重排序。由于一个final域的引用对象的成员域写入不能与随后将这个被构造出来的对象赋给引用变量重排序,因此2和3不能重排序。 240 | 241 | > **对final修饰的对象的成员域读操作** 242 | 243 | JMM可以确保线程C至少能看到写线程A对final引用的对象的成员域的写入,即能看下arrays[0] = 1,而写线程B对数组元素的写入可能看到可能看不到。JMM不保证线程B的写入对线程C可见,线程B和线程C之间存在数据竞争,此时的结果是不可预知的。如果可见的,可使用锁或者volatile。 244 | 245 | >**关于final重排序的总结** 246 | 247 | 按照final修饰的数据类型分类: 248 | 249 | 基本数据类型: 250 | 251 | 1. final域写:禁止**final域写**与**构造方法**重排序,即禁止final域写重排序到构造方法之外,从而保证该对象对所有线程可见时,该对象的final域全部已经初始化过。 252 | 2. final域读:禁止初次**读对象的引用**与**读该对象包含的final域**的重排序。 253 | 254 | 引用数据类型: 255 | 256 | 额外增加约束:禁止在构造函数对**一个final修饰的对象的成员域的写入**与随后将**这个被构造的对象的引用赋值给引用变量** 重排序 257 | 258 | # 5.final的实现原理 # 259 | 上面我们提到过,写final域会要求编译器在final域写之后,构造函数返回前插入一个StoreStore屏障。读final域的重排序规则会要求编译器在读final域的操作前插入一个LoadLoad屏障。 260 | 261 | 很有意思的是,如果以X86处理为例,X86不会对写-写重排序,所以**StoreStore屏障可以省略**。由于**不会对有间接依赖性的操作重排序**,所以在X86处理器中,读final域需要的**LoadLoad屏障也会被省略掉**。也就是说,**以X86为例的话,对final域的读/写的内存屏障都会被省略**!具体是否插入还是得看是什么处理器 262 | 263 | # 6. 为什么final引用不能从构造函数中“溢出” # 264 | 这里还有一个比较有意思的问题:上面对final域写重排序规则可以确保我们在使用一个对象引用的时候该对象的final域已经在构造函数被初始化过了。但是这里其实是有一个前提条件的,也就是:**在构造函数,不能让这个被构造的对象被其他线程可见,也就是说该对象引用不能在构造函数中“逸出”**。以下面的例子来说: 265 | 266 | public class FinalReferenceEscapeDemo { 267 | private final int a; 268 | private FinalReferenceEscapeDemo referenceDemo; 269 | 270 | public FinalReferenceEscapeDemo() { 271 | a = 1; //1 272 | referenceDemo = this; //2 273 | } 274 | 275 | public void writer() { 276 | new FinalReferenceEscapeDemo(); 277 | } 278 | 279 | public void reader() { 280 | if (referenceDemo != null) { //3 281 | int temp = referenceDemo.a; //4 282 | } 283 | } 284 | } 285 | 286 | 可能的执行时序如图所示: 287 | 288 | ![final域引用可能的执行时序](http://upload-images.jianshu.io/upload_images/2615789-e020492056ee1242.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 289 | 290 | 假设一个线程A执行writer方法另一个线程执行reader方法。因为构造函数中操作1和2之间没有数据依赖性,1和2可以重排序,先执行了2,这个时候引用对象referenceDemo是个没有完全初始化的对象,而当线程B去读取该对象时就会出错。尽管依然满足了final域写重排序规则:在引用对象对所有线程可见时,其final域已经完全初始化成功。但是,引用对象“this”逸出,该代码依然存在线程安全的问题。 291 | 292 | 293 | > 参看文献 294 | 295 | 《java并发编程的艺术》 296 | 297 | 《疯狂java讲义》 -------------------------------------------------------------------------------- /06.你以为你真的了解final吗?/写final修饰引用类型数据可能的执行时序.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/06.你以为你真的了解final吗?/写final修饰引用类型数据可能的执行时序.png -------------------------------------------------------------------------------- /07.三大性质总结:原子性、可见性以及有序性/三大性质总结:原子性、可见性以及有序性.md: -------------------------------------------------------------------------------- 1 | # 1. 三大性质简介 # 2 | 在并发编程中分析线程安全的问题时往往需要切入点,那就是**两大核心**:JMM抽象内存模型以及happens-before规则(在[这篇文章](https://juejin.im/post/5ae6d309518825673123fd0e)中已经经过了),三条性质:**原子性,有序性和可见性**。关于[synchronized](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)和[volatile](https://juejin.im/post/5ae9b41b518825670b33e6c4)已经讨论过了,就想着将并发编程中这两大神器在 **原子性,有序性和可见性**上做一个比较,当然这也是面试中的高频考点,值得注意。 3 | 4 | # 2. 原子性 # 5 | 原子性是指**一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉**。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。我们先来看看哪些是原子操作,哪些不是原子操作,有一个直观的印象: 6 | 7 | > int a = 10; //1 8 | > 9 | > a++; //2 10 | > 11 | > int b=a; //3 12 | > 13 | > a = a+1; //4 14 | 15 | 上面这四个语句中只**有第1个语句是原子操作**,将10赋值给线程工作内存的变量a,而语句2(a++),实际上包含了三个操作:1. 读取变量a的值;2:对a进行加一的操作;3.将计算后的值再赋值给变量a,而这三个操作无法构成原子操作。对语句3,4的分析同理可得这两条语句不具备原子性。当然,[java内存模型](https://juejin.im/post/5ae6d309518825673123fd0e)中定义了8中操作都是原子的,不可再分的。 16 | 17 | 1. lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态; 18 | 2. unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定 19 | 3. read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用; 20 | 4. load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本 21 | 5. use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作; 22 | 6. assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作; 23 | 7. store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用; 24 | 8. write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。 25 | 26 | 上面的这些指令操作是相当底层的,可以作为扩展知识面掌握下。那么如何理解这些指令了?比如,把一个变量从主内存中复制到工作内存中就需要执行read,load操作,将工作内存同步到主内存中就需要执行store,write操作。注意的是:**java内存模型只是要求上述两个操作是顺序执行的并不是连续执行的**。也就是说read和load之间可以插入其他指令,store和writer可以插入其他指令。比如对主内存中的a,b进行访问就可以出现这样的操作顺序:**read a,read b, load b,load a**。 27 | 28 | 由原子性变量操作read,load,use,assign,store,write,可以**大致认为基本数据类型的访问读写具备原子性**(例外就是long和double的非原子性协定) 29 | 30 | > synchronized 31 | 32 | 上面一共有八条原子操作,其中六条可以满足基本数据类型的访问读写具备原子性,还剩下lock和unlock两条原子操作。如果我们需要更大范围的原子性操作就可以使用lock和unlock原子操作。尽管jvm没有把lock和unlock开放给我们使用,但jvm以更高层次的指令monitorenter和monitorexit指令开放给我们使用,反应到java代码中就是---synchronized关键字,也就是说**synchronized满足原子性**。 33 | 34 | > volatile 35 | 我们先来看这样一个例子: 36 | 37 | public class VolatileExample { 38 | private static volatile int counter = 0; 39 | 40 | public static void main(String[] args) { 41 | for (int i = 0; i < 10; i++) { 42 | Thread thread = new Thread(new Runnable() { 43 | @Override 44 | public void run() { 45 | for (int i = 0; i < 10000; i++) 46 | counter++; 47 | } 48 | }); 49 | thread.start(); 50 | } 51 | try { 52 | Thread.sleep(1000); 53 | } catch (InterruptedException e) { 54 | e.printStackTrace(); 55 | } 56 | System.out.println(counter); 57 | } 58 | } 59 | 60 | 开启10个线程,每个线程都自加10000次,如果不出现线程安全的问题最终的结果应该就是:10*10000 = 100000;可是运行多次都是小于100000的结果,问题在于 **volatile并不能保证原子性**,在前面说过counter++这并不是一个原子操作,包含了三个步骤:1.读取变量counter的值;2.对counter加一;3.将新值赋值给变量counter。如果线程A读取counter到工作内存后,其他线程对这个值已经做了自增操作后,那么线程A的这个值自然而然就是一个过期的值,因此,总结果必然会是小于100000的。 61 | 62 | 如果让volatile保证原子性,必须符合以下两条规则: 63 | 64 | 1. **运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值;** 65 | 2. **变量不需要与其他的状态变量共同参与不变约束** 66 | 67 | # 3. 有序性 # 68 | 69 | > synchronized 70 | 71 | synchronized语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。因此,synchronized语义就要求线程在访问读写共享变量时只能“串行”执行,因此**synchronized具有有序性**。 72 | 73 | > volatile 74 | 75 | 在java内存模型中说过,为了性能优化,编译器和处理器会进行指令重排序;也就是说java程序天然的有序性可以总结为:**如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另一个线程,所有的操作都是无序的**。在单例模式的实现上有一种双重检验锁定的方式(Double-checked Locking)。代码如下: 76 | 77 | 78 | public class Singleton { 79 | private Singleton() { } 80 | private volatile static Singleton instance; 81 | public Singleton getInstance(){ 82 | if(instance==null){ 83 | synchronized (Singleton.class){ 84 | if(instance==null){ 85 | instance = new Singleton(); 86 | } 87 | } 88 | } 89 | return instance; 90 | } 91 | } 92 | 93 | 这里为什么要加volatile了?我们先来分析一下不加volatile的情况,有问题的语句是这条: 94 | > instance = new Singleton(); 95 | 96 | 这条语句实际上包含了三个操作:1.分配对象的内存空间;2.初始化对象;3.设置instance指向刚分配的内存地址。但由于存在重排序的问题,可能有以下的执行顺序: 97 | 98 | ![不加volatile可能的执行时序](http://upload-images.jianshu.io/upload_images/2615789-e7931260b0449eb1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 99 | 100 | 101 | 如果2和3进行了重排序的话,线程B进行判断if(instance==null)时就会为true,而实际上这个instance并没有初始化成功,显而易见对线程B来说之后的操作就会是错得。而**用volatile修饰**的话就可以禁止2和3操作重排序,从而避免这种情况。**volatile包含禁止指令重排序的语义,其具有有序性**。 102 | # 4. 可见性 # 103 | 104 | 可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。通过之前对[synchronzed](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,**synchronized具有可见性**。同样的在[volatile分析中](https://juejin.im/post/5ae9b41b518825670b33e6c4),会通过在指令中添加**lock指令**,以实现内存可见性。因此, **volatile具有可见性** 105 | 106 | # 5. 总结 # 107 | 通过这篇文章,主要是比较了synchronized和volatile在三条性质:原子性,可见性,以及有序性的情况,归纳如下: 108 | 109 | > **synchronized: 具有原子性,有序性和可见性**; 110 | > **volatile:具有有序性和可见性** 111 | 112 | 113 | > 参考文献 114 | 115 | 《java并发编程的艺术》 116 | 《深入理解java虚拟机》 -------------------------------------------------------------------------------- /07.三大性质总结:原子性、可见性以及有序性/不加volatile可能的执行时序.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/07.三大性质总结:原子性、可见性以及有序性/不加volatile可能的执行时序.png -------------------------------------------------------------------------------- /08.初识Lock与AbstractQueuedSynchronizer(AQS)/AQS可重写的方法.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/08.初识Lock与AbstractQueuedSynchronizer(AQS)/AQS可重写的方法.png -------------------------------------------------------------------------------- /08.初识Lock与AbstractQueuedSynchronizer(AQS)/AQS提供的模板方法.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/08.初识Lock与AbstractQueuedSynchronizer(AQS)/AQS提供的模板方法.png -------------------------------------------------------------------------------- /08.初识Lock与AbstractQueuedSynchronizer(AQS)/concurrent包实现整体示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/08.初识Lock与AbstractQueuedSynchronizer(AQS)/concurrent包实现整体示意图.png -------------------------------------------------------------------------------- /08.初识Lock与AbstractQueuedSynchronizer(AQS)/concurrent目录结构.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/08.初识Lock与AbstractQueuedSynchronizer(AQS)/concurrent目录结构.png -------------------------------------------------------------------------------- /08.初识Lock与AbstractQueuedSynchronizer(AQS)/mutex的执行情况.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/08.初识Lock与AbstractQueuedSynchronizer(AQS)/mutex的执行情况.png -------------------------------------------------------------------------------- /08.初识Lock与AbstractQueuedSynchronizer(AQS)/初识Lock与AbstractQueuedSynchronizer(AQS).md: -------------------------------------------------------------------------------- 1 | # 1. concurrent包的结构层次 # 2 | 在针对并发编程中,Doug Lea大师为我们提供了大量实用,高性能的工具类,针对这些代码进行研究会让我们队并发编程的掌握更加透彻也会大大提升我们队并发编程技术的热爱。这些代码在java.util.concurrent包下。如下图,即为concurrent包的目录结构图。 3 | 4 | ![concurrent目录结构.png](http://upload-images.jianshu.io/upload_images/2615789-da951eb99c5dabfd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 5 | 6 | 7 | 其中包含了两个子包:atomic以及lock,另外在concurrent下的阻塞队列以及executors,这些就是concurrent包中的精华,之后会一一进行学习。而这些类的实现主要是依赖于volatile以及CAS(关于volatile可以看[这篇文章](https://juejin.im/post/5ae9b41b518825670b33e6c4),关于CAS可以看[这篇文章的3.1节](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)),从整体上来看concurrent包的整体实现图如下图所示: 8 | 9 | ![concurrent包实现整体示意图.png](http://upload-images.jianshu.io/upload_images/2615789-24da822ddc226b03.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 10 | 11 | 12 | # 2. lock简介 # 13 | 我们下来看concurent包下的lock子包。锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源。在Lock接口出现之前,java程序主要是靠synchronized关键字实现锁功能的,而java SE5之后,并发包中增加了lock接口,它提供了与synchronized一样的锁功能。**虽然它失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。**通常使用显示使用lock的形式如下: 14 | 15 | Lock lock = new ReentrantLock(); 16 | lock.lock(); 17 | try{ 18 | ....... 19 | }finally{ 20 | lock.unlock(); 21 | } 22 | 23 | 需要注意的是**synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁,因此在finally块中释放锁**。 24 | 25 | ## 2.1 Lock接口API ## 26 | 27 | 我们现在就来看看lock接口定义了哪些方法: 28 | 29 | > void lock(); //获取锁 30 | > void lockInterruptibly() throws InterruptedException;//获取锁的过程能够响应中断 31 | > boolean tryLock();//非阻塞式响应中断能立即返回,获取锁放回true反之返回fasle 32 | > boolean tryLock(long time, TimeUnit unit) throws InterruptedException;//超时获取锁,在超时内或者未中断的情况下能够获取锁 33 | > Condition newCondition();//获取与lock绑定的等待通知组件,当前线程必须获得了锁才能进行等待,进行等待时会先释放锁,当再次获取锁时才能从等待中返回 34 | 35 | 上面是lock接口下的五个方法,也只是从源码中英译中翻译了一遍,感兴趣的可以自己的去看看。那么在locks包下有哪些类实现了该接口了?先从最熟悉的ReentrantLock说起。 36 | 37 | > public class ReentrantLock implements **Lock**, java.io.Serializable 38 | 39 | 很显然ReentrantLock实现了lock接口,接下来我们来仔细研究一下它是怎样实现的。当你查看源码时你会惊讶的发现ReentrantLock并没有多少代码,另外有一个很明显的特点是:**基本上所有的方法的实现实际上都是调用了其静态内存类`Sync`中的方法,而Sync类继承了`AbstractQueuedSynchronizer(AQS)`**。可以看出要想理解ReentrantLock关键核心在于对队列同步器AbstractQueuedSynchronizer(简称同步器)的理解。 40 | 41 | ## 2.2 初识AQS ## 42 | 43 | 关于AQS在源码中有十分具体的解释: 44 | 45 | Provides a framework for implementing blocking locks and related 46 | synchronizers (semaphores, events, etc) that rely on 47 | first-in-first-out (FIFO) wait queues. This class is designed to 48 | be a useful basis for most kinds of synchronizers that rely on a 49 | single atomic {@code int} value to represent state. Subclasses 50 | must define the protected methods that change this state, and which 51 | define what that state means in terms of this object being acquired 52 | or released. Given these, the other methods in this class carry 53 | out all queuing and blocking mechanics. Subclasses can maintain 54 | other state fields, but only the atomically updated {@code int} 55 | value manipulated using methods {@link #getState}, {@link 56 | #setState} and {@link #compareAndSetState} is tracked with respect 57 | to synchronization. 58 | 59 |

Subclasses should be defined as non-public internal helper 60 | classes that are used to implement the synchronization properties 61 | of their enclosing class. Class 62 | {@code AbstractQueuedSynchronizer} does not implement any 63 | synchronization interface. Instead it defines methods such as 64 | {@link #acquireInterruptibly} that can be invoked as 65 | appropriate by concrete locks and related synchronizers to 66 | implement their public methods. 67 | 68 | 同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成等待队列。它的**子类必须重写AQS的几个protected修饰的用来改变同步状态的方法**,其他方法主要是实现了排队和阻塞机制。**状态的更新使用getState,setState以及compareAndSetState这三个方法**。 69 | 70 | 子类被**推荐定义为自定义同步组件的静态内部类**,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以方便的实现不同类型的同步组件。 71 | 72 | 同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者的关系:**锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作**。锁和同步器很好的隔离了使用者和实现者所需关注的领域。 73 | 74 | ## 2.3 AQS的模板方法设计模式 ## 75 | 76 | AQS的设计是使用模板方法设计模式,它将**一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法**。举个例子,AQS中需要重写的方法tryAcquire: 77 | 78 | protected boolean tryAcquire(int arg) { 79 | throw new UnsupportedOperationException(); 80 | } 81 | 82 | ReentrantLock中NonfairSync(继承AQS)会重写该方法为: 83 | 84 | protected final boolean tryAcquire(int acquires) { 85 | return nonfairTryAcquire(acquires); 86 | } 87 | 而AQS中的模板方法acquire(): 88 | 89 | public final void acquire(int arg) { 90 | if (!tryAcquire(arg) && 91 | acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) 92 | selfInterrupt(); 93 | } 94 | 会调用tryAcquire方法,而此时当继承AQS的NonfairSync调用模板方法acquire时就会调用已经被NonfairSync重写的tryAcquire方法。这就是使用AQS的方式,在弄懂这点后会lock的实现理解有很大的提升。可以归纳总结为这么几点: 95 | 96 | 1. 同步组件(这里不仅仅值锁,还包括CountDownLatch等)的实现依赖于同步器AQS,在同步组件实现中,使用AQS的方式被推荐定义继承AQS的静态内存类; 97 | 2. AQS采用模板方法进行设计,AQS的protected修饰的方法需要由继承AQS的子类进行重写实现,当调用AQS的子类的方法时就会调用被重写的方法; 98 | 3. AQS负责同步状态的管理,线程的排队,等待和唤醒这些底层操作,而Lock等同步组件主要专注于实现同步语义; 99 | 4. 在重写AQS的方式时,使用AQS提供的`getState(),setState(),compareAndSetState()`方法进行修改同步状态 100 | 101 | AQS可重写的方法如下图(摘自《java并发编程的艺术》一书): 102 | 103 | ![AQS可重写的方法.png](http://upload-images.jianshu.io/upload_images/2615789-214b5823e76f8eb0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 104 | 105 | 在实现同步组件时AQS提供的模板方法如下图: 106 | 107 | ![AQS提供的模板方法.png](http://upload-images.jianshu.io/upload_images/2615789-33aa10c3be109206.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 108 | 109 | 110 | AQS提供的模板方法可以分为3类: 111 | 1. 独占式获取与释放同步状态; 112 | 2. 共享式获取与释放同步状态; 113 | 3. 查询同步队列中等待线程情况; 114 | 115 | 同步组件通过AQS提供的模板方法实现自己的同步语义。 116 | 117 | # 3. 一个例子 # 118 | 下面使用一个例子来进一步理解下AQS的使用。这个例子也是来源于AQS源码中的example。 119 | 120 | class Mutex implements Lock, java.io.Serializable { 121 | // Our internal helper class 122 | // 继承AQS的静态内存类 123 | // 重写方法 124 | private static class Sync extends AbstractQueuedSynchronizer { 125 | // Reports whether in locked state 126 | protected boolean isHeldExclusively() { 127 | return getState() == 1; 128 | } 129 | 130 | // Acquires the lock if state is zero 131 | public boolean tryAcquire(int acquires) { 132 | assert acquires == 1; // Otherwise unused 133 | if (compareAndSetState(0, 1)) { 134 | setExclusiveOwnerThread(Thread.currentThread()); 135 | return true; 136 | } 137 | return false; 138 | } 139 | 140 | // Releases the lock by setting state to zero 141 | protected boolean tryRelease(int releases) { 142 | assert releases == 1; // Otherwise unused 143 | if (getState() == 0) throw new IllegalMonitorStateException(); 144 | setExclusiveOwnerThread(null); 145 | setState(0); 146 | return true; 147 | } 148 | 149 | // Provides a Condition 150 | Condition newCondition() { 151 | return new ConditionObject(); 152 | } 153 | 154 | // Deserializes properly 155 | private void readObject(ObjectInputStream s) 156 | throws IOException, ClassNotFoundException { 157 | s.defaultReadObject(); 158 | setState(0); // reset to unlocked state 159 | } 160 | } 161 | 162 | // The sync object does all the hard work. We just forward to it. 163 | private final Sync sync = new Sync(); 164 | //使用同步器的模板方法实现自己的同步语义 165 | public void lock() { 166 | sync.acquire(1); 167 | } 168 | 169 | public boolean tryLock() { 170 | return sync.tryAcquire(1); 171 | } 172 | 173 | public void unlock() { 174 | sync.release(1); 175 | } 176 | 177 | public Condition newCondition() { 178 | return sync.newCondition(); 179 | } 180 | 181 | public boolean isLocked() { 182 | return sync.isHeldExclusively(); 183 | } 184 | 185 | public boolean hasQueuedThreads() { 186 | return sync.hasQueuedThreads(); 187 | } 188 | 189 | public void lockInterruptibly() throws InterruptedException { 190 | sync.acquireInterruptibly(1); 191 | } 192 | 193 | public boolean tryLock(long timeout, TimeUnit unit) 194 | throws InterruptedException { 195 | return sync.tryAcquireNanos(1, unit.toNanos(timeout)); 196 | } 197 | } 198 | 199 | MutexDemo: 200 | 201 | public class MutextDemo { 202 | private static Mutex mutex = new Mutex(); 203 | 204 | public static void main(String[] args) { 205 | for (int i = 0; i < 10; i++) { 206 | Thread thread = new Thread(() -> { 207 | mutex.lock(); 208 | try { 209 | Thread.sleep(3000); 210 | } catch (InterruptedException e) { 211 | e.printStackTrace(); 212 | } finally { 213 | mutex.unlock(); 214 | } 215 | }); 216 | thread.start(); 217 | } 218 | } 219 | } 220 | 221 | 执行情况: 222 | 223 | ![mutex的执行情况.png](http://upload-images.jianshu.io/upload_images/2615789-cabcd4a169178b5b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 224 | 225 | 上面的这个例子实现了独占锁的语义,在同一个时刻只允许一个线程占有锁。MutexDemo新建了10个线程,分别睡眠3s。从执行情况也可以看出来当前Thread-6正在执行占有锁而其他Thread-7,Thread-8等线程处于WAIT状态。按照推荐的方式,Mutex定义了一个**继承AQS的静态内部类Sync**,并且重写了AQS的tryAcquire等等方法,而对state的更新也是利用了setState(),getState(),compareAndSetState()这三个方法。在实现实现lock接口中的方法也只是调用了AQS提供的模板方法(因为Sync继承AQS)。从这个例子就可以很清楚的看出来,在同步组件的实现上主要是利用了AQS,而AQS“屏蔽”了同步状态的修改,线程排队等底层实现,通过AQS的模板方法可以很方便的给同步组件的实现者进行调用。而针对用户来说,只需要调用同步组件提供的方法来实现并发编程即可。同时在新建一个同步组件时需要把握的两个关键点是: 226 | 1. 实现同步组件时推荐定义继承AQS的静态内存类,并重写需要的protected修饰的方法; 227 | 2. 同步组件语义的实现依赖于AQS的模板方法,而AQS模板方法又依赖于被AQS的子类所重写的方法。 228 | 229 | 通俗点说,因为AQS整体设计思路采用模板方法设计模式,同步组件以及AQS的功能实际上别切分成各自的两部分: 230 | 231 | **同步组件实现者的角度:** 232 | 233 | 通过可重写的方法:**独占式**: tryAcquire()(独占式获取同步状态),tryRelease()(独占式释放同步状态);**共享式** :tryAcquireShared()(共享式获取同步状态),tryReleaseShared()(共享式释放同步状态);**告诉AQS怎样判断当前同步状态是否成功获取或者是否成功释放**。同步组件专注于对当前同步状态的逻辑判断,从而实现自己的同步语义。这句话比较抽象,举例来说,上面的Mutex例子中通过tryAcquire方法实现自己的同步语义,在该方法中如果当前同步状态为0(即该同步组件没被任何线程获取),当前线程可以获取同时将状态更改为1返回true,否则,该组件已经被线程占用返回false。很显然,该同步组件只能在同一时刻被线程占用,Mutex专注于获取释放的逻辑来实现自己想要表达的同步语义。 234 | 235 | **AQS的角度** 236 | 237 | 而对AQS来说,只需要同步组件返回的true和false即可,因为AQS会对true和false会有不同的操作,true会认为当前线程获取同步组件成功直接返回,而false的话就AQS也会将当前线程插入同步队列等一系列的方法。 238 | 239 | 总的来说,同步组件通过重写AQS的方法实现自己想要表达的同步语义,而AQS只需要同步组件表达的true和false即可,AQS会针对true和false不同的情况做不同的处理,至于底层实现,可以[看这篇文章](http://www.jianshu.com/p/cc308d82cc71)。 240 | 241 | 242 | 243 | 244 | > 参考文献 245 | 246 | 《java并发编程的艺术》 247 | -------------------------------------------------------------------------------- /09.深入理解AbstractQueuedSynchronizer(AQS)/LockDemo debug下.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/09.深入理解AbstractQueuedSynchronizer(AQS)/LockDemo debug下.png -------------------------------------------------------------------------------- /09.深入理解AbstractQueuedSynchronizer(AQS)/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/09.深入理解AbstractQueuedSynchronizer(AQS)/banner.jpg -------------------------------------------------------------------------------- /09.深入理解AbstractQueuedSynchronizer(AQS)/当前节点引用线程获取锁,当前节点设置为队列头结点.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/09.深入理解AbstractQueuedSynchronizer(AQS)/当前节点引用线程获取锁,当前节点设置为队列头结点.png -------------------------------------------------------------------------------- /09.深入理解AbstractQueuedSynchronizer(AQS)/独占式锁获取(acquire()方法)流程图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/09.深入理解AbstractQueuedSynchronizer(AQS)/独占式锁获取(acquire()方法)流程图.png -------------------------------------------------------------------------------- /09.深入理解AbstractQueuedSynchronizer(AQS)/自旋获取锁整体示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/09.深入理解AbstractQueuedSynchronizer(AQS)/自旋获取锁整体示意图.png -------------------------------------------------------------------------------- /09.深入理解AbstractQueuedSynchronizer(AQS)/超时等待式获取锁(doAcquireNanos()方法).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/09.深入理解AbstractQueuedSynchronizer(AQS)/超时等待式获取锁(doAcquireNanos()方法).png -------------------------------------------------------------------------------- /09.深入理解AbstractQueuedSynchronizer(AQS)/队列示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/09.深入理解AbstractQueuedSynchronizer(AQS)/队列示意图.png -------------------------------------------------------------------------------- /10.彻底理解ReentrantLock/彻底理解ReentrantLock.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 1. ReentrantLock的介绍 # 4 | ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,**支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞**。在java关键字synchronized隐式支持重入性(关于synchronized可以[看这篇文章](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)),synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持**公平锁和非公平锁**两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:1. 重入性的实现原理;2. 公平锁和非公平锁。 5 | 6 | # 2. 重入性的实现原理 # 7 | 8 | 要想支持重入性,就要解决两个问题:**1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。**通过[这篇文章](https://juejin.im/post/5aeb07ab6fb9a07ac36350c8),我们知道,同步组件主要是通过重写AQS的几个protected方法来表达自己的同步语义。针对第一个问题,我们来看看ReentrantLock是怎样实现的,以非公平锁为例,判断当前线程能否获得锁为例,核心方法为nonfairTryAcquire: 9 | 10 | 11 | final boolean nonfairTryAcquire(int acquires) { 12 | final Thread current = Thread.currentThread(); 13 | int c = getState(); 14 | //1. 如果该锁未被任何线程占有,该锁能被当前线程获取 15 | if (c == 0) { 16 | if (compareAndSetState(0, acquires)) { 17 | setExclusiveOwnerThread(current); 18 | return true; 19 | } 20 | } 21 | //2.若被占有,检查占有线程是否是当前线程 22 | else if (current == getExclusiveOwnerThread()) { 23 | // 3. 再次获取,计数加一 24 | int nextc = c + acquires; 25 | if (nextc < 0) // overflow 26 | throw new Error("Maximum lock count exceeded"); 27 | setState(nextc); 28 | return true; 29 | } 30 | return false; 31 | } 32 | 33 | 这段代码的逻辑也很简单,具体请看注释。为了支持重入性,在第二步增加了处理逻辑,如果该锁已经被线程所占有了,会继续检查占有线程是否为当前线程,如果是的话,同步状态加1返回true,表示可以再次获取成功。每次重新获取都会对同步状态进行加一的操作,那么释放的时候处理思路是怎样的了?(依然还是以非公平锁为例)核心方法为tryRelease: 34 | 35 | protected final boolean tryRelease(int releases) { 36 | //1. 同步状态减1 37 | int c = getState() - releases; 38 | if (Thread.currentThread() != getExclusiveOwnerThread()) 39 | throw new IllegalMonitorStateException(); 40 | boolean free = false; 41 | if (c == 0) { 42 | //2. 只有当同步状态为0时,锁成功被释放,返回true 43 | free = true; 44 | setExclusiveOwnerThread(null); 45 | } 46 | // 3. 锁未被完全释放,返回false 47 | setState(c); 48 | return free; 49 | } 50 | 51 | 代码的逻辑请看注释,需要注意的是,重入锁的释放必须得等到同步状态为0时锁才算成功释放,否则锁仍未释放。如果锁被获取n次,释放了n-1次,该锁未完全释放返回false,只有被释放n次才算成功释放,返回true。到现在我们可以理清ReentrantLock重入性的实现了,也就是理解了同步语义的第一条。 52 | 53 | # 3. 公平锁与公平锁 # 54 | ReentrantLock支持两种锁:**公平锁**和**非公平锁**。**何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO**。ReentrantLock的构造方法无参时是构造非公平锁,源码为: 55 | 56 | public ReentrantLock() { 57 | sync = new NonfairSync(); 58 | } 59 | 60 | 另外还提供了另外一种方式,可传入一个boolean值,true时为公平锁,false时为非公平锁,源码为: 61 | 62 | public ReentrantLock(boolean fair) { 63 | sync = fair ? new FairSync() : new NonfairSync(); 64 | } 65 | 66 | 在上面非公平锁获取时(nonfairTryAcquire方法)只是简单的获取了一下当前状态做了一些逻辑处理,并没有考虑到当前同步队列中线程等待的情况。我们来看看公平锁的处理逻辑是怎样的,核心方法为: 67 | 68 | protected final boolean tryAcquire(int acquires) { 69 | final Thread current = Thread.currentThread(); 70 | int c = getState(); 71 | if (c == 0) { 72 | if (!hasQueuedPredecessors() && 73 | compareAndSetState(0, acquires)) { 74 | setExclusiveOwnerThread(current); 75 | return true; 76 | } 77 | } 78 | else if (current == getExclusiveOwnerThread()) { 79 | int nextc = c + acquires; 80 | if (nextc < 0) 81 | throw new Error("Maximum lock count exceeded"); 82 | setState(nextc); 83 | return true; 84 | } 85 | return false; 86 | } 87 | } 88 | 89 | 这段代码的逻辑与nonfairTryAcquire基本上一直,唯一的不同在于增加了hasQueuedPredecessors的逻辑判断,方法名就可知道该方法用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。**公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁**。 90 | 91 | > **公平锁 VS 非公平锁** 92 | 93 | 1. 公平锁每次获取到锁为同步队列中的第一个节点,**保证请求资源时间上的绝对顺序**,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,**造成“饥饿”现象**。 94 | 95 | 2. 公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,**保证了系统更大的吞吐量**。 96 | 97 | 98 | > 参考文献 99 | 100 | 101 | 《java并发编程的艺术》 102 | -------------------------------------------------------------------------------- /11.深入理解读写锁ReentrantReadWriteLock/深入理解读写锁ReentrantReadWriteLock.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 1.读写锁的介绍 # 4 | 在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java提供的关键字synchronized(关于synchronized可以[看这篇文章](https://juejin.im/post/5ae6dc04f265da0ba351d3ff))或者concurrents包中实现了Lock接口的[ReentrantLock](https://juejin.im/post/5aeb0a8b518825673a2066f0)。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。**读写所允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞**。在分析WirteLock和ReadLock的互斥性时可以按照WriteLock与WriteLock之间,WriteLock与ReadLock之间以及ReadLock与ReadLock之间进行分析。更多关于读写锁特性介绍大家可以看源码上的介绍(阅读源码时最好的一种学习方式,我也正在学习中,与大家共勉),这里做一个归纳总结: 5 | 6 | 1. **公平性选择**:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平; 7 | 2. **重入性**:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁; 8 | 3. **锁降级**:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁 9 | 10 | 要想能够彻底的理解读写锁必须能够理解这样几个问题:1. 读写锁是怎样实现分别记录读写状态的?2. 写锁是怎样获取和释放的?3.读锁是怎样获取和释放的?我们带着这样的三个问题,再去了解下读写锁。 11 | 12 | # 2.写锁详解 # 13 | 14 | ## 2.1.写锁的获取 ## 15 | 同步组件的实现聚合了同步器(AQS),并通过重写重写同步器(AQS)中的方法实现同步组件的同步语义(关于同步组件的实现层级结构可以[看这篇文章](https://juejin.im/post/5aeb055b6fb9a07abf725c8c),AQS的底层实现分析可以[看这篇文章](https://juejin.im/post/5aeb07ab6fb9a07ac36350c8))。因此,写锁的实现依然也是采用这种方式。在同一时刻写锁是不能被多个线程所获取,很显然写锁是独占式锁,而实现写锁的同步语义是通过重写AQS中的tryAcquire方法实现的。源码为: 16 | 17 | protected final boolean tryAcquire(int acquires) { 18 | /* 19 | * Walkthrough: 20 | * 1. If read count nonzero or write count nonzero 21 | * and owner is a different thread, fail. 22 | * 2. If count would saturate, fail. (This can only 23 | * happen if count is already nonzero.) 24 | * 3. Otherwise, this thread is eligible for lock if 25 | * it is either a reentrant acquire or 26 | * queue policy allows it. If so, update state 27 | * and set owner. 28 | */ 29 | Thread current = Thread.currentThread(); 30 | // 1. 获取写锁当前的同步状态 31 | int c = getState(); 32 | // 2. 获取写锁获取的次数 33 | int w = exclusiveCount(c); 34 | if (c != 0) { 35 | // (Note: if c != 0 and w == 0 then shared count != 0) 36 | // 3.1 当读锁已被读线程获取或者当前线程不是已经获取写锁的线程的话 37 | // 当前线程获取写锁失败 38 | if (w == 0 || current != getExclusiveOwnerThread()) 39 | return false; 40 | if (w + exclusiveCount(acquires) > MAX_COUNT) 41 | throw new Error("Maximum lock count exceeded"); 42 | // Reentrant acquire 43 | // 3.2 当前线程获取写锁,支持可重复加锁 44 | setState(c + acquires); 45 | return true; 46 | } 47 | // 3.3 写锁未被任何线程获取,当前线程可获取写锁 48 | if (writerShouldBlock() || 49 | !compareAndSetState(c, c + acquires)) 50 | return false; 51 | setExclusiveOwnerThread(current); 52 | return true; 53 | } 54 | 55 | 这段代码的逻辑请看注释,这里有一个地方需要重点关注,exclusiveCount(c)方法,该方法源码为: 56 | 57 | static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; } 58 | 其中**EXCLUSIVE_MASK**为: `static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;` EXCLUSIVE _MASK为1左移16位然后减1,即为0x0000FFFF。而exclusiveCount方法是将同步状态(state为int类型)与0x0000FFFF相与,即取同步状态的低16位。那么低16位代表什么呢?根据exclusiveCount方法的注释为独占式获取的次数即写锁被获取的次数,现在就可以得出来一个结论**同步状态的低16位用来表示写锁的获取次数**。同时还有一个方法值得我们注意: 59 | 60 | static int sharedCount(int c) { return c >>> SHARED_SHIFT; } 61 | 62 | 该方法是获取读锁被获取的次数,是将同步状态(int c)右移16次,即取同步状态的高16位,现在我们可以得出另外一个结论**同步状态的高16位用来表示读锁被获取的次数**。现在还记得我们开篇说的需要弄懂的第一个问题吗?读写锁是怎样实现分别记录读锁和写锁的状态的,现在这个问题的答案就已经被我们弄清楚了,其示意图如下图所示: 63 | 64 | ![读写锁的读写状态设计.png](http://upload-images.jianshu.io/upload_images/2615789-6af1818bbfa83051.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 65 | 66 | 67 | 现在我们回过头来看写锁获取方法tryAcquire,其主要逻辑为:**当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态。** 68 | 69 | ## 2.2.写锁的释放 ## 70 | 写锁释放通过重写AQS的tryRelease方法,源码为: 71 | 72 | protected final boolean tryRelease(int releases) { 73 | if (!isHeldExclusively()) 74 | throw new IllegalMonitorStateException(); 75 | //1. 同步状态减去写状态 76 | int nextc = getState() - releases; 77 | //2. 当前写状态是否为0,为0则释放写锁 78 | boolean free = exclusiveCount(nextc) == 0; 79 | if (free) 80 | setExclusiveOwnerThread(null); 81 | //3. 不为0则更新同步状态 82 | setState(nextc); 83 | return free; 84 | } 85 | 86 | 源码的实现逻辑请看注释,不难理解与ReentrantLock基本一致,这里需要注意的是,减少写状态` int nextc = getState() - releases;`只需要用**当前同步状态直接减去写状态的原因正是我们刚才所说的写状态是由同步状态的低16位表示的**。 87 | 88 | # 3.读锁详解 # 89 | ## 3.1.读锁的获取 ## 90 | 看完了写锁,现在来看看读锁,读锁不是独占式锁,即同一时刻该锁可以被多个读线程获取也就是一种共享式锁。按照之前对AQS介绍,实现共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法。读锁的获取实现方法为: 91 | 92 | protected final int tryAcquireShared(int unused) { 93 | /* 94 | * Walkthrough: 95 | * 1. If write lock held by another thread, fail. 96 | * 2. Otherwise, this thread is eligible for 97 | * lock wrt state, so ask if it should block 98 | * because of queue policy. If not, try 99 | * to grant by CASing state and updating count. 100 | * Note that step does not check for reentrant 101 | * acquires, which is postponed to full version 102 | * to avoid having to check hold count in 103 | * the more typical non-reentrant case. 104 | * 3. If step 2 fails either because thread 105 | * apparently not eligible or CAS fails or count 106 | * saturated, chain to version with full retry loop. 107 | */ 108 | Thread current = Thread.currentThread(); 109 | int c = getState(); 110 | //1. 如果写锁已经被获取并且获取写锁的线程不是当前线程的话,当前 111 | // 线程获取读锁失败返回-1 112 | if (exclusiveCount(c) != 0 && 113 | getExclusiveOwnerThread() != current) 114 | return -1; 115 | int r = sharedCount(c); 116 | if (!readerShouldBlock() && 117 | r < MAX_COUNT && 118 | //2. 当前线程获取读锁 119 | compareAndSetState(c, c + SHARED_UNIT)) { 120 | //3. 下面的代码主要是新增的一些功能,比如getReadHoldCount()方法 121 | //返回当前获取读锁的次数 122 | if (r == 0) { 123 | firstReader = current; 124 | firstReaderHoldCount = 1; 125 | } else if (firstReader == current) { 126 | firstReaderHoldCount++; 127 | } else { 128 | HoldCounter rh = cachedHoldCounter; 129 | if (rh == null || rh.tid != getThreadId(current)) 130 | cachedHoldCounter = rh = readHolds.get(); 131 | else if (rh.count == 0) 132 | readHolds.set(rh); 133 | rh.count++; 134 | } 135 | return 1; 136 | } 137 | //4. 处理在第二步中CAS操作失败的自旋已经实现重入性 138 | return fullTryAcquireShared(current); 139 | } 140 | 141 | 代码的逻辑请看注释,需要注意的是 **当写锁被其他线程获取后,读锁获取失败**,否则获取成功利用CAS更新同步状态。另外,当前同步状态需要加上SHARED_UNIT(`(1 << SHARED_SHIFT)`即0x00010000)的原因这是我们在上面所说的同步状态的高16位用来表示读锁被获取的次数。如果CAS失败或者已经获取读锁的线程再次获取读锁时,是靠fullTryAcquireShared方法实现的,这段代码就不展开说了,有兴趣可以看看。 142 | 143 | ## 3.2.读锁的释放 ## 144 | 读锁释放的实现主要通过方法tryReleaseShared,源码如下,主要逻辑请看注释: 145 | 146 | protected final boolean tryReleaseShared(int unused) { 147 | Thread current = Thread.currentThread(); 148 | // 前面还是为了实现getReadHoldCount等新功能 149 | if (firstReader == current) { 150 | // assert firstReaderHoldCount > 0; 151 | if (firstReaderHoldCount == 1) 152 | firstReader = null; 153 | else 154 | firstReaderHoldCount--; 155 | } else { 156 | HoldCounter rh = cachedHoldCounter; 157 | if (rh == null || rh.tid != getThreadId(current)) 158 | rh = readHolds.get(); 159 | int count = rh.count; 160 | if (count <= 1) { 161 | readHolds.remove(); 162 | if (count <= 0) 163 | throw unmatchedUnlockException(); 164 | } 165 | --rh.count; 166 | } 167 | for (;;) { 168 | int c = getState(); 169 | // 读锁释放 将同步状态减去读状态即可 170 | int nextc = c - SHARED_UNIT; 171 | if (compareAndSetState(c, nextc)) 172 | // Releasing the read lock has no effect on readers, 173 | // but it may allow waiting writers to proceed if 174 | // both read and write locks are now free. 175 | return nextc == 0; 176 | } 177 | } 178 | 179 | # 4.锁降级 # 180 | 读写锁支持锁降级,**遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁**,不支持锁升级,关于锁降级下面的示例代码摘自ReentrantWriteReadLock源码中: 181 | 182 | void processCachedData() { 183 | rwl.readLock().lock(); 184 | if (!cacheValid) { 185 | // Must release read lock before acquiring write lock 186 | rwl.readLock().unlock(); 187 | rwl.writeLock().lock(); 188 | try { 189 | // Recheck state because another thread might have 190 | // acquired write lock and changed state before we did. 191 | if (!cacheValid) { 192 | data = ... 193 | cacheValid = true; 194 | } 195 | // Downgrade by acquiring read lock before releasing write lock 196 | rwl.readLock().lock(); 197 | } finally { 198 | rwl.writeLock().unlock(); // Unlock write, still hold read 199 | } 200 | } 201 | 202 | try { 203 | use(data); 204 | } finally { 205 | rwl.readLock().unlock(); 206 | } 207 | } 208 | } -------------------------------------------------------------------------------- /11.深入理解读写锁ReentrantReadWriteLock/读写锁的读写状态设计.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/11.深入理解读写锁ReentrantReadWriteLock/读写锁的读写状态设计.png -------------------------------------------------------------------------------- /12.详解Condition的await和signal等待通知机制/AQS持有多个Condition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/12.详解Condition的await和signal等待通知机制/AQS持有多个Condition.png -------------------------------------------------------------------------------- /12.详解Condition的await和signal等待通知机制/await方法示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/12.详解Condition的await和signal等待通知机制/await方法示意图.png -------------------------------------------------------------------------------- /12.详解Condition的await和signal等待通知机制/condition下的等待通知机制.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/12.详解Condition的await和signal等待通知机制/condition下的等待通知机制.png -------------------------------------------------------------------------------- /12.详解Condition的await和signal等待通知机制/debug模式下情景图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/12.详解Condition的await和signal等待通知机制/debug模式下情景图.png -------------------------------------------------------------------------------- /12.详解Condition的await和signal等待通知机制/signal执行示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/12.详解Condition的await和signal等待通知机制/signal执行示意图.png -------------------------------------------------------------------------------- /12.详解Condition的await和signal等待通知机制/等待队列的示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/12.详解Condition的await和signal等待通知机制/等待队列的示意图.png -------------------------------------------------------------------------------- /12.详解Condition的await和signal等待通知机制/详解Condition的await和signal等待通知机制.md: -------------------------------------------------------------------------------- 1 | 2 | # 1.Condition简介 # 3 | 任何一个java对象都天然继承于Object类,在线程间实现通信的往往会应用到Object的几个方法,比如wait(),wait(long timeout),wait(long timeout, int nanos)与notify(),notifyAll()几个方法实现等待/通知机制,同样的, 在java Lock体系下依然会有同样的方法实现等待/通知机制。从整体上来看**Object的wait和notify/notify是与对象监视器配合完成线程间的等待/通知机制,而Condition与Lock配合完成等待通知机制,前者是java底层级别的,后者是语言级别的,具有更高的可控制性和扩展性**。两者除了在使用方式上不同外,在**功能特性**上还是有很多的不同: 4 | 5 | 1. Condition能够支持不响应中断,而通过使用Object方式不支持; 6 | 2. Condition能够支持多个等待队列(new 多个Condition对象),而Object方式只能支持一个; 7 | 3. Condition能够支持超时时间的设置,而Object不支持 8 | 9 | 10 | 参照Object的wait和notify/notifyAll方法,Condition也提供了同样的方法: 11 | 12 | > **针对Object的wait方法** 13 | 14 | 1. void await() throws InterruptedException:当前线程进入等待状态,如果其他线程调用condition的signal或者signalAll方法并且当前线程获取Lock从await方法返回,如果在等待状态中被中断会抛出被中断异常; 15 | 2. long awaitNanos(long nanosTimeout):当前线程进入等待状态直到被通知,中断或者**超时**; 16 | 3. boolean await(long time, TimeUnit unit)throws InterruptedException:同第二种,支持自定义时间单位 17 | 4. boolean awaitUntil(Date deadline) throws InterruptedException:当前线程进入等待状态直到被通知,中断或者**到了某个时间** 18 | 19 | 20 | > **针对Object的notify/notifyAll方法** 21 | 22 | 1. void signal():唤醒一个等待在condition上的线程,将该线程从**等待队列**中转移到**同步队列**中,如果在同步队列中能够竞争到Lock则可以从等待方法中返回。 23 | 2. void signalAll():与1的区别在于能够唤醒所有等待在condition上的线程 24 | # 2.Condition实现原理分析 # 25 | ## 2.1 等待队列 ## 26 | 要想能够深入的掌握condition还是应该知道它的实现原理,现在我们一起来看看condiiton的源码。创建一个condition对象是通过`lock.newCondition()`,而这个方法实际上是会new出一个**ConditionObject**对象,该类是AQS([AQS的实现原理的文章](https://juejin.im/post/5aeb07ab6fb9a07ac36350c8))的一个内部类,有兴趣可以去看看。前面我们说过,condition是要和lock配合使用的也就是condition和Lock是绑定在一起的,而lock的实现原理又依赖于AQS,自然而然ConditionObject作为AQS的一个内部类无可厚非。我们知道在锁机制的实现上,AQS内部维护了一个同步队列,如果是独占式锁的话,所有获取锁失败的线程的尾插入到**同步队列**,同样的,condition内部也是使用同样的方式,内部维护了一个 **等待队列**,所有调用condition.await方法的线程会加入到等待队列中,并且线程状态转换为等待状态。另外注意到ConditionObject中有两个成员变量: 27 | 28 | /** First node of condition queue. */ 29 | private transient Node firstWaiter; 30 | /** Last node of condition queue. */ 31 | private transient Node lastWaiter; 32 | 33 | 这样我们就可以看出来ConditionObject通过持有等待队列的头尾指针来管理等待队列。主要注意的是Node类复用了在AQS中的Node类,其节点状态和相关属性可以去看[AQS的实现原理的文章](https://juejin.im/post/5aeb07ab6fb9a07ac36350c8),如果您仔细看完这篇文章对condition的理解易如反掌,对lock体系的实现也会有一个质的提升。Node类有这样一个属性: 34 | 35 | //后继节点 36 | Node nextWaiter; 37 | 进一步说明,**等待队列是一个单向队列**,而在之前说AQS时知道同步队列是一个双向队列。接下来我们用一个demo,通过debug进去看是不是符合我们的猜想: 38 | 39 | public static void main(String[] args) { 40 | for (int i = 0; i < 10; i++) { 41 | Thread thread = new Thread(() -> { 42 | lock.lock(); 43 | try { 44 | condition.await(); 45 | } catch (InterruptedException e) { 46 | e.printStackTrace(); 47 | }finally { 48 | lock.unlock(); 49 | } 50 | }); 51 | thread.start(); 52 | } 53 | } 54 | 这段代码没有任何实际意义,甚至很臭,只是想说明下我们刚才所想的。新建了10个线程,没有线程先获取锁,然后调用condition.await方法释放锁将当前线程加入到等待队列中,通过debug控制当走到第10个线程的时候查看`firstWaiter`即等待队列中的头结点,debug模式下情景图如下: 55 | 56 | ![debug模式下情景图](http://upload-images.jianshu.io/upload_images/2615789-67a211209835e36d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 57 | 58 | 59 | 60 | 从这个图我们可以很清楚的看到这样几点:1. 调用condition.await方法后线程依次尾插入到等待队列中,如图队列中的线程引用依次为Thread-0,Thread-1,Thread-2....Thread-8;2. 等待队列是一个单向队列。通过我们的猜想然后进行实验验证,我们可以得出等待队列的示意图如下图所示: 61 | 62 | ![等待队列的示意图](http://upload-images.jianshu.io/upload_images/2615789-5aa1ee1ae8cb7f5a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 63 | 64 | 65 | 同时还有一点需要注意的是:我们可以多次调用lock.newCondition()方法创建多个condition对象,也就是一个lock可以持有多个等待队列。而在之前利用Object的方式实际上是指在**对象Object对象监视器上只能拥有一个同步队列和一个等待队列,而并发包中的Lock拥有一个同步队列和多个等待队列**。示意图如下: 66 | 67 | 68 | 69 | ![AQS持有多个Condition.png](http://upload-images.jianshu.io/upload_images/2615789-6621181fc19603c2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 70 | 71 | 72 | 73 | 如图所示,ConditionObject是AQS的内部类,因此每个ConditionObject能够访问到AQS提供的方法,相当于每个Condition都拥有所属同步器的引用。 74 | 75 | ## 2.2 await实现原理 ## 76 | **当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列,如果该线程能够从await()方法返回的话一定是该线程获取了与condition相关联的lock**。接下来,我们还是从源码的角度去看,只有熟悉了源码的逻辑我们的理解才是最深的。await()方法源码为: 77 | 78 | public final void await() throws InterruptedException { 79 | if (Thread.interrupted()) 80 | throw new InterruptedException(); 81 | // 1. 将当前线程包装成Node,尾插入到等待队列中 82 | Node node = addConditionWaiter(); 83 | // 2. 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点 84 | int savedState = fullyRelease(node); 85 | int interruptMode = 0; 86 | while (!isOnSyncQueue(node)) { 87 | // 3. 当前线程进入到等待状态 88 | LockSupport.park(this); 89 | if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) 90 | break; 91 | } 92 | // 4. 自旋等待获取到同步状态(即获取到lock) 93 | if (acquireQueued(node, savedState) && interruptMode != THROW_IE) 94 | interruptMode = REINTERRUPT; 95 | if (node.nextWaiter != null) // clean up if cancelled 96 | unlinkCancelledWaiters(); 97 | // 5. 处理被中断的情况 98 | if (interruptMode != 0) 99 | reportInterruptAfterWait(interruptMode); 100 | } 101 | 102 | 103 | 代码的主要逻辑**请看注释**,我们都知道**当当前线程调用condition.await()方法后,会使得当前线程释放lock然后加入到等待队列中,直至被signal/signalAll后会使得当前线程从等待队列中移至到同步队列中去,直到获得了lock后才会从await方法返回,或者在等待时被中断会做中断处理**。那么关于这个实现过程我们会有这样几个问题:1. 是怎样将当前线程添加到等待队列中去的?2.释放锁的过程?3.怎样才能从await方法退出?而这段代码的逻辑就是告诉我们这三个问题的答案。具体**请看注释**,在第1步中调用addConditionWaiter将当前线程添加到等待队列中,该方法源码为: 104 | 105 | private Node addConditionWaiter() { 106 | Node t = lastWaiter; 107 | // If lastWaiter is cancelled, clean out. 108 | if (t != null && t.waitStatus != Node.CONDITION) { 109 | unlinkCancelledWaiters(); 110 | t = lastWaiter; 111 | } 112 | //将当前线程包装成Node 113 | Node node = new Node(Thread.currentThread(), Node.CONDITION); 114 | if (t == null) 115 | firstWaiter = node; 116 | else 117 | //尾插入 118 | t.nextWaiter = node; 119 | //更新lastWaiter 120 | lastWaiter = node; 121 | return node; 122 | } 123 | 124 | 这段代码就很容易理解了,将当前节点包装成Node,如果等待队列的firstWaiter为null的话(等待队列为空队列),则将firstWaiter指向当前的Node,否则,更新lastWaiter(尾节点)即可。就是**通过尾插入的方式将当前线程封装的Node插入到等待队列中即可**,同时可以看出等待队列是一个**不带头结点的链式队列**,之前我们学习AQS时知道同步队列**是一个带头结点的链式队列**,这是两者的一个区别。将当前节点插入到等待对列之后,会使当前线程释放lock,由fullyRelease方法实现,fullyRelease源码为: 125 | 126 | final int fullyRelease(Node node) { 127 | boolean failed = true; 128 | try { 129 | int savedState = getState(); 130 | if (release(savedState)) { 131 | //成功释放同步状态 132 | failed = false; 133 | return savedState; 134 | } else { 135 | //不成功释放同步状态抛出异常 136 | throw new IllegalMonitorStateException(); 137 | } 138 | } finally { 139 | if (failed) 140 | node.waitStatus = Node.CANCELLED; 141 | } 142 | } 143 | 144 | 这段代码就很容易理解了,**调用AQS的模板方法release方法释放AQS的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程**,如果释放成功则正常返回,若失败的话就抛出异常。到目前为止,这两段代码已经解决了前面的两个问题的答案了,还剩下第三个问题,怎样从await方法退出?现在回过头再来看await方法有这样一段逻辑: 145 | 146 | while (!isOnSyncQueue(node)) { 147 | // 3. 当前线程进入到等待状态 148 | LockSupport.park(this); 149 | if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) 150 | break; 151 | } 152 | 153 | 很显然,当线程第一次调用condition.await()方法时,会进入到这个while()循环中,然后通过LockSupport.park(this)方法使得当前线程进入等待状态,那么要想退出这个await方法第一个前提条件自然而然的是要先退出这个while循环,出口就只剩下两个地方:**1. 逻辑走到break退出while循环;2. while循环中的逻辑判断为false**。再看代码出现第1种情况的条件是当前等待的线程被中断后代码会走到break退出,第二种情况是当前节点被移动到了同步队列中(即另外线程调用的condition的signal或者signalAll方法),while中逻辑判断为false后结束while循环。总结下,就是**当前线程被中断或者调用condition.signal/condition.signalAll方法当前节点移动到了同步队列后** ,这是当前线程退出await方法的前提条件。当退出while循环后就会调用`acquireQueued(node, savedState)`,这个方法在介绍AQS的底层实现时说过了,若感兴趣的话可以去[看这篇文章](https://juejin.im/post/5aeb07ab6fb9a07ac36350c8),该方法的作用是在**自旋过程中线程不断尝试获取同步状态,直至成功(线程获取到lock)**。这样也说明了**退出await方法必须是已经获得了condition引用(关联)的lock**。到目前为止,开头的三个问题我们通过阅读源码的方式已经完全找到了答案,也对await方法的理解加深。await方法示意图如下图: 154 | 155 | ![await方法示意图](http://upload-images.jianshu.io/upload_images/2615789-1cb1c2fe3c1ddf38.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 156 | 157 | 158 | 159 | 160 | 如图,调用condition.await方法的线程必须是已经获得了lock,也就是当前线程是同步队列中的头结点。调用该方法后会使得当前线程所封装的Node尾插入到等待队列中。 161 | 162 | > 超时机制的支持 163 | 164 | condition还额外支持了超时机制,使用者可调用方法awaitNanos,awaitUtil。这两个方法的实现原理,基本上与AQS中的tryAcquire方法如出一辙,关于tryAcquire可以仔细阅读[这篇文章的第3.4部分](https://juejin.im/post/5aeb07ab6fb9a07ac36350c8#heading-6)。 165 | 166 | > 不响应中断的支持 167 | 168 | 要想不响应中断可以调用condition.awaitUninterruptibly()方法,该方法的源码为: 169 | 170 | public final void awaitUninterruptibly() { 171 | Node node = addConditionWaiter(); 172 | int savedState = fullyRelease(node); 173 | boolean interrupted = false; 174 | while (!isOnSyncQueue(node)) { 175 | LockSupport.park(this); 176 | if (Thread.interrupted()) 177 | interrupted = true; 178 | } 179 | if (acquireQueued(node, savedState) || interrupted) 180 | selfInterrupt(); 181 | } 182 | 183 | 这段方法与上面的await方法基本一致,只不过减少了对中断的处理,并省略了reportInterruptAfterWait方法抛被中断的异常。 184 | 185 | ## 2.3 signal/signalAll实现原理 ## 186 | **调用condition的signal或者signalAll方法可以将等待队列中等待时间最长的节点移动到同步队列中**,使得该节点能够有机会获得lock。按照等待队列是先进先出(FIFO)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal方法是将头节点移动到同步队列中。我们来通过看源码的方式来看这样的猜想是不是对的,signal方法源码为: 187 | 188 | public final void signal() { 189 | //1. 先检测当前线程是否已经获取lock 190 | if (!isHeldExclusively()) 191 | throw new IllegalMonitorStateException(); 192 | //2. 获取等待队列中第一个节点,之后的操作都是针对这个节点 193 | Node first = firstWaiter; 194 | if (first != null) 195 | doSignal(first); 196 | } 197 | 198 | signal方法首先会检测当前线程是否已经获取lock,如果没有获取lock会直接抛出异常,如果获取的话再得到等待队列的头指针引用的节点,之后的操作的doSignal方法也是基于该节点。下面我们来看看doSignal方法做了些什么事情,doSignal方法源码为: 199 | 200 | private void doSignal(Node first) { 201 | do { 202 | if ( (firstWaiter = first.nextWaiter) == null) 203 | lastWaiter = null; 204 | //1. 将头结点从等待队列中移除 205 | first.nextWaiter = null; 206 | //2. while中transferForSignal方法对头结点做真正的处理 207 | } while (!transferForSignal(first) && 208 | (first = firstWaiter) != null); 209 | } 210 | 211 | 具体逻辑请看注释,真正对头节点做处理的逻辑在**transferForSignal**放,该方法源码为: 212 | 213 | final boolean transferForSignal(Node node) { 214 | /* 215 | * If cannot change waitStatus, the node has been cancelled. 216 | */ 217 | //1. 更新状态为0 218 | if (!compareAndSetWaitStatus(node, Node.CONDITION, 0)) 219 | return false; 220 | 221 | /* 222 | * Splice onto queue and try to set waitStatus of predecessor to 223 | * indicate that thread is (probably) waiting. If cancelled or 224 | * attempt to set waitStatus fails, wake up to resync (in which 225 | * case the waitStatus can be transiently and harmlessly wrong). 226 | */ 227 | //2.将该节点移入到同步队列中去 228 | Node p = enq(node); 229 | int ws = p.waitStatus; 230 | if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)) 231 | LockSupport.unpark(node.thread); 232 | return true; 233 | } 234 | 235 | 关键逻辑请看注释,这段代码主要做了两件事情1.将头结点的状态更改为CONDITION;2.调用enq方法,将该节点尾插入到同步队列中,关于enq方法请看AQS的底层实现这篇文章。现在我们可以得出结论:**调用condition的signal的前提条件是当前线程已经获取了lock,该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列,而移入到同步队列后才有机会使得等待线程被唤醒,即从await方法中的LockSupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出**。signal执行示意图如下图: 236 | 237 | ![signal执行示意图](http://upload-images.jianshu.io/upload_images/2615789-3750f5baf7995623.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 238 | 239 | 240 | 241 | > signalAll 242 | 243 | sigllAll与sigal方法的区别体现在doSignalAll方法上,前面我们已经知道d**oSignal方法只会对等待队列的头节点进行操作,**,而doSignalAll的源码为: 244 | 245 | private void doSignalAll(Node first) { 246 | lastWaiter = firstWaiter = null; 247 | do { 248 | Node next = first.nextWaiter; 249 | first.nextWaiter = null; 250 | transferForSignal(first); 251 | first = next; 252 | } while (first != null); 253 | } 254 | 255 | 该方法只不过时间等待队列中的每一个节点都移入到同步队列中,即“通知”当前调用condition.await()方法的每一个线程。 256 | 257 | # 3. await与signal/signalAll的结合思考 # 258 | 文章开篇提到等待/通知机制,通过使用condition提供的await和signal/signalAll方法就可以实现这种机制,而这种机制能够解决最经典的问题就是“生产者与消费者问题”,关于“生产者消费者问题”之后会用单独的一篇文章进行讲解,这也是面试的高频考点。await和signal和signalAll方法就像一个开关控制着线程A(等待方)和线程B(通知方)。它们之间的关系可以用下面一个图来表现得更加贴切: 259 | 260 | ![condition下的等待通知机制.png](http://upload-images.jianshu.io/upload_images/2615789-02449dc316fe1de6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 261 | 262 | 263 | 如图,**线程awaitThread先通过lock.lock()方法获取锁成功后调用了condition.await方法进入等待队列,而另一个线程signalThread通过lock.lock()方法获取锁成功后调用了condition.signal或者signalAll方法,使得线程awaitThread能够有机会移入到同步队列中,当其他线程释放lock后使得线程awaitThread能够有机会获取lock,从而使得线程awaitThread能够从await方法中退出执行后续操作。如果awaitThread获取lock失败会直接进入到同步队列**。 264 | 265 | # 3. 一个例子 # 266 | 我们用一个很简单的例子说说condition的用法: 267 | 268 | 269 | public class AwaitSignal { 270 | private static ReentrantLock lock = new ReentrantLock(); 271 | private static Condition condition = lock.newCondition(); 272 | private static volatile boolean flag = false; 273 | 274 | public static void main(String[] args) { 275 | Thread waiter = new Thread(new waiter()); 276 | waiter.start(); 277 | Thread signaler = new Thread(new signaler()); 278 | signaler.start(); 279 | } 280 | 281 | static class waiter implements Runnable { 282 | 283 | @Override 284 | public void run() { 285 | lock.lock(); 286 | try { 287 | while (!flag) { 288 | System.out.println(Thread.currentThread().getName() + "当前条件不满足等待"); 289 | try { 290 | condition.await(); 291 | } catch (InterruptedException e) { 292 | e.printStackTrace(); 293 | } 294 | } 295 | System.out.println(Thread.currentThread().getName() + "接收到通知条件满足"); 296 | } finally { 297 | lock.unlock(); 298 | } 299 | } 300 | } 301 | 302 | static class signaler implements Runnable { 303 | 304 | @Override 305 | public void run() { 306 | lock.lock(); 307 | try { 308 | flag = true; 309 | condition.signalAll(); 310 | } finally { 311 | lock.unlock(); 312 | } 313 | } 314 | } 315 | } 316 | 317 | 输出结果为: 318 | 319 | Thread-0当前条件不满足等待 320 | Thread-0接收到通知,条件满足 321 | 322 | 开启了两个线程waiter和signaler,waiter线程开始执行的时候由于条件不满足,执行condition.await方法使该线程进入等待状态同时释放锁,signaler线程获取到锁之后更改条件,并通知所有的等待线程后释放锁。这时,waiter线程获取到锁,并由于signaler线程更改了条件此时相对于waiter来说条件满足,继续执行。 323 | 324 | 325 | 326 | > 参考文献 327 | 328 | 《java并发编程的艺术》 -------------------------------------------------------------------------------- /13.LockSupport工具/LockSupport工具.md: -------------------------------------------------------------------------------- 1 | 2 | # 1. LockSupport简介 # 3 | 在之前介绍[AQS的底层实现](https://juejin.im/post/5aeb07ab6fb9a07ac36350c8),已经在介绍java中的Lock时,比如[ReentrantLock](https://juejin.im/post/5aeb0a8b518825673a2066f0),[ReentReadWriteLocks](https://juejin.im/post/5aeb0e016fb9a07ab7740d90),已经在介绍线程间等待/通知机制使用的[Condition](https://juejin.im/post/5aeea5e951882506a36c67f0)时都会调用LockSupport.park()方法和LockSupport.unpark()方法。而这个在同步组件的实现中被频繁使用的LockSupport到底是何方神圣,现在就来看看。LockSupport位于java.util.concurrent.locks包下,有兴趣的可以直接去看源码,该类的方法并不是很多。LockSupprot是线程的阻塞原语,用来阻塞线程和唤醒线程。每个使用LockSupport的线程都会与一个许可关联,如果该许可可用,并且可在线程中使用,则调用park()将会立即返回,否则可能阻塞。如果许可尚不可用,则可以调用 unpark 使其可用。但是注意许可**不可重入**,也就是说只能调用一次park()方法,否则会一直阻塞。 4 | 5 | # 2. LockSupport方法介绍 # 6 | LockSupport中的方法不多,这里将这些方法做一个总结: 7 | 8 | > **阻塞线程** 9 | 10 | 1. void park():阻塞当前线程,如果调用unpark方法或者当前线程被中断,从能从park()方法中返回 11 | 2. void park(Object blocker):功能同方法1,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查; 12 | 3. void parkNanos(long nanos):阻塞当前线程,最长不超过nanos纳秒,增加了超时返回的特性; 13 | 4. void parkNanos(Object blocker, long nanos):功能同方法3,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查; 14 | 5. void parkUntil(long deadline):阻塞当前线程,知道deadline; 15 | 6. void parkUntil(Object blocker, long deadline):功能同方法5,入参增加一个Object对象,用来记录导致线程阻塞的阻塞对象,方便进行问题排查; 16 | 17 | > **唤醒线程** 18 | 19 | void unpark(Thread thread):唤醒处于阻塞状态的指定线程 20 | 21 | 实际上LockSupport阻塞和唤醒线程的功能是依赖于sun.misc.Unsafe,这是一个很底层的类,有兴趣的可以去查阅资料,比如park()方法的功能实现则是靠unsafe.park()方法。另外在阻塞线程这一系列方法中还有一个很有意思的现象就是,每个方法都会新增一个带有Object的阻塞对象的重载方法。那么增加了一个Object对象的入参会有什么不同的地方了?示例代码很简单就不说了,直接看dump线程的信息。 22 | 23 | **调用park()方法dump线程**: 24 | 25 | "main" #1 prio=5 os_prio=0 tid=0x02cdcc00 nid=0x2b48 waiting on condition [0x00d6f000] 26 | java.lang.Thread.State: WAITING (parking) 27 | at sun.misc.Unsafe.park(Native Method) 28 | at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304) 29 | at learn.LockSupportDemo.main(LockSupportDemo.java:7) 30 | 31 | **调用park(Object blocker)方法dump线程** 32 | 33 | "main" #1 prio=5 os_prio=0 tid=0x0069cc00 nid=0x6c0 waiting on condition [0x00dcf000] 34 | java.lang.Thread.State: WAITING (parking) 35 | at sun.misc.Unsafe.park(Native Method) 36 | - parking to wait for <0x048c2d18> (a java.lang.String) 37 | at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) 38 | at learn.LockSupportDemo.main(LockSupportDemo.java:7) 39 | 40 | 41 | 通过分别调用这两个方法然后dump线程信息可以看出,带Object的park方法相较于无参的park方法会增加 parking to wait for <0x048c2d18> (a java.lang.String)的信息,这种信息就类似于记录“案发现场”,有助于工程人员能够迅速发现问题解决问题。有个有意思的事情是,我们都知道如果使用synchronzed阻塞了线程dump线程时都会有阻塞对象的描述,在java 5推出LockSupport时遗漏了这一点,在java 6时进行了补充。还有一点需要需要的是:**synchronzed致使线程阻塞,线程会进入到BLOCKED状态,而调用LockSupprt方法阻塞线程会致使线程进入到WAITING状态。** 42 | 43 | # 3. 一个例子 # 44 | 45 | 用一个很简单的例子说说这些方法怎么用。 46 | 47 | public class LockSupportDemo { 48 | public static void main(String[] args) { 49 | Thread thread = new Thread(() -> { 50 | LockSupport.park(); 51 | System.out.println(Thread.currentThread().getName() + "被唤醒"); 52 | }); 53 | thread.start(); 54 | try { 55 | Thread.sleep(3000); 56 | } catch (InterruptedException e) { 57 | e.printStackTrace(); 58 | } 59 | LockSupport.unpark(thread); 60 | } 61 | } 62 | 63 | thread线程调用LockSupport.park()致使thread阻塞,当mian线程睡眠3秒结束后通过LockSupport.unpark(thread)方法唤醒thread线程,thread线程被唤醒执行后续操作。另外,还有一点值得关注的是,**LockSupport.unpark(thread)可以指定线程对象唤醒指定的线程**。 -------------------------------------------------------------------------------- /14.并发容器之ConcurrentHashMap(JDK 1.8版本)/ConcurrentHashMap扩容示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/14.并发容器之ConcurrentHashMap(JDK 1.8版本)/ConcurrentHashMap扩容示意图.png -------------------------------------------------------------------------------- /14.并发容器之ConcurrentHashMap(JDK 1.8版本)/ConcurrentHashMap散列桶数组结构示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/14.并发容器之ConcurrentHashMap(JDK 1.8版本)/ConcurrentHashMap散列桶数组结构示意图.png -------------------------------------------------------------------------------- /15.并发容器之ConcurrentLinkedQueue/ConcurrentLinkedQueue初始化状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/15.并发容器之ConcurrentLinkedQueue/ConcurrentLinkedQueue初始化状态.png -------------------------------------------------------------------------------- /15.并发容器之ConcurrentLinkedQueue/Node2从队列中出队后的状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/15.并发容器之ConcurrentLinkedQueue/Node2从队列中出队后的状态.png -------------------------------------------------------------------------------- /15.并发容器之ConcurrentLinkedQueue/offer 1后队列的状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/15.并发容器之ConcurrentLinkedQueue/offer 1后队列的状态.png -------------------------------------------------------------------------------- /15.并发容器之ConcurrentLinkedQueue/offer和poll相互影响分析时队列初始状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/15.并发容器之ConcurrentLinkedQueue/offer和poll相互影响分析时队列初始状态.png -------------------------------------------------------------------------------- /15.并发容器之ConcurrentLinkedQueue/并发容器之ConcurrentLinkedQueue.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 1.ConcurrentLinkedQueue简介 # 4 | 在单线程编程中我们会经常用到一些集合类,比如ArrayList,HashMap等,但是这些类都不是线程安全的类。在面试中也经常会有一些考点,比如ArrayList不是线程安全的,Vector是线程安全。而保障Vector线程安全的方式,是非常粗暴的在方法上用synchronized独占锁,将多线程执行变成串行化。要想将ArrayList变成线程安全的也可以使用`Collections.synchronizedList(List list)`方法ArrayList转换成线程安全的,但这种转换方式依然是通过synchronized修饰方法实现的,很显然这不是一种高效的方式,同时,队列也是我们常用的一种数据结构,为了解决线程安全的问题,Doug Lea大师为我们准备了ConcurrentLinkedQueue这个线程安全的队列。从类名就可以看的出来实现队列的数据结构是链式。 5 | 6 | ## 1.1 Node ## 7 | 要想先学习ConcurrentLinkedQueue自然而然得先从它的节点类看起,明白它的底层数据结构。Node类的源码为: 8 | 9 | private static class Node { 10 | volatile E item; 11 | volatile Node next; 12 | ....... 13 | } 14 | 15 | Node节点主要包含了两个域:一个是数据域item,另一个是next指针,用于指向下一个节点从而构成链式队列。并且都是用volatile进行修饰的,以保证内存可见性(关于volatile[可以看这篇文章](https://juejin.im/post/5ae9b41b518825670b33e6c4))。另外ConcurrentLinkedQueue含有这样两个成员变量: 16 | 17 | private transient volatile Node head; 18 | private transient volatile Node tail; 19 | 20 | 说明ConcurrentLinkedQueue通过持有头尾指针进行管理队列。当我们调用无参构造器时,其源码为: 21 | 22 | public ConcurrentLinkedQueue() { 23 | head = tail = new Node(null); 24 | } 25 | head和tail指针会指向一个item域为null的节点,此时ConcurrentLinkedQueue状态如下图所示: 26 | 27 | 如图,head和tail指向同一个节点Node0,该节点item域为null,next域为null。 28 | 29 | 30 | 31 | 32 | ![1.ConcurrentLinkedQueue初始化状态.png](http://upload-images.jianshu.io/upload_images/2615789-a3dbf8f54bb3452e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 33 | 34 | 35 | 36 | ## 1.2 操作Node的几个CAS操作 ## 37 | 在队列进行出队入队的时候免不了对节点需要进行操作,在多线程就很容易出现线程安全的问题。可以看出在处理器指令集能够支持**CMPXCHG**指令后,在java源码中涉及到并发处理都会使用CAS操作[(关于CAS操作可以看这篇文章的第3.1节](https://juejin.im/post/5ae6dc04f265da0ba351d3ff)),那么在ConcurrentLinkedQueue对Node的CAS操作有这样几个: 38 | 39 | //更改Node中的数据域item 40 | boolean casItem(E cmp, E val) { 41 | return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val); 42 | } 43 | //更改Node中的指针域next 44 | void lazySetNext(Node val) { 45 | UNSAFE.putOrderedObject(this, nextOffset, val); 46 | } 47 | //更改Node中的指针域next 48 | boolean casNext(Node cmp, Node val) { 49 | return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); 50 | } 51 | 52 | 可以看出这些方法实际上是通过调用UNSAFE实例的方法,UNSAFE为**sun.misc.Unsafe**类,该类是hotspot底层方法,目前为止了解即可,知道CAS的操作归根结底是由该类提供就好。 53 | 54 | # 2.offer方法 # 55 | 对一个队列来说,插入满足FIFO特性,插入元素总是在队列最末尾的地方进行插入,而取(移除)元素总是从队列的队头。所有要想能够彻底弄懂ConcurrentLinkedQueue自然而然是从offer方法和poll方法开始。那么为了能够理解offer方法,采用debug的方式来一行一行的看代码走。另外,在看多线程的代码时,可采用这样的思维方式: 56 | 57 | > **单个线程offer** 58 | > **多个线程offer** 59 | > **部分线程offer,部分线程poll** 60 | > ----offer的速度快于poll 61 | > --------队列长度会越来越长,由于offer节点总是在对队列队尾,而poll节点总是在队列对头,也就是说offer线程和poll线程两者并无“交集”,也就是说两类线程间并不会相互影响,这种情况站在相对速率的角度来看,也就是一个"单线程offer" 62 | > ----offer的速度慢于poll 63 | > --------poll的相对速率快于offer,也就是队头删的速度要快于队尾添加节点的速度,导致的结果就是队列长度会越来越短,而offer线程和poll线程就会出现“交集”,即那一时刻就可以称之为offer线程和poll线程同时操作的节点为 **临界点** ,且在该节点offer线程和poll线程必定相互影响。根据在临界点时offer和poll发生的相对顺序又可从两个角度去思考:**1. 执行顺序为offer-->poll-->offer**,即表现为当offer线程在Node1后插入Node2时,此时poll线程已经将Node1删除,这种情况很显然需要在offer方法中考虑; **2.执行顺序可能为:poll-->offer-->poll**,即表现为当poll线程准备删除的节点为null时(队列为空队列),此时offer线程插入一个节点使得队列变为非空队列 64 | 65 | 66 | 先看这么一段代码: 67 | 68 | 1. ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); 69 | 2. queue.offer(1); 70 | 3. queue.offer(2); 71 | 创建一个ConcurrentLinkedQueue实例,先offer 1,然后再offer 2。offer的源码为: 72 | 73 | public boolean offer(E e) { 74 | 1. checkNotNull(e); 75 | 2. final Node newNode = new Node(e); 76 | 77 | 3. for (Node t = tail, p = t;;) { 78 | 4. Node q = p.next; 79 | 5. if (q == null) { 80 | 6. // p is last node 81 | 7. if (p.casNext(null, newNode)) { 82 | // Successful CAS is the linearization point 83 | // for e to become an element of this queue, 84 | // and for newNode to become "live". 85 | 8. if (p != t) // hop two nodes at a time 86 | 9. casTail(t, newNode); // Failure is OK. 87 | 10. return true; 88 | } 89 | // Lost CAS race to another thread; re-read next 90 | } 91 | 11. else if (p == q) 92 | // We have fallen off list. If tail is unchanged, it 93 | // will also be off-list, in which case we need to 94 | // jump to head, from which all live nodes are always 95 | // reachable. Else the new tail is a better bet. 96 | 12. p = (t != (t = tail)) ? t : head; 97 | else 98 | // Check for tail updates after two hops. 99 | 13. p = (p != t && t != (t = tail)) ? t : q; 100 | } 101 | } 102 | 103 | 104 | **单线程执行角度分析**: 105 | 106 | 先从**单线程执行的角度**看起,分析offer 1的过程。第1行代码会对是否为null进行判断,为null的话就直接抛出空指针异常,第2行代码将e包装成一个Node类,第3行为for循环,只有初始化条件没有循环结束条件,这很符合CAS的“套路”,在循环体CAS操作成功会直接return返回,如果CAS操作失败的话就在for循环中不断重试直至成功。这里实例变量t被初始化为tail,p被初始化为t即tail。为了方便下面的理解,**p被认为队列真正的尾节点,tail不一定指向对象真正的尾节点,因为在ConcurrentLinkedQueue中tail是被延迟更新的**,具体原因我们慢慢来看。代码走到第3行的时候,t和p都分别指向初始化时创建的item域为null,next域为null的Node0。第4行变量q被赋值为null,第5行if判断为true,在第7行使用casNext将插入的Node设置成当前队列尾节点p的next节点,如果CAS操作失败,此次循环结束在下次循环中进行重试。CAS操作成功走到第8行,此时p==t,if判断为false,直接return true返回。如果成功插入1的话,此时ConcurrentLinkedQueue的状态如下图所示: 107 | 108 | ![2.offer 1后队列的状态.png](http://upload-images.jianshu.io/upload_images/2615789-f2509bec71a8dc33.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 109 | 110 | 如图,此时队列的尾节点应该为Node1,而tail指向的节点依然还是Node0,因此可以说明tail是延迟更新的。那么我们继续来看offer 2的时候的情况,很显然此时第4行q指向的节点不为null了,而是指向Node1,第5行if判断为false,第11行if判断为false,代码会走到第13行。好了,**再插入节点的时候我们会问自己这样一个问题?上面已经解释了tail并不是指向队列真正的尾节点,那么在插入节点的时候,我们是不是应该最开始做的就是找到队列当前的尾节点在哪里才能插入?**那么第13行代码就是**找出队列真正的尾节点**。 111 | 112 | > **定位队列真正的对尾节点** 113 | 114 | p = (p != t && t != (t = tail)) ? t : q; 115 | 116 | 我们来分析一下这行代码,如果这段代码在**单线程环境**执行时,很显然由于p==t,此时p会被赋值为q,而q等于`Node q = p.next`,即Node1。在第一次循环中指针p指向了队列真正的队尾节点Node1,那么在下一次循环中第4行q指向的节点为null,那么在第5行中if判断为true,那么在第7行依然通过casNext方法设置p节点的next为当前新增的Node,接下来走到第8行,这个时候p!=t,第8行if判断为true,会通过`casTail(t, newNode)`将当前节点Node设置为队列的队尾节点,此时的队列状态示意图如下图所示: 117 | ![3.队列offer 2后的状态.png](http://upload-images.jianshu.io/upload_images/2615789-6f8fe58d7a83fe61.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 118 | 119 | 120 | **tail指向的节点由Node0改变为Node2**,这里的casTail失败不需要重试的原因是,offer代码中主要是通过p的next节点q(`Node q = p.next`)决定后面的逻辑走向的,当casTail失败时状态示意图如下: 121 | ![4.队列进行入队操作后casTail失败后的状态图.png](http://upload-images.jianshu.io/upload_images/2615789-3b07de9df192dfc7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 122 | 123 | 124 | 如图,**如果这里casTail设置tail失败即tail还是指向Node0节点的话,无非就是多循环几次通过13行代码定位到队尾节点**。 125 | 126 | 127 | 通过对单线程执行角度进行分析,我们可以了解到poll的执行逻辑为: 128 | 129 | 1. **如果tail指向的节点的下一个节点(next域)为null的话,说明tail指向的节点即为队列真正的队尾节点,因此可以通过casNext插入当前待插入的节点,但此时tail并未变化,如图2;** 130 | 131 | 2. **如果tail指向的节点的下一个节点(next域)不为null的话,说明tail指向的节点不是队列的真正队尾节点。通过`q(Node q = p.next)`指针往前递进去找到队尾节点,然后通过casNext插入当前待插入的节点,并通过casTail方式更改tail,如图3**。 132 | 133 | 我们回过头再来看`p = (p != t && t != (t = tail)) ? t : q;`这行代码在单线程中,这段代码永远不会将p赋值为t,那么这么写就不会有任何作用,那我们试着在**多线程**的情况下进行分析。 134 | 135 | **多线程执行角度分析** 136 | 137 | > **多个线程offer** 138 | 139 | 很显然这么写另有深意,其实在**多线程环境**下这行代码很有意思的。 `t != (t = tail)`这个操作**并非一个原子操作**,有这样一种情况: 140 | 141 | 142 | 143 | 144 | ![5.线程A和线程B有可能的执行时序.png](http://upload-images.jianshu.io/upload_images/2615789-9fd7db3a6c9372ff.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 145 | 146 | 如图,假设线程A此时读取了变量t,线程B刚好在这个时候offer一个Node后,此时会修改tail指针,那么这个时候线程A再次执行t=tail时t会指向另外一个节点,很显然线程A前后两次读取的变量t指向的节点不相同,即`t != (t = tail)`为true,并且由于t指向节点的变化`p != t`也为true,此时该行代码的执行结果为p和t最新的t指针指向了同一个节点,并且此时t也是队列真正的对尾节点。那么,现在已经定位到队列真正的队尾节点,就可以执行offer操作了。 147 | 148 | 149 | > **offer->poll->offer** 150 | 151 | 那么还剩下第11行的代码我们没有分析,大致可以猜想到应该就是回答**一部分线程offer,一部分poll**的这种情况。当`if (p == q)`为true时,说明p指向的节点的next也指向它自己,这种节点称之为**哨兵节点**,**这种节点在队列中存在的价值不大,一般表示为要删除的节点或者是空节点**。为了能够很好的理解这种情况,我们先看看poll方法的执行过程后,再回过头来看,总之这是一个很有意思的事情 :)。 152 | 153 | # 3.poll方法 # 154 | poll方法源码如下: 155 | 156 | public E poll() { 157 | restartFromHead: 158 | 1. for (;;) { 159 | 2. for (Node h = head, p = h, q;;) { 160 | 3. E item = p.item; 161 | 162 | 4. if (item != null && p.casItem(item, null)) { 163 | // Successful CAS is the linearization point 164 | // for item to be removed from this queue. 165 | 5. if (p != h) // hop two nodes at a time 166 | 6. updateHead(h, ((q = p.next) != null) ? q : p); 167 | 7. return item; 168 | } 169 | 8. else if ((q = p.next) == null) { 170 | 9. updateHead(h, p); 171 | 10. return null; 172 | } 173 | 11. else if (p == q) 174 | 12. continue restartFromHead; 175 | else 176 | 13. p = q; 177 | } 178 | } 179 | } 180 | 181 | 我们还是先站在**单线程的角度**去理清该方法的基本逻辑。假设ConcurrentLinkedQueue初始状态如下图所示: 182 | 183 | 184 | ![6.队列初始状态.png](http://upload-images.jianshu.io/upload_images/2615789-450e7301fd19e6df.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 185 | 186 | 187 | 参数offer时的定义,我们还是先将**变量p作为队列要删除真正的队头节点,h(head)指向的节点并不一定是队列的队头节点**。先来看poll出Node1时的情况,由于`p=h=head`,参照上图,很显然此时p指向的Node1的数据域不为null,在第4行代码中`item!=null`判断为true后接下来通过`casItem`将Node1的数据域设置为null。如果CAS设置失败则此次循环结束等待下一次循环进行重试。若第4行执行成功进入到第5行代码,此时p和h都指向Node1,第5行if判断为false,然后直接到第7行return回Node1的数据域1,方法运行结束,此时的队列状态如下图。 188 | 189 | 190 | 191 | ![7.队列出队操作后的状态.png](http://upload-images.jianshu.io/upload_images/2615789-c3c45ac89c461ab5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 192 | 193 | 194 | 下面继续从队列中poll,很显然当前h和p指向的Node1的数据域为null,那么第一件事就是要**定位准备删除的队头节点(找到数据域不为null的节点)**。 195 | 196 | > 定位删除的队头节点 197 | 198 | 199 | 继续看,第三行代码item为null,第4行代码if判断为false,走到第8行代码(`q = p.next`)if也为false,由于q指向了Node2,在第11行的if判断也为false,因此代码走到了第13行,这个时候p和q共同指向了Node2,也就找到了要删除的真正的队头节点。可以总结出,定位待删除的队头节点的过程为:**如果当前节点的数据域为null,很显然该节点不是待删除的节点,就用当前节点的下一个节点去试探**。在经过第一次循环后,此时状态图为下图: 200 | 201 | 202 | 203 | ![8.经过一次循环后的状态.png](http://upload-images.jianshu.io/upload_images/2615789-c4deb3237eefb777.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 204 | 205 | 206 | 进行下一次循环,第4行的操作同上述,当前假设第4行中casItem设置成功,由于p已经指向了Node2,而h还依旧指向Node1,此时第5行的if判断为true,然后执行`updateHead(h, ((q = p.next) != null) ? q : p)`,此时q指向的Node3,所有传入updateHead方法的分别是指向Node1的h引用和指向Node3的q引用。updateHead方法的源码为: 207 | 208 | final void updateHead(Node h, Node p) { 209 | if (h != p && casHead(h, p)) 210 | h.lazySetNext(h); 211 | } 212 | 213 | 该方法主要是通过`casHead`将队列的head指向Node3,并且通过 `h.lazySetNext`将Node1的next域指向它自己。最后在第7行代码中返回Node2的值。此时队列的状态如下图所示: 214 | 215 | ![9.Node2从队列中出队后的状态.png](http://upload-images.jianshu.io/upload_images/2615789-5a93cb7a44f40745.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 216 | 217 | 218 | 219 | Node1的next域指向它自己,head指向了Node3。如果队列为空队列的话,就会执行到代码的第8行`(q = p.next) == null`,if判断为true,因此在第10行中直接返回null。以上的分析是从单线程执行的角度去看,也可以让我们了解poll的整体思路,现在来做一个总结: 220 | 221 | 1. **如果当前head,h和p指向的节点的Item不为null的话,说明该节点即为真正的队头节点(待删除节点),只需要通过casItem方法将item域设置为null,然后将原来的item直接返回即可。** 222 | 223 | 2. **如果当前head,h和p指向的节点的item为null的话,则说明该节点不是真正的待删除节点,那么应该做的就是寻找item不为null的节点。通过让q指向p的下一个节点(q = p.next)进行试探,若找到则通过updateHead方法更新head指向的节点以及构造哨兵节点(`通过updateHead方法的h.lazySetNext(h)`)**。 224 | 225 | 接下来,按照上面分析offer的思维方式,下面来分析一下多线程的情况,第一种情况是; 226 | 227 | **多线程执行情况分析:** 228 | 229 | > **多个线程poll** 230 | 231 | 现在回过头来看poll方法的源码,有这样一部分: 232 | 233 | 234 | else if (p == q) 235 | continue restartFromHead; 236 | 237 | 这一部分就是处理多个线程poll的情况,`q = p.next`也就是说q永远指向的是p的下一个节点,那么什么情况下会使得p,q指向同一个节点呢?根据上面我们的分析,只有p指向的节点在poll的时候转变成了**哨兵节点**(通过updateHead方法中的h.lazySetNext)。当线程A在判断`p==q`时,线程B已经将执行完poll方法将p指向的节点转换为**哨兵节点**并且head指向的节点已经发生了改变,所以就需要从restartFromHead处执行,保证用到的是最新的head。 238 | 239 | > **poll->offer->poll** 240 | 241 | 试想,还有这样一种情况,如果当前队列为空队列,线程A进行poll操作,同时线程B执行offer,然后线程A在执行poll,那么此时线程A返回的是null还是线程B刚插入的最新的那个节点呢?我们来写一代demo: 242 | 243 | public static void main(String[] args) { 244 | Thread thread1 = new Thread(() -> { 245 | Integer value = queue.poll(); 246 | System.out.println(Thread.currentThread().getName() + " poll 的值为:" + value); 247 | System.out.println("queue当前是否为空队列:" + queue.isEmpty()); 248 | }); 249 | thread1.start(); 250 | Thread thread2 = new Thread(() -> { 251 | queue.offer(1); 252 | }); 253 | thread2.start(); 254 | } 255 | 256 | 输出结果为: 257 | 258 | > Thread-0 poll 的值为:null 259 | > queue当前是否为空队列:false 260 | 261 | 262 | 通过debug控制线程thread1和线程thread2的执行顺序,thread1先执行到第8行代码`if ((q = p.next) == null)`,由于此时队列为空队列if判断为true,进入if块,此时先让thread1暂停,然后thread2进行offer插入值为1的节点后,thread2执行结束。再让thread1执行,这时**thread1并没有进行重试**,而是代码继续往下走,返回null,尽管此时队列由于thread2已经插入了值为1的新的节点。所以输出结果为thread0 poll的为null,然队列不为空队列。因此,**在判断队列是否为空队列的时候是不能通过线程在poll的时候返回为null进行判断的,可以通过isEmpty方法进行判断**。 263 | 264 | 265 | # 4. offer方法中部分线程offer部分线程poll # 266 | 267 | 在分析offer方法的时候我们还留下了一个问题,即对offer方法中第11行代码的理解。 268 | 269 | > **offer->poll->offer** 270 | 271 | 在offer方法的第11行代码`if (p == q)`,能够让if判断为true的情况为p指向的节点为**哨兵节点**,而什么时候会构造哨兵节点呢?在对poll方法的讨论中,我们已经找到了答案,即**当head指向的节点的item域为null时会寻找真正的队头节点,等到待插入的节点插入之后,会更新head,并且将原来head指向的节点设置为哨兵节点。**假设队列初始状态如下图所示: 272 | ![10.offer和poll相互影响分析时队列初始状态.png](http://upload-images.jianshu.io/upload_images/2615789-70b0af25bced807a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 273 | 因此在线程A执行offer时,线程B执行poll就会存在如下一种情况: 274 | ![11.线程A和线程B可能存在的执行时序.png](http://upload-images.jianshu.io/upload_images/2615789-cf872ba6fdd99099.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 如图,线程A的tail节点存在next节点Node1,因此会通过引用q往前寻找队列真正的队尾节点,当执行到判断`if (p == q)`时,此时线程B执行poll操作,在对线程B来说,head和p指向Node0,由于Node0的item域为null,同样会往前递进找到队列真正的队头节点Node1,在线程B执行完poll之后,Node0就会转换为**哨兵节点**,也就意味着队列的head发生了改变,此时队列状态为下图。 283 | 284 | 285 | 286 | ![12.线程B进行poll后队列的状态图.png](http://upload-images.jianshu.io/upload_images/2615789-d0d2d16b16c11802.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 287 | 288 | 289 | 此时线程A在执行判断`if (p == q)`时就为true,会继续执行` p = (t != (t = tail)) ? t : head;`,由于tail指针没有发生改变所以p被赋值为head,重新从head开始完成插入操作。 290 | 291 | 292 | # 5. HOPS的设计 # 293 | 294 | 通过上面对offer和poll方法的分析,我们发现tail和head是延迟更新的,两者更新触发时机为: 295 | 296 | **tail更新触发时机**:当tail指向的节点的下一个节点不为null的时候,会执行定位队列真正的队尾节点的操作,找到队尾节点后完成插入之后才会通过casTail进行tail更新;当tail指向的节点的下一个节点为null的时候,只插入节点不更新tail。 297 | 298 | **head更新触发时机:**当head指向的节点的item域为null的时候,会执行定位队列真正的队头节点的操作,找到队头节点后完成删除之后才会通过updateHead进行head更新;当head指向的节点的item域不为null的时候,只删除节点不更新head。 299 | 300 | 并且在更新操作时,源码中会有注释为:**hop two nodes at a time**。所以这种延迟更新的策略就被叫做HOPS的大概原因是这个(猜的 :)),从上面更新时的状态图可以看出,head和tail的更新是“跳着的”即中间总是间隔了一个。那么这样设计的意图是什么呢? 301 | 302 | 如果让tail永远作为队列的队尾节点,实现的代码量会更少,而且逻辑更易懂。但是,这样做有一个缺点,**如果大量的入队操作,每次都要执行CAS进行tail的更新,汇总起来对性能也会是大大的损耗。如果能减少CAS更新的操作,无疑可以大大提升入队的操作效率,所以doug lea大师每间隔1次(tail和队尾节点的距离为1)进行才利用CAS更新tail。**对head的更新也是同样的道理,虽然,这样设计会多出在循环中定位队尾节点,但总体来说读的操作效率要远远高于写的性能,因此,多出来的在循环中定位尾节点的操作的性能损耗相对而言是很小的。 303 | 304 | 305 | >参考资料 306 | 307 | 《java并发编程的艺术》 308 | 《Java高并发程序设计》 309 | ConcurrentLinkedQueue博文:https://www.cnblogs.com/sunshine-2015/p/6067709.html 310 | -------------------------------------------------------------------------------- /15.并发容器之ConcurrentLinkedQueue/线程A和线程B可能存在的执行时序.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/15.并发容器之ConcurrentLinkedQueue/线程A和线程B可能存在的执行时序.png -------------------------------------------------------------------------------- /15.并发容器之ConcurrentLinkedQueue/线程A和线程B有可能的执行时序.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/15.并发容器之ConcurrentLinkedQueue/线程A和线程B有可能的执行时序.png -------------------------------------------------------------------------------- /15.并发容器之ConcurrentLinkedQueue/线程B进行poll后队列的状态图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/15.并发容器之ConcurrentLinkedQueue/线程B进行poll后队列的状态图.png -------------------------------------------------------------------------------- /15.并发容器之ConcurrentLinkedQueue/经过一次循环后的状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/15.并发容器之ConcurrentLinkedQueue/经过一次循环后的状态.png -------------------------------------------------------------------------------- /15.并发容器之ConcurrentLinkedQueue/队列offer 2后的状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/15.并发容器之ConcurrentLinkedQueue/队列offer 2后的状态.png -------------------------------------------------------------------------------- /15.并发容器之ConcurrentLinkedQueue/队列出队操作后的状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/15.并发容器之ConcurrentLinkedQueue/队列出队操作后的状态.png -------------------------------------------------------------------------------- /15.并发容器之ConcurrentLinkedQueue/队列初始状态.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/15.并发容器之ConcurrentLinkedQueue/队列初始状态.png -------------------------------------------------------------------------------- /15.并发容器之ConcurrentLinkedQueue/队列进行入队操作后casTail失败后的状态图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/15.并发容器之ConcurrentLinkedQueue/队列进行入队操作后casTail失败后的状态图.png -------------------------------------------------------------------------------- /16.并发容器之CopyOnWriteArrayList/并发容器之CopyOnWriteArrayList.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 1. CopyOnWriteArrayList的简介 # 4 | java学习者都清楚ArrayList并不是线程安全的,在读线程在读取ArrayList的时候如果有写线程在写数据的时候,基于fast-fail机制,会抛出**ConcurrentModificationException**异常,也就是说ArrayList并不是一个线程安全的容器,当然您可以用Vector,或者使用Collections的静态方法将ArrayList包装成一个线程安全的类,但是这些方式都是采用java关键字synchronzied对方法进行修饰,利用独占式锁来保证线程安全的。但是,由于独占式锁在同一时刻只有一个线程能够获取到对象监视器,很显然这种方式效率并不是太高。 5 | 6 | 回到业务场景中,有很多业务往往是读多写少的,比如系统配置的信息,除了在初始进行系统配置的时候需要写入数据,其他大部分时刻其他模块之后对系统信息只需要进行读取,又比如白名单,黑名单等配置,只需要读取名单配置然后检测当前用户是否在该配置范围以内。类似的还有很多业务场景,它们都是属于**读多写少**的场景。如果在这种情况用到上述的方法,使用Vector,Collections转换的这些方式是不合理的,因为尽管多个读线程从同一个数据容器中读取数据,但是读线程对数据容器的数据并不会发生发生修改。很自然而然的我们会联想到ReenTrantReadWriteLock(关于读写锁可以看[这篇文章](https://juejin.im/post/5aeb0e016fb9a07ab7740d90)),通过**读写分离**的思想,使得读读之间不会阻塞,无疑如果一个list能够做到被多个读线程读取的话,性能会大大提升不少。但是,如果仅仅是将list通过读写锁(ReentrantReadWriteLock)进行再一次封装的话,由于读写锁的特性,当写锁被写线程获取后,读写线程都会被阻塞。如果仅仅使用读写锁对list进行封装的话,这里仍然存在读线程在读数据的时候被阻塞的情况,如果想list的读效率更高的话,这里就是我们的突破口,如果我们保证读线程无论什么时候都不被阻塞,效率岂不是会更高? 7 | 8 | Doug Lea大师就为我们提供CopyOnWriteArrayList容器可以保证线程安全,保证读读之间在任何时候都不会被阻塞,CopyOnWriteArrayList也被广泛应用于很多业务场景之中,CopyOnWriteArrayList值得被我们好好认识一番。 9 | 10 | # 2. COW的设计思想 # 11 | 回到上面所说的,如果简单的使用读写锁的话,在写锁被获取之后,读写线程被阻塞,只有当写锁被释放后读线程才有机会获取到锁从而读到最新的数据,站在**读线程的角度来看,即读线程任何时候都是获取到最新的数据,满足数据实时性**。既然我们说到要进行优化,必然有trade-off,我们就可以**牺牲数据实时性满足数据的最终一致性即可**。而CopyOnWriteArrayList就是通过Copy-On-Write(COW),即写时复制的思想来通过延时更新的策略来实现数据的最终一致性,并且能够保证读线程间不阻塞。 12 | 13 | 14 | COW通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。对CopyOnWrite容器进行并发的读的时候,不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,延时更新的策略是通过在写的时候针对的是不同的数据容器来实现的,放弃数据实时性达到数据的最终一致性。 15 | 16 | # 3. CopyOnWriteArrayList的实现原理 # 17 | 现在我们来通过看源码的方式来理解CopyOnWriteArrayList,实际上CopyOnWriteArrayList内部维护的就是一个数组 18 | 19 | /** The array, accessed only via getArray/setArray. */ 20 | private transient volatile Object[] array; 21 | 22 | 并且该数组引用是被volatile修饰,注意这里**仅仅是修饰的是数组引用**,其中另有玄机,稍后揭晓。关于volatile很重要的一条性质是它能够够保证可见性,关于volatile的详细讲解可以看[这篇文章](https://juejin.im/post/5ae9b41b518825670b33e6c4)。对list来说,我们自然而然最关心的就是读写的时候,分别为get和add方法的实现。 23 | 24 | ## 3.1 get方法实现原理 ## 25 | 26 | get方法的源码为: 27 | 28 | public E get(int index) { 29 | return get(getArray(), index); 30 | } 31 | /** 32 | * Gets the array. Non-private so as to also be accessible 33 | * from CopyOnWriteArraySet class. 34 | */ 35 | final Object[] getArray() { 36 | return array; 37 | } 38 | private E get(Object[] a, int index) { 39 | return (E) a[index]; 40 | } 41 | 可以看出来get方法实现非常简单,几乎就是一个“单线程”程序,没有对多线程添加任何的线程安全控制,也没有加锁也没有CAS操作等等,原因是,所有的读线程只是会读取数据容器中的数据,并不会进行修改。 42 | 43 | ## 3.2 add方法实现原理 ## 44 | 45 | 再来看下如何进行添加数据的?add方法的源码为: 46 | 47 | public boolean add(E e) { 48 | final ReentrantLock lock = this.lock; 49 | //1. 使用Lock,保证写线程在同一时刻只有一个 50 | lock.lock(); 51 | try { 52 | //2. 获取旧数组引用 53 | Object[] elements = getArray(); 54 | int len = elements.length; 55 | //3. 创建新的数组,并将旧数组的数据复制到新数组中 56 | Object[] newElements = Arrays.copyOf(elements, len + 1); 57 | //4. 往新数组中添加新的数据 58 | newElements[len] = e; 59 | //5. 将旧数组引用指向新的数组 60 | setArray(newElements); 61 | return true; 62 | } finally { 63 | lock.unlock(); 64 | } 65 | } 66 | 67 | add方法的逻辑也比较容易理解,请看上面的注释。需要注意这么几点: 68 | 69 | 1. 采用ReentrantLock,保证同一时刻只有一个写线程正在进行数组的复制,否则的话内存中会有多份被复制的数据; 70 | 2. 前面说过数组引用是volatile修饰的,因此将旧的数组引用指向新的数组,根据volatile的happens-before规则,写线程对数组引用的修改对读线程是可见的。 71 | 3. 由于在写数据的时候,是在新的数组中插入数据的,从而保证读写实在两个不同的数据容器中进行操作。 72 | # 4. 总结 # 73 | 我们知道COW和读写锁都是通过读写分离的思想实现的,但两者还是有些不同,可以进行比较: 74 | 75 | > **COW vs 读写锁** 76 | 77 | 相同点:1. 两者都是通过读写分离的思想实现;2.读线程间是互不阻塞的 78 | 79 | 不同点:**对读线程而言,为了实现数据实时性,在写锁被获取后,读线程会等待或者当读锁被获取后,写线程会等待,从而解决“脏读”等问题。也就是说如果使用读写锁依然会出现读线程阻塞等待的情况。而COW则完全放开了牺牲数据实时性而保证数据最终一致性,即读线程对数据的更新是延时感知的,因此读线程不会存在等待的情况**。 80 | 81 | 对这一点从文字上还是很难理解,我们来通过debug看一下,add方法核心代码为: 82 | 83 | 1.Object[] elements = getArray(); 84 | 2.int len = elements.length; 85 | 3.Object[] newElements = Arrays.copyOf(elements, len + 1); 86 | 4.newElements[len] = e; 87 | 5.setArray(newElements); 88 | 89 | 假设COW的变化如下图所示: 90 | 91 | 92 | ![最终一致性的分析.png](http://upload-images.jianshu.io/upload_images/2615789-4519051e92e5252b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 93 | 94 | 95 | 96 | 97 | 98 | 99 | 数组中已有数据1,2,3,现在写线程想往数组中添加数据4,我们在第5行处打上断点,让写线程暂停。读线程依然会“不受影响”的能从数组中读取数据,可是还是只能读到1,2,3。**如果读线程能够立即读到新添加的数据的话就叫做能保证数据实时性**。当对第5行的断点放开后,读线程才能感知到数据变化,读到完整的数据1,2,3,4,而保证**数据最终一致性**,尽管有可能中间间隔了好几秒才感知到。 100 | 101 | 这里还有这样一个问题: **为什么需要复制呢? 如果将array 数组设定为volitile的, 对volatile变量写happens-before读,读线程不是能够感知到volatile变量的变化**。 102 | 103 | 原因是,这里volatile的修饰的**仅仅**只是**数组引用**,**数组中的元素的修改是不能保证可见性的**。因此COW采用的是新旧两个数据容器,通过第5行代码将数组引用指向新的数组。 104 | 105 | 这也是为什么concurrentHashMap只具有弱一致性的原因,关于concurrentHashMap的弱一致性可以[看这篇文章](http://ifeve.com/volatile-array-visiblity/294529737/)。 106 | 107 | 108 | > **COW的缺点** 109 | 110 | CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。 111 | 112 | 1. **内存占用问题**:因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对 象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对 象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比 如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的minor GC和major GC。 113 | 114 | 2. **数据一致性问题**:CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。 115 | 116 | > 参考资料 117 | 118 | 《java并发编程的艺术》 119 | 120 | [COW讲解](https://www.cnblogs.com/dolphin0520/p/3938914.html) 121 | -------------------------------------------------------------------------------- /16.并发容器之CopyOnWriteArrayList/最终一致性的分析.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/16.并发容器之CopyOnWriteArrayList/最终一致性的分析.png -------------------------------------------------------------------------------- /17.并发容器之ThreadLocal/ThreadLocal各引用间的关系.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/17.并发容器之ThreadLocal/ThreadLocal各引用间的关系.png -------------------------------------------------------------------------------- /17.并发容器之ThreadLocal/分离链表法示意图.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/17.并发容器之ThreadLocal/分离链表法示意图.gif -------------------------------------------------------------------------------- /17.并发容器之ThreadLocal/并发容器之ThreadLocal.md: -------------------------------------------------------------------------------- 1 | # 1. ThreadLocal的简介 # 2 | 在多线程编程中通常解决线程安全的问题我们会利用synchronzed或者lock控制线程对临界区资源的同步顺序从而解决线程安全的问题,但是这种加锁的方式会让未获取到锁的线程进行阻塞等待,很显然这种方式的时间效率并不是很好。**线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作**,那么,如果每个线程都使用自己的“共享资源”,各自使用各自的,又互相不影响到彼此即让多个线程间达到隔离的状态,这样就不会出现线程安全的问题。事实上,这就是一种“**空间换时间**”的方案,每个线程都会都拥有自己的“共享资源”无疑内存会大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待的情况从而提高的时间效率。 3 | 4 | 虽然ThreadLocal并不在java.util.concurrent包中而在java.lang包中,但我更倾向于把它当作是一种并发容器(虽然真正存放数据的是ThreadLoclMap)进行归类。从**ThreadLocal这个类名可以顾名思义的进行理解,表示线程的“本地变量”,即每个线程都拥有该变量副本,达到人手一份的效果,各用各的这样就可以避免共享资源的竞争**。 5 | 6 | 7 | # 2. ThreadLocal的实现原理 8 | 要想学习到ThreadLocal的实现原理,就必须了解它的几个核心方法,包括怎样存怎样取等等,下面我们一个个来看。 9 | 10 | > **void set(T value)** 11 | 12 | **set方法设置在当前线程中threadLocal变量的值**,该方法的源码为: 13 | 14 | public void set(T value) { 15 | //1. 获取当前线程实例对象 16 | Thread t = Thread.currentThread(); 17 | //2. 通过当前线程实例获取到ThreadLocalMap对象 18 | ThreadLocalMap map = getMap(t); 19 | if (map != null) 20 | //3. 如果Map不为null,则以当前threadLocl实例为key,值为value进行存入 21 | map.set(this, value); 22 | else 23 | //4.map为null,则新建ThreadLocalMap并存入value 24 | createMap(t, value); 25 | } 26 | 27 | 方法的逻辑很清晰,具体请看上面的注释。通过源码我们知道value是存放在了ThreadLocalMap里了,当前先把它理解为一个普普通通的map即可,也就是说,**数据value是真正的存放在了ThreadLocalMap这个容器中了,并且是以当前threadLocal实例为key**。先简单的看下ThreadLocalMap是什么,有个简单的认识就好,下面会具体说的。 28 | 29 | **首先ThreadLocalMap是怎样来的**?源码很清楚,是通过`getMap(t)`进行获取: 30 | 31 | ThreadLocalMap getMap(Thread t) { 32 | return t.threadLocals; 33 | } 34 | 35 | 该方法直接返回的就是当前线程对象t的一个成员变量threadLocals: 36 | 37 | /* ThreadLocal values pertaining to this thread. This map is maintained 38 | * by the ThreadLocal class. */ 39 | ThreadLocal.ThreadLocalMap threadLocals = null; 40 | 也就是说**ThreadLocalMap的引用是作为Thread的一个成员变量,被Thread进行维护的**。回过头再来看看set方法,当map为Null的时候会通过`createMap(t,value)`方法: 41 | 42 | void createMap(Thread t, T firstValue) { 43 | t.threadLocals = new ThreadLocalMap(this, firstValue); 44 | } 45 | 该方法就是**new一个ThreadLocalMap实例对象,然后同样以当前threadLocal实例作为key,值为value存放到threadLocalMap中,然后将当前线程对象的threadLocals赋值为threadLocalMap**。 46 | 47 | 现在来对set方法进行总结一下: 48 | **通过当前线程对象thread获取该thread所维护的threadLocalMap,若threadLocalMap不为null,则以threadLocal实例为key,值为value的键值对存入threadLocalMap,若threadLocalMap为null的话,就新建threadLocalMap然后在以threadLocal为键,值为value的键值对存入即可。** 49 | 50 | > T get() 51 | 52 | **get方法是获取当前线程中threadLocal变量的值**,同样的还是来看看源码: 53 | 54 | public T get() { 55 | //1. 获取当前线程的实例对象 56 | Thread t = Thread.currentThread(); 57 | //2. 获取当前线程的threadLocalMap 58 | ThreadLocalMap map = getMap(t); 59 | if (map != null) { 60 | //3. 获取map中当前threadLocal实例为key的值的entry 61 | ThreadLocalMap.Entry e = map.getEntry(this); 62 | if (e != null) { 63 | @SuppressWarnings("unchecked") 64 | //4. 当前entitiy不为null的话,就返回相应的值value 65 | T result = (T)e.value; 66 | return result; 67 | } 68 | } 69 | //5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value 70 | return setInitialValue(); 71 | } 72 | 73 | 弄懂了set方法的逻辑,看get方法只需要带着逆向思维去看就好,如果是那样存的,反过来去拿就好。代码逻辑请看注释,另外,看下setInitialValue主要做了些什么事情? 74 | 75 | private T setInitialValue() { 76 | T value = initialValue(); 77 | Thread t = Thread.currentThread(); 78 | ThreadLocalMap map = getMap(t); 79 | if (map != null) 80 | map.set(this, value); 81 | else 82 | createMap(t, value); 83 | return value; 84 | } 85 | 86 | 这段方法的逻辑和set方法几乎一致,另外值得关注的是initialValue方法: 87 | 88 | protected T initialValue() { 89 | return null; 90 | } 91 | 92 | 这个**方法是protected修饰的也就是说继承ThreadLocal的子类可重写该方法,实现赋值为其他的初始值**。关于get方法来总结一下: 93 | 94 | **通过当前线程thread实例获取到它所维护的threadLocalMap,然后以当前threadLocal实例为key获取该map中的键值对(Entry),若Entry不为null则返回Entry的value。如果获取threadLocalMap为null或者Entry为null的话,就以当前threadLocal为Key,value为null存入map后,并返回null。** 95 | 96 | > void remove() 97 | 98 | public void remove() { 99 | //1. 获取当前线程的threadLocalMap 100 | ThreadLocalMap m = getMap(Thread.currentThread()); 101 | if (m != null) 102 | //2. 从map中删除以当前threadLocal实例为key的键值对 103 | m.remove(this); 104 | } 105 | get,set方法实现了存数据和读数据,我们当然还得学会如何删数据**。删除数据当然是从map中删除数据,先获取与当前线程相关联的threadLocalMap然后从map中删除该threadLocal实例为key的键值对即可**。 106 | # 3. ThreadLocalMap详解 # 107 | 从上面的分析我们已经知道,数据其实都放在了threadLocalMap中,threadLocal的get,set和remove方法实际上具体是通过threadLocalMap的getEntry,set和remove方法实现的。如果想真正全方位的弄懂threadLocal,势必得在对threadLocalMap做一番理解。 108 | 109 | ## 3.1 Entry数据结构 ## 110 | 111 | ThreadLocalMap是threadLocal一个静态内部类,和大多数容器一样内部维护了一个数组,同样的threadLocalMap内部维护了一个Entry类型的table数组。 112 | 113 | /** 114 | * The table, resized as necessary. 115 | * table.length MUST always be a power of two. 116 | */ 117 | private Entry[] table; 118 | 通过注释可以看出,table数组的长度为2的幂次方。接下来看下Entry是什么: 119 | 120 | 121 | static class Entry extends WeakReference> { 122 | /** The value associated with this ThreadLocal. */ 123 | Object value; 124 | 125 | Entry(ThreadLocal k, Object v) { 126 | super(k); 127 | value = v; 128 | } 129 | } 130 | 131 | Entry是一个以ThreadLocal为key,Object为value的键值对,另外需要注意的是这里的**threadLocal是弱引用,因为Entry继承了WeakReference,在Entry的构造方法中,调用了super(k)方法就会将threadLocal实例包装成一个WeakReferenece。**到这里我们可以用一个图(下图来自http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/)来理解下thread,threadLocal,threadLocalMap,Entry之间的关系: 132 | 133 | ![ThreadLocal各引用间的关系](http://upload-images.jianshu.io/upload_images/2615789-12aef2e6ff040cae.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/610) 134 | 135 | 136 | 137 | 注意上图中的实线表示强引用,虚线表示弱引用。如图所示,每个线程实例中可以通过threadLocals获取到threadLocalMap,而threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry数组。当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放。需要注意的是**Entry中的key是弱引用,当threadLocal外部强引用被置为null(`threadLocalInstance=null`),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。**当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。在实际开发中,会使用线程池去维护线程的创建和复用,比如固定大小的线程池,线程为了复用是不会主动结束的,所以,threadLocal的内存泄漏问题,是应该值得我们思考和注意的问题,关于这个问题可以看这篇文章----[详解threadLocal内存泄漏问题](http://www.jianshu.com/p/dde92ec37bd1) 138 | 139 | ## 3.2 set方法 ## 140 | 141 | 与concurrentHashMap,hashMap等容器一样,threadLocalMap也是采用散列表进行实现的。在了解set方法前,我们先来回顾下关于散列表相关的知识(摘自[这篇的threadLocalMap的讲解部分](https://www.cnblogs.com/zhangjk1993/archive/2017/03/29/6641745.html)以及[这篇文章的hash](http://faculty.cs.niu.edu/~freedman/340/340notes/340hash.htm))。 142 | 143 | 144 | 145 | 146 | - 散列表 147 | 148 | 理想状态下,散列表就是一个包含关键字的固定大小的数组,通过使用散列函数,将关键字映射到数组的不同位置。下面是 149 | 150 | 151 | ![理想散列表的一个示意图](http://upload-images.jianshu.io/upload_images/2615789-bf2dfb86819f6823.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 152 | 153 | 在理想状态下,哈希函数可以将关键字均匀的分散到数组的不同位置,不会出现两个关键字散列值相同(假设关键字数量小于数组的大小)的情况。但是在实际使用中,经常会出现多个关键字散列值相同的情况(被映射到数组的同一个位置),我们将这种情况称为散列冲突。为了解决散列冲突,主要采用下面两种方式: **分离链表法**(separate chaining)和**开放定址法**(open addressing) 154 | 155 | 156 | 157 | - 分离链表法 158 | 159 | 分散链表法使用链表解决冲突,将散列值相同的元素都保存到一个链表中。当查询的时候,首先找到元素所在的链表,然后遍历链表查找对应的元素,典型实现为hashMap,concurrentHashMap的拉链法。下面是一个示意图: 160 | 161 | 162 | ![分离链表法示意图](http://upload-images.jianshu.io/upload_images/2615789-32b422909f2f933c.gif?imageMogr2/auto-orient/strip%7CimageView2/2/w/610) 163 | 164 | 165 | 166 | 图片来自 http://faculty.cs.niu.edu/~freedman/340/340notes/340hash.htm 167 | 168 | 169 | - 开放定址法 170 | 171 | 开放定址法不会创建链表,当关键字散列到的数组单元已经被另外一个关键字占用的时候,就会尝试在数组中寻找其他的单元,直到找到一个空的单元。探测数组空单元的方式有很多,这里介绍一种最简单的 -- 线性探测法。线性探测法就是从冲突的数组单元开始,依次往后搜索空单元,如果到数组尾部,再从头开始搜索(环形查找)。如下图所示: 172 | 173 | ![开放定址法示意图](http://upload-images.jianshu.io/upload_images/2615789-0d85565e94c4bd6b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/610) 174 | 175 | 176 | 图片来自 http://alexyyek.github.io/2014/12/14/hashCollapse/ 177 | 178 | 关于两种方式的比较,可以参考 [这篇文章](http://www.nowamagic.net/academy/detail/3008060)。**ThreadLocalMap 中使用开放地址法来处理散列冲突**,而 HashMap 中使用的分离链表法。之所以采用不同的方式主要是因为:在 ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突。并且 ThreadLocalMap 经常需要清除无用的对象,使用纯数组更加方便。 179 | 180 | 在了解这些相关知识后我们再回过头来看一下set方法。set方法的源码为: 181 | 182 | private void set(ThreadLocal key, Object value) { 183 | 184 | // We don't use a fast path as with get() because it is at 185 | // least as common to use set() to create new entries as 186 | // it is to replace existing ones, in which case, a fast 187 | // path would fail more often than not. 188 | 189 | Entry[] tab = table; 190 | int len = tab.length; 191 | //根据threadLocal的hashCode确定Entry应该存放的位置 192 | int i = key.threadLocalHashCode & (len-1); 193 | 194 | //采用开放地址法,hash冲突的时候使用线性探测 195 | for (Entry e = tab[i]; 196 | e != null; 197 | e = tab[i = nextIndex(i, len)]) { 198 | ThreadLocal k = e.get(); 199 | //覆盖旧Entry 200 | if (k == key) { 201 | e.value = value; 202 | return; 203 | } 204 | //当key为null时,说明threadLocal强引用已经被释放掉,那么就无法 205 | //再通过这个key获取threadLocalMap中对应的entry,这里就存在内存泄漏的可能性 206 | if (k == null) { 207 | //用当前插入的值替换掉这个key为null的“脏”entry 208 | replaceStaleEntry(key, value, i); 209 | return; 210 | } 211 | } 212 | //新建entry并插入table中i处 213 | tab[i] = new Entry(key, value); 214 | int sz = ++size; 215 | //插入后再次清除一些key为null的“脏”entry,如果大于阈值就需要扩容 216 | if (!cleanSomeSlots(i, sz) && sz >= threshold) 217 | rehash(); 218 | } 219 | 220 | 221 | set方法的关键部分**请看上面的注释**,主要有这样几点需要注意: 222 | 223 | 1. threadLocal的hashcode? 224 | 225 | private final int threadLocalHashCode = nextHashCode(); 226 | private static final int HASH_INCREMENT = 0x61c88647; 227 | private static AtomicInteger nextHashCode =new AtomicInteger(); 228 | /** 229 | * Returns the next hash code. 230 | */ 231 | private static int nextHashCode() { 232 | return nextHashCode.getAndAdd(HASH_INCREMENT); 233 | } 234 | 从源码中我们可以清楚的看到threadLocal实例的hashCode是通过nextHashCode()方法实现的,该方法实际上总是用一个AtomicInteger加上0x61c88647来实现的。0x61c88647这个数是有特殊意义的,它能够保证hash表的每个散列桶能够均匀的分布,这是`Fibonacci Hashing`,关于更多介绍可以看[这篇文章的threadLocal散列值部分](https://www.cnblogs.com/zhangjk1993/archive/2017/03/29/6641745.html)。也正是能够均匀分布,所以threadLocal选择使用开放地址法来解决hash冲突的问题。 235 | 236 | 2. 怎样确定新值插入到哈希表中的位置? 237 | 238 | 该操作源码为:`key.threadLocalHashCode & (len-1)`,同hashMap和ConcurrentHashMap等容器的方式一样,利用当前key(即threadLocal实例)的hashcode与哈希表大小相与,因为哈希表大小总是为2的幂次方,所以相与等同于一个取模的过程,这样就可以通过Key分配到具体的哈希桶中去。而至于为什么取模要通过位与运算的原因就是位运算的执行效率远远高于了取模运算。 239 | 240 | 3. 怎样解决hash冲突? 241 | 242 | 源码中通过`nextIndex(i, len)`方法解决hash冲突的问题,该方法为`((i + 1 < len) ? i + 1 : 0);`,也就是不断往后线性探测,当到哈希表末尾的时候再从0开始,成环形。 243 | 244 | 4. 怎样解决“脏”Entry? 245 | 246 | 在分析threadLocal,threadLocalMap以及Entry的关系的时候,我们已经知道使用threadLocal有可能存在内存泄漏(对象创建出来后,在之后的逻辑一直没有使用该对象,但是垃圾回收器无法回收这个部分的内存),在源码中针对这种key为null的Entry称之为“stale entry”,直译为不新鲜的entry,我把它理解为“脏entry”,自然而然,Josh Bloch and Doug Lea大师考虑到了这种情况,在set方法的for循环中寻找和当前Key相同的可覆盖entry的过程中通过**replaceStaleEntry**方法解决脏entry的问题。如果当前table[i]为null的话,直接插入新entry后也会通过**cleanSomeSlots**来解决脏entry的问题,关于[cleanSomeSlots和replaceStaleEntry方法,会在详解threadLocal内存泄漏中讲到,具体可看那篇文章](http://www.jianshu.com/p/dde92ec37bd1) 247 | 248 | 5. 如何进行扩容? 249 | 250 | > threshold的确定 251 | 252 | 也几乎和大多数容器一样,threadLocalMap会有扩容机制,那么它的threshold又是怎样确定的了? 253 | 254 | private int threshold; // Default to 0 255 | /** 256 | * The initial capacity -- MUST be a power of two. 257 | */ 258 | private static final int INITIAL_CAPACITY = 16; 259 | 260 | ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { 261 | table = new Entry[INITIAL_CAPACITY]; 262 | int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 263 | table[i] = new Entry(firstKey, firstValue); 264 | size = 1; 265 | setThreshold(INITIAL_CAPACITY); 266 | } 267 | 268 | /** 269 | * Set the resize threshold to maintain at worst a 2/3 load factor. 270 | */ 271 | private void setThreshold(int len) { 272 | threshold = len * 2 / 3; 273 | } 274 | 275 | 276 | 根据源码可知,在第一次为threadLocal进行赋值的时候会创建初始大小为16的threadLocalMap,并且通过setThreshold方法设置threshold,其值为当前哈希数组长度乘以(2/3),也就是说加载因子为2/3(**加载因子是衡量哈希表密集程度的一个参数,如果加载因子越大的话,说明哈希表被装载的越多,出现hash冲突的可能性越大,反之,则被装载的越少,出现hash冲突的可能性越小。同时如果过小,很显然内存使用率不高,该值取值应该考虑到内存使用率和hash冲突概率的一个平衡,如hashMap,concurrentHashMap的加载因子都为0.75**)。这里**threadLocalMap初始大小为16**,**加载因子为2/3**,所以哈希表可用大小为:16*2/3=10,即哈希表可用容量为10。 277 | 278 | 279 | 280 | > 扩容resize 281 | 282 | 从set方法中可以看出当hash表的size大于threshold的时候,会通过resize方法进行扩容。 283 | 284 | /** 285 | * Double the capacity of the table. 286 | */ 287 | private void resize() { 288 | Entry[] oldTab = table; 289 | int oldLen = oldTab.length; 290 | //新数组为原数组的2倍 291 | int newLen = oldLen * 2; 292 | Entry[] newTab = new Entry[newLen]; 293 | int count = 0; 294 | 295 | for (int j = 0; j < oldLen; ++j) { 296 | Entry e = oldTab[j]; 297 | if (e != null) { 298 | ThreadLocal k = e.get(); 299 | //遍历过程中如果遇到脏entry的话直接另value为null,有助于value能够被回收 300 | if (k == null) { 301 | e.value = null; // Help the GC 302 | } else { 303 | //重新确定entry在新数组的位置,然后进行插入 304 | int h = k.threadLocalHashCode & (newLen - 1); 305 | while (newTab[h] != null) 306 | h = nextIndex(h, newLen); 307 | newTab[h] = e; 308 | count++; 309 | } 310 | } 311 | } 312 | //设置新哈希表的threshHold和size属性 313 | setThreshold(newLen); 314 | size = count; 315 | table = newTab; 316 | } 317 | 318 | 方法逻辑**请看注释**,新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的entry并将其插入到新的hash数组中,主要注意的是,**在扩容的过程中针对脏entry的话会令value为null,以便能够被垃圾回收器能够回收,解决隐藏的内存泄漏的问题**。 319 | 320 | 321 | ## 3.3 getEntry方法 ## 322 | 323 | getEntry方法源码为: 324 | 325 | private Entry getEntry(ThreadLocal key) { 326 | //1. 确定在散列数组中的位置 327 | int i = key.threadLocalHashCode & (table.length - 1); 328 | //2. 根据索引i获取entry 329 | Entry e = table[i]; 330 | //3. 满足条件则返回该entry 331 | if (e != null && e.get() == key) 332 | return e; 333 | else 334 | //4. 未查找到满足条件的entry,额外在做的处理 335 | return getEntryAfterMiss(key, i, e); 336 | } 337 | 338 | 方法逻辑很简单,若能当前定位的entry的key和查找的key相同的话就直接返回这个entry,否则的话就是在set的时候存在hash冲突的情况,需要通过getEntryAfterMiss做进一步处理。getEntryAfterMiss方法为: 339 | 340 | private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { 341 | Entry[] tab = table; 342 | int len = tab.length; 343 | 344 | while (e != null) { 345 | ThreadLocal k = e.get(); 346 | if (k == key) 347 | //找到和查询的key相同的entry则返回 348 | return e; 349 | if (k == null) 350 | //解决脏entry的问题 351 | expungeStaleEntry(i); 352 | else 353 | //继续向后环形查找 354 | i = nextIndex(i, len); 355 | e = tab[i]; 356 | } 357 | return null; 358 | } 359 | 360 | 这个方法同样很好理解,通过nextIndex往后环形查找,如果找到和查询的key相同的entry的话就直接返回,如果在查找过程中遇到脏entry的话使用expungeStaleEntry方法进行处理。到目前为止**,为了解决潜在的内存泄漏的问题,在set,resize,getEntry这些地方都会对这些脏entry进行处理,可见为了尽可能解决这个问题几乎无时无刻都在做出努力。** 361 | 362 | ## 3.4 remove ## 363 | 364 | 365 | /** 366 | * Remove the entry for key. 367 | */ 368 | private void remove(ThreadLocal key) { 369 | Entry[] tab = table; 370 | int len = tab.length; 371 | int i = key.threadLocalHashCode & (len-1); 372 | for (Entry e = tab[i]; 373 | e != null; 374 | e = tab[i = nextIndex(i, len)]) { 375 | if (e.get() == key) { 376 | //将entry的key置为null 377 | e.clear(); 378 | //将该entry的value也置为null 379 | expungeStaleEntry(i); 380 | return; 381 | } 382 | } 383 | } 384 | 385 | 该方法逻辑很简单,通过往后环形查找到与指定key相同的entry后,先通过clear方法将key置为null后,使其转换为一个脏entry,然后调用expungeStaleEntry方法将其value置为null,以便垃圾回收时能够清理,同时将table[i]置为null。 386 | 387 | # 4. ThreadLocal的使用场景 # 388 | 389 | **ThreadLocal 不是用来解决共享对象的多线程访问问题的**,数据实质上是放在每个thread实例引用的threadLocalMap,也就是说**每个不同的线程都拥有专属于自己的数据容器(threadLocalMap),彼此不影响**。因此threadLocal只适用于 **共享对象会造成线程安全** 的业务场景。比如**hibernate中通过threadLocal管理Session**就是一个典型的案例,不同的请求线程(用户)拥有自己的session,若将session共享出去被多线程访问,必然会带来线程安全问题。下面,我们自己来写一个例子,SimpleDateFormat.parse方法会有线程安全的问题,我们可以尝试使用threadLocal包装SimpleDateFormat,将该实例不被多线程共享即可。 390 | 391 | public class ThreadLocalDemo { 392 | private static ThreadLocal sdf = new ThreadLocal<>(); 393 | 394 | public static void main(String[] args) { 395 | ExecutorService executorService = Executors.newFixedThreadPool(10); 396 | for (int i = 0; i < 100; i++) { 397 | executorService.submit(new DateUtil("2019-11-25 09:00:" + i % 60)); 398 | } 399 | } 400 | 401 | static class DateUtil implements Runnable { 402 | private String date; 403 | 404 | public DateUtil(String date) { 405 | this.date = date; 406 | } 407 | 408 | @Override 409 | public void run() { 410 | if (sdf.get() == null) { 411 | sdf.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); 412 | } else { 413 | try { 414 | Date date = sdf.get().parse(this.date); 415 | System.out.println(date); 416 | } catch (ParseException e) { 417 | e.printStackTrace(); 418 | } 419 | } 420 | } 421 | } 422 | } 423 | 424 | 1. 如果当前线程不持有SimpleDateformat对象实例,那么就新建一个并把它设置到当前线程中,如果已经持有,就直接使用。另外,**从` if (sdf.get() == null){....}else{.....}`可以看出为每一个线程分配一个SimpleDateformat对象实例是从应用层面(业务代码逻辑)去保证的。** 425 | 2. 在上面我们说过threadLocal有可能存在内存泄漏,在使用完之后,最好使用remove方法将这个变量移除,就像在使用数据库连接一样,及时关闭连接。 426 | 427 | > 参考资料 428 | 429 | 《java高并发程序设计》 430 | [这篇文章的threadLocalMap讲解和threadLocal的hashCode讲解不错](https://www.cnblogs.com/zhangjk1993/archive/2017/03/29/6641745.html) 431 | [这篇文章讲解了hash,不错](http://faculty.cs.niu.edu/~freedman/340/340notes/340hash.htm) 432 | [解决hash冲突 链地址法和开放地址法的比较](http://www.nowamagic.net/academy/detail/3008060) 433 | -------------------------------------------------------------------------------- /17.并发容器之ThreadLocal/开放定址法示意图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/17.并发容器之ThreadLocal/开放定址法示意图.jpg -------------------------------------------------------------------------------- /17.并发容器之ThreadLocal/理想散列表的一个示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/17.并发容器之ThreadLocal/理想散列表的一个示意图.png -------------------------------------------------------------------------------- /18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/1.2情况示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/1.2情况示意图.png -------------------------------------------------------------------------------- /18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/cleanSomeSlots执行情景图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/cleanSomeSlots执行情景图.png -------------------------------------------------------------------------------- /18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/cleanSomeSlots示意图 (2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/cleanSomeSlots示意图 (2).png -------------------------------------------------------------------------------- /18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/cleanSomeSlots示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/cleanSomeSlots示意图.png -------------------------------------------------------------------------------- /18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/threadLocal引用示意图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/threadLocal引用示意图.png -------------------------------------------------------------------------------- /18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/前向未搜索到脏entry,后向环形搜索到可覆盖的entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/前向未搜索到脏entry,后向环形搜索到可覆盖的entry.png -------------------------------------------------------------------------------- /18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/前向环形搜索到脏entry,向后环形未搜索可覆盖entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/前向环形搜索到脏entry,向后环形未搜索可覆盖entry.png -------------------------------------------------------------------------------- /18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/前向环形未搜索到脏entry,后向环形查找未查找到可覆盖的entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/前向环形未搜索到脏entry,后向环形查找未查找到可覆盖的entry.png -------------------------------------------------------------------------------- /18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/向前环形搜索到脏entry,向后环形查找到可覆盖的entry的情况.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/18.一篇文章,从源码深入详解ThreadLocal内存泄漏问题/向前环形搜索到脏entry,向后环形查找到可覆盖的entry的情况.png -------------------------------------------------------------------------------- /18362989059.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/18362989059.pdf -------------------------------------------------------------------------------- /19.并发容器之BlockingQueue/BlockingQueue和BlockingDeque的区别.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/19.并发容器之BlockingQueue/BlockingQueue和BlockingDeque的区别.png -------------------------------------------------------------------------------- /19.并发容器之BlockingQueue/BlockingQueue基本操作.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/19.并发容器之BlockingQueue/BlockingQueue基本操作.png -------------------------------------------------------------------------------- /19.并发容器之BlockingQueue/LinkedBlockingDeque的基本操作.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/19.并发容器之BlockingQueue/LinkedBlockingDeque的基本操作.png -------------------------------------------------------------------------------- /19.并发容器之BlockingQueue/并发容器之BlockingQueue.md: -------------------------------------------------------------------------------- 1 | 2 | # 1. BlockingQueue简介 # 3 | 在实际编程中,会经常使用到JDK中Collection集合框架中的各种容器类如实现List,Map,Queue接口的容器类,但是这些容器类基本上不是线程安全的,除了使用Collections可以将其转换为线程安全的容器,Doug Lea大师为我们都准备了对应的线程安全的容器,如实现List接口的CopyOnWriteArrayList([关于CopyOnWriteArrayList可以看这篇文章](https://juejin.im/post/5aeeb55f5188256715478c21)),实现Map接口的ConcurrentHashMap([关于ConcurrentHashMap可以看这篇文章](https://juejin.im/post/5aeeaba8f265da0b9d781d16)),实现Queue接口的ConcurrentLinkedQueue([关于ConcurrentLinkedQueue可以看这篇文章](https://juejin.im/post/5aeeae756fb9a07ab11112af))。 4 | 5 | 最常用的"**生产者-消费者**"问题中,队列通常被视作线程间操作的数据容器,这样,可以对各个模块的业务功能进行解耦,生产者将“生产”出来的数据放置在数据容器中,而消费者仅仅只需要在“数据容器”中进行获取数据即可,这样生产者线程和消费者线程就能够进行解耦,只专注于自己的业务功能即可。阻塞队列(BlockingQueue)被广泛使用在“生产者-消费者”问题中,其原因是BlockingQueue提供了可阻塞的插入和移除的方法。**当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止。** 6 | 7 | # 2. 基本操作 # 8 | 9 | BlockingQueue基本操作总结如下(此图来源于JAVA API文档): 10 | 11 | ![BlockingQueue基本操作.png](http://upload-images.jianshu.io/upload_images/2615789-19d06e0ba334fe52.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 12 | 13 | BlockingQueue继承于Queue接口,因此,对数据元素的基本操作有: 14 | 15 | > 插入元素 16 | 17 | 1. add(E e) :往队列插入数据,当队列满时,插入元素时会抛出IllegalStateException异常; 18 | 2. offer(E e):当往队列插入数据时,插入成功返回`true`,否则则返回`false`。当队列满时不会抛出异常; 19 | 20 | > 删除元素 21 | 22 | 1. remove(Object o):从队列中删除数据,成功则返回`true`,否则为`false` 23 | 2. poll:删除数据,当队列为空时,返回null; 24 | 25 | > 查看元素 26 | 27 | 1. element:获取队头元素,如果队列为空时则抛出NoSuchElementException异常; 28 | 2. peek:获取队头元素,如果队列为空则抛出NoSuchElementException异常 29 | 30 | BlockingQueue具有的特殊操作: 31 | 32 | > 插入数据: 33 | 34 | 1. put:当阻塞队列容量已经满时,往阻塞队列插入数据的线程会被阻塞,直至阻塞队列已经有空余的容量可供使用; 35 | 2. offer(E e, long timeout, TimeUnit unit):若阻塞队列已经满时,同样会阻塞插入数据的线程,直至阻塞队列已经有空余的地方,与put方法不同的是,该方法会有一个超时时间,若超过当前给定的超时时间,插入数据的线程会退出; 36 | 37 | > 删除数据 38 | 39 | 1. take():当阻塞队列为空时,获取队头数据的线程会被阻塞; 40 | 2. poll(long timeout, TimeUnit unit):当阻塞队列为空时,获取数据的线程会被阻塞,另外,如果被阻塞的线程超过了给定的时长,该线程会退出 41 | 42 | 43 | # 3. 常用的BlockingQueue # 44 | 实现BlockingQueue接口的有`ArrayBlockingQueue, DelayQueue, LinkedBlockingDeque, LinkedBlockingQueue, LinkedTransferQueue, PriorityBlockingQueue, SynchronousQueue`,而这几种常见的阻塞队列也是在实际编程中会常用的,下面对这几种常见的阻塞队列进行说明: 45 | 46 | > 1.ArrayBlockingQueue 47 | 48 | **ArrayBlockingQueue**是由数组实现的有界阻塞队列。该队列命令元素FIFO(先进先出)。因此,对头元素时队列中存在时间最长的数据元素,而对尾数据则是当前队列最新的数据元素。ArrayBlockingQueue可作为“有界数据缓冲区”,生产者插入数据到队列容器中,并由消费者提取。ArrayBlockingQueue一旦创建,容量不能改变。 49 | 50 | 当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。 51 | 52 | ArrayBlockingQueue默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到ArrayBlockingQueue。而非公平性则是指访问ArrayBlockingQueue的顺序不是遵守严格的时间顺序,有可能存在,一旦ArrayBlockingQueue可以被访问时,长时间阻塞的线程依然无法访问到ArrayBlockingQueue。**如果保证公平性,通常会降低吞吐量**。如果需要获得公平性的ArrayBlockingQueue,可采用如下代码: 53 | 54 | private static ArrayBlockingQueue blockingQueue = new ArrayBlockingQueue(10,true); 55 | 56 | 关于ArrayBlockingQueue的实现原理,可以[看这篇文章](https://juejin.im/post/5aeebdb26fb9a07aa83ea17e)。 57 | 58 | 59 | > 2.LinkedBlockingQueue 60 | 61 | LinkedBlockingQueue是用链表实现的有界阻塞队列,同样满足FIFO的特性,与ArrayBlockingQueue相比起来具有更高的吞吐量,为了防止LinkedBlockingQueue容量迅速增,损耗大量内存。通常在创建LinkedBlockingQueue对象时,会指定其大小,如果未指定,容量等于Integer.MAX_VALUE 62 | 63 | 64 | > 3.PriorityBlockingQueue 65 | 66 | PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现compareTo()方法来指定元素排序规则,或者初始化时通过构造器参数Comparator来指定排序规则。 67 | 68 | > 4.SynchronousQueue 69 | 70 | SynchronousQueue每个插入操作必须等待另一个线程进行相应的删除操作,因此,SynchronousQueue实际上没有存储任何数据元素,因为只有线程在删除数据时,其他线程才能插入数据,同样的,如果当前有线程在插入数据时,线程才能删除数据。SynchronousQueue也可以通过构造器参数来为其指定公平性。 71 | 72 | 73 | > 5.LinkedTransferQueue 74 | 75 | LinkedTransferQueue是一个由链表数据结构构成的无界阻塞队列,由于该队列实现了TransferQueue接口,与其他阻塞队列相比主要有以下不同的方法: 76 | 77 | **transfer(E e)** 78 | 如果当前有线程(消费者)正在调用take()方法或者可延时的poll()方法进行消费数据时,生产者线程可以调用transfer方法将数据传递给消费者线程。如果当前没有消费者线程的话,生产者线程就会将数据插入到队尾,直到有消费者能够进行消费才能退出; 79 | 80 | **tryTransfer(E e)** 81 | tryTransfer方法如果当前有消费者线程(调用take方法或者具有超时特性的poll方法)正在消费数据的话,该方法可以将数据立即传送给消费者线程,如果当前没有消费者线程消费数据的话,就立即返回`false`。因此,与transfer方法相比,transfer方法是必须等到有消费者线程消费数据时,生产者线程才能够返回。而tryTransfer方法能够立即返回结果退出。 82 | 83 | **tryTransfer(E e,long timeout,imeUnit unit)**
84 | 与transfer基本功能一样,只是增加了超时特性,如果数据才规定的超时时间内没有消费者进行消费的话,就返回`false`。 85 | 86 | 87 | > 6.LinkedBlockingDeque 88 | 89 | LinkedBlockingDeque是基于链表数据结构的有界阻塞双端队列,如果在创建对象时为指定大小时,其默认大小为Integer.MAX_VALUE。与LinkedBlockingQueue相比,主要的不同点在于,LinkedBlockingDeque具有双端队列的特性。LinkedBlockingDeque基本操作如下图所示(来源于java文档) 90 | 91 | ![LinkedBlockingDeque的基本操作.png](http://upload-images.jianshu.io/upload_images/2615789-d51d940d30786e32.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/600) 92 | 93 | 94 | 95 | 如上图所示,LinkedBlockingDeque的基本操作可以分为四种类型:1.特殊情况,抛出异常;2.特殊情况,返回特殊值如null或者false;3.当线程不满足操作条件时,线程会被阻塞直至条件满足;4. 操作具有超时特性。 96 | 97 | 另外,LinkedBlockingDeque实现了BlockingDueue接口而LinkedBlockingQueue实现的是BlockingQueue,这两个接口的主要区别如下图所示(来源于java文档): 98 | 99 | 100 | ![BlockingQueue和BlockingDeque的区别.png](http://upload-images.jianshu.io/upload_images/2615789-7316a2543b99caa2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/600) 101 | 102 | 从上图可以看出,两个接口的功能是可以等价使用的,比如BlockingQueue的add方法和BlockingDeque的addLast方法的功能是一样的。 103 | 104 | > 7.DelayQueue 105 | 106 | DelayQueue是一个存放实现Delayed接口的数据的无界阻塞队列,只有当数据对象的延时时间达到时才能插入到队列进行存储。如果当前所有的数据都还没有达到创建时所指定的延时期,则队列没有队头,并且线程通过poll等方法获取数据元素则返回null。所谓数据延时期满时,则是通过Delayed接口的`getDelay(TimeUnit.NANOSECONDS)`来进行判定,如果该方法返回的是小于等于0则说明该数据元素的延时期已满。 107 | 108 | 109 | -------------------------------------------------------------------------------- /20.并发容器之ArrayBlockingQueue和LinkedBlockingQueue实现原理详解/并发容器之ArrayBlockingQueue和LinkedBlockingQueue实现原理详解.md: -------------------------------------------------------------------------------- 1 | 2 | # 1. ArrayBlockingQueue简介 # 3 | 4 | 在多线程编程过程中,为了业务解耦和架构设计,经常会使用并发容器用于存储多线程间的共享数据,这样不仅可以保证线程安全,还可以简化各个线程操作。例如在“生产者-消费者”问题中,会使用阻塞队列(BlockingQueue)作为数据容器,关于BlockingQueue可以[看这篇文章](https://juejin.im/post/5aeebd02518825672f19c546)。为了加深对阻塞队列的理解,唯一的方式是对其实验原理进行理解,这篇文章就主要来看看ArrayBlockingQueue和LinkedBlockingQueue的实现原理。 5 | 6 | # 2. ArrayBlockingQueue实现原理 # 7 | 8 | 阻塞队列最核心的功能是,能够可阻塞式的插入和删除队列元素。当前队列为空时,会阻塞消费数据的线程,直至队列非空时,通知被阻塞的线程;当队列满时,会阻塞插入数据的线程,直至队列未满时,通知插入数据的线程(生产者线程)。那么,多线程中消息通知机制最常用的是lock的condition机制,关于condition可以[看这篇文章的详细介绍](https://juejin.im/post/5aeea5e951882506a36c67f0)。那么ArrayBlockingQueue的实现是不是也会采用Condition的通知机制呢?下面来看看。 9 | 10 | ## 2.1 ArrayBlockingQueue的主要属性 11 | 12 | ArrayBlockingQueue的主要属性如下: 13 | 14 | /** The queued items */ 15 | final Object[] items; 16 | 17 | /** items index for next take, poll, peek or remove */ 18 | int takeIndex; 19 | 20 | /** items index for next put, offer, or add */ 21 | int putIndex; 22 | 23 | /** Number of elements in the queue */ 24 | int count; 25 | 26 | /* 27 | * Concurrency control uses the classic two-condition algorithm 28 | * found in any textbook. 29 | */ 30 | 31 | /** Main lock guarding all access */ 32 | final ReentrantLock lock; 33 | 34 | /** Condition for waiting takes */ 35 | private final Condition notEmpty; 36 | 37 | /** Condition for waiting puts */ 38 | private final Condition notFull; 39 | 40 | 从源码中可以看出ArrayBlockingQueue内部是采用数组进行数据存储的(`属性items`),为了保证线程安全,采用的是`ReentrantLock lock`,为了保证可阻塞式的插入删除数据利用的是Condition,当获取数据的消费者线程被阻塞时会将该线程放置到notEmpty等待队列中,当插入数据的生产者线程被阻塞时,会将该线程放置到notFull等待队列中。而notEmpty和notFull等中要属性在构造方法中进行创建: 41 | 42 | public ArrayBlockingQueue(int capacity, boolean fair) { 43 | if (capacity <= 0) 44 | throw new IllegalArgumentException(); 45 | this.items = new Object[capacity]; 46 | lock = new ReentrantLock(fair); 47 | notEmpty = lock.newCondition(); 48 | notFull = lock.newCondition(); 49 | } 50 | 51 | 接下来,主要看看可阻塞式的put和take方法是怎样实现的。 52 | 53 | ## 2.2 put方法详解 54 | 55 | ` put(E e)`方法源码如下: 56 | 57 | public void put(E e) throws InterruptedException { 58 | checkNotNull(e); 59 | final ReentrantLock lock = this.lock; 60 | lock.lockInterruptibly(); 61 | try { 62 | //如果当前队列已满,将线程移入到notFull等待队列中 63 | while (count == items.length) 64 | notFull.await(); 65 | //满足插入数据的要求,直接进行入队操作 66 | enqueue(e); 67 | } finally { 68 | lock.unlock(); 69 | } 70 | } 71 | 72 | 73 | 该方法的逻辑很简单,当队列已满时(`count == items.length`)将线程移入到notFull等待队列中,如果当前满足插入数据的条件,就可以直接调用` enqueue(e)`插入数据元素。enqueue方法源码为: 74 | 75 | private void enqueue(E x) { 76 | // assert lock.getHoldCount() == 1; 77 | // assert items[putIndex] == null; 78 | final Object[] items = this.items; 79 | //插入数据 80 | items[putIndex] = x; 81 | if (++putIndex == items.length) 82 | putIndex = 0; 83 | count++; 84 | //通知消费者线程,当前队列中有数据可供消费 85 | notEmpty.signal(); 86 | } 87 | 88 | enqueue方法的逻辑同样也很简单,先完成插入数据,即往数组中添加数据(`items[putIndex] = x`),然后通知被阻塞的消费者线程,当前队列中有数据可供消费(`notEmpty.signal()`)。 89 | 90 | ## 2.3 take方法详解 91 | 92 | take方法源码如下: 93 | 94 | 95 | public E take() throws InterruptedException { 96 | final ReentrantLock lock = this.lock; 97 | lock.lockInterruptibly(); 98 | try { 99 | //如果队列为空,没有数据,将消费者线程移入等待队列中 100 | while (count == 0) 101 | notEmpty.await(); 102 | //获取数据 103 | return dequeue(); 104 | } finally { 105 | lock.unlock(); 106 | } 107 | } 108 | 109 | take方法也主要做了两步:1. 如果当前队列为空的话,则将获取数据的消费者线程移入到等待队列中;2. 若队列不为空则获取数据,即完成出队操作`dequeue`。dequeue方法源码为: 110 | 111 | private E dequeue() { 112 | // assert lock.getHoldCount() == 1; 113 | // assert items[takeIndex] != null; 114 | final Object[] items = this.items; 115 | @SuppressWarnings("unchecked") 116 | //获取数据 117 | E x = (E) items[takeIndex]; 118 | items[takeIndex] = null; 119 | if (++takeIndex == items.length) 120 | takeIndex = 0; 121 | count--; 122 | if (itrs != null) 123 | itrs.elementDequeued(); 124 | //通知被阻塞的生产者线程 125 | notFull.signal(); 126 | return x; 127 | } 128 | 129 | dequeue方法也主要做了两件事情:1. 获取队列中的数据,即获取数组中的数据元素(`(E) items[takeIndex]`);2. 通知notFull等待队列中的线程,使其由等待队列移入到同步队列中,使其能够有机会获得lock,并执行完成功退出。 130 | 131 | 从以上分析,可以看出put和take方法主要是通过condition的通知机制来完成可阻塞式的插入数据和获取数据。在理解ArrayBlockingQueue后再去理解LinkedBlockingQueue就很容易了。 132 | 133 | 134 | # 3. LinkedBlockingQueue实现原理 # 135 | LinkedBlockingQueue是用链表实现的有界阻塞队列,当构造对象时为指定队列大小时,队列默认大小为`Integer.MAX_VALUE`。从它的构造方法可以看出: 136 | 137 | public LinkedBlockingQueue() { 138 | this(Integer.MAX_VALUE); 139 | } 140 | 141 | 142 | # 3.1 LinkedBlockingQueue的主要属性 # 143 | 144 | 145 | LinkedBlockingQueue的主要属性有: 146 | 147 | /** Current number of elements */ 148 | private final AtomicInteger count = new AtomicInteger(); 149 | 150 | /** 151 | * Head of linked list. 152 | * Invariant: head.item == null 153 | */ 154 | transient Node head; 155 | 156 | /** 157 | * Tail of linked list. 158 | * Invariant: last.next == null 159 | */ 160 | private transient Node last; 161 | 162 | /** Lock held by take, poll, etc */ 163 | private final ReentrantLock takeLock = new ReentrantLock(); 164 | 165 | /** Wait queue for waiting takes */ 166 | private final Condition notEmpty = takeLock.newCondition(); 167 | 168 | /** Lock held by put, offer, etc */ 169 | private final ReentrantLock putLock = new ReentrantLock(); 170 | 171 | /** Wait queue for waiting puts */ 172 | private final Condition notFull = putLock.newCondition(); 173 | 174 | 可以看出与ArrayBlockingQueue主要的区别是,LinkedBlockingQueue在插入数据和删除数据时分别是由两个不同的lock(`takeLock`和`putLock`)来控制线程安全的,因此,也由这两个lock生成了两个对应的condition(`notEmpty`和`notFull`)来实现可阻塞的插入和删除数据。并且,采用了链表的数据结构来实现队列,Node结点的定义为: 175 | 176 | static class Node { 177 | E item; 178 | 179 | /** 180 | * One of: 181 | * - the real successor Node 182 | * - this Node, meaning the successor is head.next 183 | * - null, meaning there is no successor (this is the last node) 184 | */ 185 | Node next; 186 | 187 | Node(E x) { item = x; } 188 | } 189 | 190 | 接下来,我们也同样来看看put方法和take方法的实现。 191 | 192 | ## 3.2 put方法详解 ## 193 | 194 | put方法源码为: 195 | 196 | public void put(E e) throws InterruptedException { 197 | if (e == null) throw new NullPointerException(); 198 | // Note: convention in all put/take/etc is to preset local var 199 | // holding count negative to indicate failure unless set. 200 | int c = -1; 201 | Node node = new Node(e); 202 | final ReentrantLock putLock = this.putLock; 203 | final AtomicInteger count = this.count; 204 | putLock.lockInterruptibly(); 205 | try { 206 | /* 207 | * Note that count is used in wait guard even though it is 208 | * not protected by lock. This works because count can 209 | * only decrease at this point (all other puts are shut 210 | * out by lock), and we (or some other waiting put) are 211 | * signalled if it ever changes from capacity. Similarly 212 | * for all other uses of count in other wait guards. 213 | */ 214 | //如果队列已满,则阻塞当前线程,将其移入等待队列 215 | while (count.get() == capacity) { 216 | notFull.await(); 217 | } 218 | //入队操作,插入数据 219 | enqueue(node); 220 | c = count.getAndIncrement(); 221 | //若队列满足插入数据的条件,则通知被阻塞的生产者线程 222 | if (c + 1 < capacity) 223 | notFull.signal(); 224 | } finally { 225 | putLock.unlock(); 226 | } 227 | if (c == 0) 228 | signalNotEmpty(); 229 | } 230 | 231 | put方法的逻辑也同样很容易理解,可见注释。基本上和ArrayBlockingQueue的put方法一样。take方法的源码如下: 232 | 233 | public E take() throws InterruptedException { 234 | E x; 235 | int c = -1; 236 | final AtomicInteger count = this.count; 237 | final ReentrantLock takeLock = this.takeLock; 238 | takeLock.lockInterruptibly(); 239 | try { 240 | //当前队列为空,则阻塞当前线程,将其移入到等待队列中,直至满足条件 241 | while (count.get() == 0) { 242 | notEmpty.await(); 243 | } 244 | //移除队头元素,获取数据 245 | x = dequeue(); 246 | c = count.getAndDecrement(); 247 | //如果当前满足移除元素的条件,则通知被阻塞的消费者线程 248 | if (c > 1) 249 | notEmpty.signal(); 250 | } finally { 251 | takeLock.unlock(); 252 | } 253 | if (c == capacity) 254 | signalNotFull(); 255 | return x; 256 | } 257 | 258 | take方法的主要逻辑请见于注释,也很容易理解。 259 | 260 | # 4. ArrayBlockingQueue与LinkedBlockingQueue的比较 # 261 | 262 | **相同点**:ArrayBlockingQueue和LinkedBlockingQueue都是通过condition通知机制来实现可阻塞式插入和删除元素,并满足线程安全的特性; 263 | 264 | **不同点**:1. ArrayBlockingQueue底层是采用的数组进行实现,而LinkedBlockingQueue则是采用链表数据结构; 265 | 2. ArrayBlockingQueue插入和删除数据,只采用了一个lock,而LinkedBlockingQueue则是在插入和删除分别采用了`putLock`和`takeLock`,这样可以降低线程由于线程无法获取到lock而进入WAITING状态的可能性,从而提高了线程并发执行的效率。 266 | -------------------------------------------------------------------------------- /21.线程池ThreadPoolExecutor实现原理/execute执行过程示意图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/21.线程池ThreadPoolExecutor实现原理/execute执行过程示意图.jpg -------------------------------------------------------------------------------- /21.线程池ThreadPoolExecutor实现原理/线程池ThreadPoolExecutor实现原理.md: -------------------------------------------------------------------------------- 1 | 2 | # 1. 为什么要使用线程池 # 3 | 在实际使用中,线程是很占用系统资源的,如果对线程管理不善很容易导致系统问题。因此,在大多数并发框架中都会使用**线程池**来管理线程,使用线程池管理线程主要有如下好处: 4 | 5 | 1. **降低资源消耗**。通过复用已存在的线程和降低线程关闭的次数来尽可能降低系统性能损耗; 6 | 2. **提升系统响应速度**。通过复用线程,省去创建线程的过程,因此整体上提升了系统的响应速度; 7 | 3. **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,因此,需要使用线程池来管理线程。 8 | 9 | # 2. 线程池的工作原理 # 10 | 11 | 当一个并发任务提交给线程池,线程池分配线程去执行任务的过程如下图所示: 12 | 13 | 14 | ![线程池执行流程图.jpg](https://upload-images.jianshu.io/upload_images/2615789-2d3eb90c8e2cf51f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 15 | 16 | 17 | 18 | 从图可以看出,线程池执行所提交的任务过程主要有这样几个阶段: 19 | 20 | 1. 先判断线程池中**核心线程池**所有的线程是否都在执行任务。如果不是,则新创建一个线程执行刚提交的任务,否则,核心线程池中所有的线程都在执行任务,则进入第2步; 21 | 2. 判断当前**阻塞队列**是否已满,如果未满,则将提交的任务放置在阻塞队列中;否则,则进入第3步; 22 | 3. 判断**线程池中所有的线程**是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则,则交给饱和策略进行处理 23 | 24 | # 3. 线程池的创建 # 25 | 26 | 创建线程池主要是**ThreadPoolExecutor**类来完成,ThreadPoolExecutor的有许多重载的构造方法,通过参数最多的构造方法来理解创建线程池有哪些需要配置的参数。ThreadPoolExecutor的构造方法为: 27 | 28 | ThreadPoolExecutor(int corePoolSize, 29 | int maximumPoolSize, 30 | long keepAliveTime, 31 | TimeUnit unit, 32 | BlockingQueue workQueue, 33 | ThreadFactory threadFactory, 34 | RejectedExecutionHandler handler) 35 | 36 | 下面对参数进行说明: 37 | 38 | 1. corePoolSize:表示核心线程池的大小。当提交一个任务时,如果当前核心线程池的线程个数没有达到corePoolSize,则会创建新的线程来执行所提交的任务,**即使当前核心线程池有空闲的线程**。如果当前核心线程池的线程个数已经达到了corePoolSize,则不再重新创建线程。如果调用了`prestartCoreThread()`或者 `prestartAllCoreThreads()`,线程池创建的时候所有的核心线程都会被创建并且启动。 39 | 2. maximumPoolSize:表示线程池能创建线程的最大个数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过maximumPoolSize的话,就会创建新的线程来执行任务。 40 | 3. keepAliveTime:空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。 41 | 4. unit:时间单位。为keepAliveTime指定时间单位。 42 | 5. workQueue:阻塞队列。用于保存任务的阻塞队列,关于阻塞队列[可以看这篇文章](https://juejin.im/post/5aeebd02518825672f19c546)。可以使用**ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue, PriorityBlockingQueue**。 43 | 6. threadFactory:创建线程的工程类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。 44 | 7. handler:饱和策略。当线程池的阻塞队列已满和指定的线程都已经开启,说明当前线程池已经处于饱和状态了,那么就需要采用一种策略来处理这种情况。采用的策略有这几种: 45 | 1. AbortPolicy: 直接拒绝所提交的任务,并抛出**RejectedExecutionException**异常; 46 | 2. CallerRunsPolicy:只用调用者所在的线程来执行任务; 47 | 3. DiscardPolicy:不处理直接丢弃掉任务; 48 | 4. DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务 49 | 50 | > 线程池执行逻辑 51 | 52 | 通过ThreadPoolExecutor创建线程池后,提交任务后执行过程是怎样的,下面来通过源码来看一看。execute方法源码如下: 53 | 54 | 55 | public void execute(Runnable command) { 56 | if (command == null) 57 | throw new NullPointerException(); 58 | /* 59 | * Proceed in 3 steps: 60 | * 61 | * 1. If fewer than corePoolSize threads are running, try to 62 | * start a new thread with the given command as its first 63 | * task. The call to addWorker atomically checks runState and 64 | * workerCount, and so prevents false alarms that would add 65 | * threads when it shouldn't, by returning false. 66 | * 67 | * 2. If a task can be successfully queued, then we still need 68 | * to double-check whether we should have added a thread 69 | * (because existing ones died since last checking) or that 70 | * the pool shut down since entry into this method. So we 71 | * recheck state and if necessary roll back the enqueuing if 72 | * stopped, or start a new thread if there are none. 73 | * 74 | * 3. If we cannot queue task, then we try to add a new 75 | * thread. If it fails, we know we are shut down or saturated 76 | * and so reject the task. 77 | */ 78 | int c = ctl.get(); 79 | //如果线程池的线程个数少于corePoolSize则创建新线程执行当前任务 80 | if (workerCountOf(c) < corePoolSize) { 81 | if (addWorker(command, true)) 82 | return; 83 | c = ctl.get(); 84 | } 85 | //如果线程个数大于corePoolSize或者创建线程失败,则将任务存放在阻塞队列workQueue中 86 | if (isRunning(c) && workQueue.offer(command)) { 87 | int recheck = ctl.get(); 88 | if (! isRunning(recheck) && remove(command)) 89 | reject(command); 90 | else if (workerCountOf(recheck) == 0) 91 | addWorker(null, false); 92 | } 93 | //如果当前任务无法放进阻塞队列中,则创建新的线程来执行任务 94 | else if (!addWorker(command, false)) 95 | reject(command); 96 | } 97 | 98 | ThreadPoolExecutor的execute方法执行逻辑请见注释。下图为ThreadPoolExecutor的execute方法的执行示意图: 99 | 100 | ![execute执行过程示意图.jpg](https://upload-images.jianshu.io/upload_images/2615789-1c5c07e48180130a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 101 | 102 | 103 | execute方法执行逻辑有这样几种情况: 104 | 105 | 1. 如果当前运行的线程少于corePoolSize,则会创建新的线程来执行新的任务; 106 | 2. 如果运行的线程个数等于或者大于corePoolSize,则会将提交的任务存放到阻塞队列workQueue中; 107 | 3. 如果当前workQueue队列已满的话,则会创建新的线程来执行任务; 108 | 4. 如果线程个数已经超过了maximumPoolSize,则会使用饱和策略RejectedExecutionHandler来进行处理。 109 | 110 | 需要注意的是,线程池的设计思想就是使用了**核心线程池corePoolSize,阻塞队列workQueue和线程池maximumPoolSize**,这样的缓存策略来处理任务,实际上这样的设计思想在需要框架中都会使用。 111 | 112 | # 4. 线程池的关闭 # 113 | 114 | 关闭线程池,可以通过`shutdown`和`shutdownNow`这两个方法。它们的原理都是遍历线程池中所有的线程,然后依次中断线程。`shutdown`和`shutdownNow`还是有不一样的地方: 115 | 116 | 1. `shutdownNow`首先将线程池的状态设置为**STOP**,然后尝试**停止所有的正在执行和未执行任务**的线程,并返回等待执行任务的列表; 117 | 2. `shutdown`只是将线程池的状态设置为**SHUTDOWN**状态,然后中断所有没有正在执行任务的线程 118 | 119 | 可以看出shutdown方法会将正在执行的任务继续执行完,而shutdownNow会直接中断正在执行的任务。调用了这两个方法的任意一个,`isShutdown`方法都会返回true,当所有的线程都关闭成功,才表示线程池成功关闭,这时调用`isTerminated`方法才会返回true。 120 | 121 | # 5. 如何合理配置线程池参数? # 122 | 123 | 要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析: 124 | 125 | 1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。 126 | 2. 任务的优先级:高,中和低。 127 | 3. 任务的执行时间:长,中和短。 128 | 4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。 129 | 130 | 任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置**Ncpu+1**个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如**2xNcpu**。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过`Runtime.getRuntime().availableProcessors()`方法获得当前设备的CPU个数。 131 | 132 | 优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。 133 | 134 | 执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。 135 | 136 | 依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。 137 | 138 | 并且,阻塞队列**最好是使用有界队列**,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。 139 | 140 | 141 | > 参考文献 142 | 143 | 《Java并发编程的艺术》 144 | [ThreadPoolExecutor源码分析,很详细](http://www.ideabuffer.cn/2017/04/04/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9AThreadPoolExecutor/#addWorker%E6%96%B9%E6%B3%95) -------------------------------------------------------------------------------- /21.线程池ThreadPoolExecutor实现原理/线程池执行流程图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/21.线程池ThreadPoolExecutor实现原理/线程池执行流程图.jpg -------------------------------------------------------------------------------- /22.线程池之ScheduledThreadPoolExecutor/ScheduledThreadPoolExecutor类的UML图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/22.线程池之ScheduledThreadPoolExecutor/ScheduledThreadPoolExecutor类的UML图.png -------------------------------------------------------------------------------- /22.线程池之ScheduledThreadPoolExecutor/线程池之ScheduledThreadPoolExecutor.md: -------------------------------------------------------------------------------- 1 | 2 | # 1. ScheduledThreadPoolExecutor简介 # 3 | ScheduledThreadPoolExecutor可以用来在给定延时后执行异步任务或者周期性执行任务,相对于任务调度的Timer来说,其功能更加强大,Timer只能使用一个后台线程执行任务,而ScheduledThreadPoolExecutor则可以通过构造函数来指定后台线程的个数。ScheduledThreadPoolExecutor类的UML图如下: 4 | 5 | 6 | ![ScheduledThreadPoolExecutor类的UML图.png](https://upload-images.jianshu.io/upload_images/2615789-adf418781bb01bf1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 7 | 8 | 9 | 10 | 1. 从UML图可以看出,ScheduledThreadPoolExecutor继承了`ThreadPoolExecutor`,也就是说ScheduledThreadPoolExecutor拥有execute()和submit()提交异步任务的基础功能,关于ThreadPoolExecutor[可以看这篇文章](https://juejin.im/post/5aeec0106fb9a07ab379574f)。但是,ScheduledThreadPoolExecutor类实现了`ScheduledExecutorService`,该接口定义了ScheduledThreadPoolExecutor能够延时执行任务和周期执行任务的功能; 11 | 2. ScheduledThreadPoolExecutor也两个重要的内部类:**DelayedWorkQueue**和**ScheduledFutureTask**。可以看出DelayedWorkQueue实现了BlockingQueue接口,也就是一个阻塞队列,ScheduledFutureTask则是继承了FutureTask类,也表示该类用于返回异步任务的结果。这两个关键类,下面会具体详细来看。 12 | 13 | 14 | ## 1.1 构造方法 ## 15 | 16 | ScheduledThreadPoolExecutor有如下几个构造方法: 17 | 18 | public ScheduledThreadPoolExecutor(int corePoolSize) { 19 | super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, 20 | new DelayedWorkQueue()); 21 | }; 22 | 23 | public ScheduledThreadPoolExecutor(int corePoolSize, 24 | ThreadFactory threadFactory) { 25 | super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, 26 | new DelayedWorkQueue(), threadFactory); 27 | }; 28 | public ScheduledThreadPoolExecutor(int corePoolSize, 29 | RejectedExecutionHandler handler) { 30 | super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, 31 | new DelayedWorkQueue(), handler); 32 | }; 33 | 34 | public ScheduledThreadPoolExecutor(int corePoolSize, 35 | ThreadFactory threadFactory, 36 | RejectedExecutionHandler handler) { 37 | super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, 38 | new DelayedWorkQueue(), threadFactory, handler); 39 | } 40 | 41 | 可以看出由于ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,它的构造方法实际上是调用了ThreadPoolExecutor,对ThreadPoolExecutor的介绍可以[可以看这篇文章](https://juejin.im/post/5aeec0106fb9a07ab379574f),理解ThreadPoolExecutor构造方法的几个参数的意义后,理解这就很容易了。可以看出,ScheduledThreadPoolExecutor的核心线程池的线程个数为指定的corePoolSize,当核心线程池的线程个数达到corePoolSize后,就会将任务提交给有界阻塞队列DelayedWorkQueue,对DelayedWorkQueue在下面进行详细介绍,线程池允许最大的线程个数为Integer.MAX_VALUE,也就是说理论上这是一个大小无界的线程池。 42 | 43 | ## 1.2 特有方法 44 | 45 | ScheduledThreadPoolExecutor实现了`ScheduledExecutorService`接口,该接口定义了**可延时执行异步任务和可周期执行异步任务的特有功能**,相应的方法分别为: 46 | 47 | //达到给定的延时时间后,执行任务。这里传入的是实现Runnable接口的任务, 48 | //因此通过ScheduledFuture.get()获取结果为null 49 | public ScheduledFuture schedule(Runnable command, 50 | long delay, TimeUnit unit); 51 | //达到给定的延时时间后,执行任务。这里传入的是实现Callable接口的任务, 52 | //因此,返回的是任务的最终计算结果 53 | public ScheduledFuture schedule(Callable callable, 54 | long delay, TimeUnit unit); 55 | 56 | //是以上一个任务开始的时间计时,period时间过去后, 57 | //检测上一个任务是否执行完毕,如果上一个任务执行完毕, 58 | //则当前任务立即执行,如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后立即执行 59 | public ScheduledFuture scheduleAtFixedRate(Runnable command, 60 | long initialDelay, 61 | long period, 62 | TimeUnit unit); 63 | //当达到延时时间initialDelay后,任务开始执行。上一个任务执行结束后到下一次 64 | //任务执行,中间延时时间间隔为delay。以这种方式,周期性执行任务。 65 | public ScheduledFuture scheduleWithFixedDelay(Runnable command, 66 | long initialDelay, 67 | long delay, 68 | TimeUnit unit); 69 | 70 | 71 | # 2. 可周期性执行的任务---ScheduledFutureTask # 72 | 73 | ScheduledThreadPoolExecutor最大的特色是能够周期性执行异步任务,当调用`schedule,scheduleAtFixedRate和scheduleWithFixedDelay方法`时,实际上是将提交的任务转换成的ScheduledFutureTask类,从源码就可以看出。以schedule方法为例: 74 | 75 | public ScheduledFuture schedule(Runnable command, 76 | long delay, 77 | TimeUnit unit) { 78 | if (command == null || unit == null) 79 | throw new NullPointerException(); 80 | RunnableScheduledFuture t = decorateTask(command, 81 | new ScheduledFutureTask(command, null, 82 | triggerTime(delay, unit))); 83 | delayedExecute(t); 84 | return t; 85 | } 86 | 87 | 可以看出,通过`decorateTask`会将传入的Runnable转换成`ScheduledFutureTask`类。线程池最大作用是将任务和线程进行解耦,线程主要是任务的执行者,而任务也就是现在所说的ScheduledFutureTask。紧接着,会想到任何线程执行任务,总会调用`run()`方法。为了保证ScheduledThreadPoolExecutor能够延时执行任务以及能够周期性执行任务,ScheduledFutureTask重写了run方法: 88 | 89 | public void run() { 90 | boolean periodic = isPeriodic(); 91 | if (!canRunInCurrentRunState(periodic)) 92 | cancel(false); 93 | else if (!periodic) 94 | //如果不是周期性执行任务,则直接调用run方法 95 | ScheduledFutureTask.super.run(); 96 | //如果是周期性执行任务的话,需要重设下一次执行任务的时间 97 | else if (ScheduledFutureTask.super.runAndReset()) { 98 | setNextRunTime(); 99 | reExecutePeriodic(outerTask); 100 | } 101 | } 102 | 103 | 从源码可以很明显的看出,在重写的run方法中会先`if (!periodic)`判断当前任务是否是周期性任务,如果不是的话就直接调用`run()方法`;否则的话执行`setNextRunTime()`方法重设下一次任务执行的时间,并通过`reExecutePeriodic(outerTask)`方法将下一次待执行的任务放置到`DelayedWorkQueue`中。 104 | 105 | 因此,可以得出结论:**`ScheduledFutureTask`最主要的功能是根据当前任务是否具有周期性,对异步任务进行进一步封装。如果不是周期性任务(调用schedule方法)则直接通过`run()`执行,若是周期性任务,则需要在每一次执行完后,重设下一次执行的时间,然后将下一次任务继续放入到阻塞队列中。** 106 | 107 | # 3. DelayedWorkQueue # 108 | 109 | 在ScheduledThreadPoolExecutor中还有另外的一个重要的类就是DelayedWorkQueue。为了实现其ScheduledThreadPoolExecutor能够延时执行异步任务以及能够周期执行任务,DelayedWorkQueue进行相应的封装。DelayedWorkQueue是一个基于堆的数据结构,类似于DelayQueue和PriorityQueue。在执行定时任务的时候,每个任务的执行时间都不同,所以DelayedWorkQueue的工作就是按照执行时间的升序来排列,执行时间距离当前时间越近的任务在队列的前面。 110 | 111 | > 为什么要使用DelayedWorkQueue呢? 112 | 113 | 定时任务执行时需要取出最近要执行的任务,所以任务在队列中每次出队时一定要是当前队列中执行时间最靠前的,所以自然要使用优先级队列。 114 | 115 | DelayedWorkQueue是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的,由于它是基于堆结构的队列,堆结构在执行插入和删除操作时的最坏时间复杂度是 O(logN)。 116 | 117 | > DelayedWorkQueue的数据结构 118 | 119 | //初始大小 120 | private static final int INITIAL_CAPACITY = 16; 121 | //DelayedWorkQueue是由一个大小为16的数组组成,数组元素为实现RunnableScheduleFuture接口的类 122 | //实际上为ScheduledFutureTask 123 | private RunnableScheduledFuture[] queue = 124 | new RunnableScheduledFuture[INITIAL_CAPACITY]; 125 | private final ReentrantLock lock = new ReentrantLock(); 126 | private int size = 0; 127 | 128 | 可以看出DelayedWorkQueue底层是采用数组构成的,关于[DelayedWorkQueue可以看这篇博主的文章](https://juejin.im/post/5aeebd02518825672f19c546),很详细。 129 | 130 | 关于DelayedWorkQueue我们可以得出这样的结论:**DelayedWorkQueue是基于堆的数据结构,按照时间顺序将每个任务进行排序,将待执行时间越近的任务放在在队列的队头位置,以便于最先进行执行**。 131 | 132 | # 4.ScheduledThreadPoolExecutor执行过程 133 | 134 | 现在我们对ScheduledThreadPoolExecutor的两个内部类ScheduledFutueTask和DelayedWorkQueue进行了了解,实际上这也是线程池工作流程中最重要的两个关键因素:**任务以及阻塞队列**。现在我们来看下ScheduledThreadPoolExecutor提交一个任务后,整体的执行过程。以ScheduledThreadPoolExecutor的schedule方法为例,具体源码为: 135 | 136 | public ScheduledFuture schedule(Runnable command, 137 | long delay, 138 | TimeUnit unit) { 139 | if (command == null || unit == null) 140 | throw new NullPointerException(); 141 | //将提交的任务转换成ScheduledFutureTask 142 | RunnableScheduledFuture t = decorateTask(command, 143 | new ScheduledFutureTask(command, null, 144 | triggerTime(delay, unit))); 145 | //延时执行任务ScheduledFutureTask 146 | delayedExecute(t); 147 | return t; 148 | } 149 | 150 | 方法很容易理解,为了满足ScheduledThreadPoolExecutor能够延时执行任务和能周期执行任务的特性,会先将实现Runnable接口的类转换成ScheduledFutureTask。然后会调用`delayedExecute`方法进行执行任务,这个方法也是关键方法,来看下源码: 151 | 152 | 153 | private void delayedExecute(RunnableScheduledFuture task) { 154 | if (isShutdown()) 155 | //如果当前线程池已经关闭,则拒绝任务 156 | reject(task); 157 | else { 158 | //将任务放入阻塞队列中 159 | super.getQueue().add(task); 160 | if (isShutdown() && 161 | !canRunInCurrentRunState(task.isPeriodic()) && 162 | remove(task)) 163 | task.cancel(false); 164 | else 165 | //保证至少有一个线程启动,即使corePoolSize=0 166 | ensurePrestart(); 167 | } 168 | } 169 | 170 | `delayedExecute`方法的主要逻辑请看注释,可以看出该方法的重要逻辑会是在`ensurePrestart()`方法中,它的源码为: 171 | 172 | void ensurePrestart() { 173 | int wc = workerCountOf(ctl.get()); 174 | if (wc < corePoolSize) 175 | addWorker(null, true); 176 | else if (wc == 0) 177 | addWorker(null, false); 178 | } 179 | 180 | 可以看出该方法逻辑很简单,关键在于它所调用的`addWorker方法`,该方法主要功能:**新建`Worker类`,当执行任务时,就会调用被`Worker所重写的run方法`,进而会继续执行`runWorker`方法。在`runWorker`方法中会调用`getTask`方法从阻塞队列中不断的去获取任务进行执行,直到从阻塞队列中获取的任务为null的话,线程结束终止**。addWorker方法是ThreadPoolExecutor类中的方法,[对ThreadPoolExecutor的源码分析可以看这篇文章,很详细。](http://www.ideabuffer.cn/2017/04/04/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3Java%E7%BA%BF%E7%A8%8B%E6%B1%A0%EF%BC%9AThreadPoolExecutor/#addWorker%E6%96%B9%E6%B3%95) 181 | 182 | # 5.总结 # 183 | 184 | 1. ScheduledThreadPoolExecutor继承了ThreadPoolExecutor类,因此,整体上功能一致,线程池主要负责创建线程(Worker类),线程从阻塞队列中不断获取新的异步任务,直到阻塞队列中已经没有了异步任务为止。但是相较于ThreadPoolExecutor来说,ScheduledThreadPoolExecutor具有延时执行任务和可周期性执行任务的特性,ScheduledThreadPoolExecutor重新设计了任务类`ScheduleFutureTask`,ScheduleFutureTask重写了`run`方法使其具有可延时执行和可周期性执行任务的特性。另外,阻塞队列`DelayedWorkQueue`是可根据优先级排序的队列,采用了堆的底层数据结构,使得与当前时间相比,待执行时间越靠近的任务放置队头,以便线程能够获取到任务进行执行; 185 | 2. 线程池无论是ThreadPoolExecutor还是ScheduledThreadPoolExecutor,在设计时的三个关键要素是:任务,执行者以及任务结果。它们的设计思想也是完全将这三个关键要素进行了解耦。 186 | 187 | **执行者** 188 | 189 | 任务的执行机制,完全交由`Worker类`,也就是进一步了封装了Thread。向线程池提交任务,无论为ThreadPoolExecutor的execute方法和submit方法,还是ScheduledThreadPoolExecutor的schedule方法,都是先将任务移入到阻塞队列中,然后通过addWork方法新建了Work类,并通过runWorker方法启动线程,并不断的从阻塞对列中获取异步任务执行交给Worker执行,直至阻塞队列中无法取到任务为止。 190 | 191 | **任务** 192 | 193 | 在ThreadPoolExecutor和ScheduledThreadPoolExecutor中任务是指实现了Runnable接口和Callable接口的实现类。ThreadPoolExecutor中会将任务转换成`FutureTask`类,而在ScheduledThreadPoolExecutor中为了实现可延时执行任务和周期性执行任务的特性,任务会被转换成`ScheduledFutureTask`类,该类继承了FutureTask,并重写了run方法。 194 | 195 | **任务结果** 196 | 197 | 在ThreadPoolExecutor中提交任务后,获取任务结果可以通过Future接口的类,在ThreadPoolExecutor中实际上为FutureTask类,而在ScheduledThreadPoolExecutor中则是`ScheduledFutureTask`类 198 | 199 | 200 | -------------------------------------------------------------------------------- /23.FutureTask基本操作总结/FutureTask基本操作总结.md: -------------------------------------------------------------------------------- 1 | 2 | # 1.FutureTask简介 # 3 | 4 | 在Executors框架体系中,FutureTask用来表示可获取结果的异步任务。FutureTask实现了Future接口,FutureTask提供了启动和取消异步任务,查询异步任务是否计算结束以及获取最终的异步任务的结果的一些常用的方法。通过`get()`方法来获取异步任务的结果,但是会阻塞当前线程直至异步任务执行结束。一旦任务执行结束,任务不能重新启动或取消,除非调用`runAndReset()`方法。在FutureTask的源码中为其定义了这些状态: 5 | 6 | private static final int NEW = 0; 7 | private static final int COMPLETING = 1; 8 | private static final int NORMAL = 2; 9 | private static final int EXCEPTIONAL = 3; 10 | private static final int CANCELLED = 4; 11 | private static final int INTERRUPTING = 5; 12 | private static final int INTERRUPTED = 6; 13 | 14 | 另外,在《java并发编程的艺术》一书,作者根据FutureTask.run()方法的执行的时机,FutureTask分为了3种状态: 15 | 16 | 17 | 18 | 1. **未启动**。FutureTask.run()方法还没有被执行之前,FutureTask处于未启动状态。当创建一个FutureTask,还没有执行FutureTask.run()方法之前,FutureTask处于未启动状态。 19 | 2. **已启动**。FutureTask.run()方法被执行的过程中,FutureTask处于已启动状态。 20 | 3. **已完成**。FutureTask.run()方法执行结束,或者调用FutureTask.cancel(...)方法取消任务,或者在执行任务期间抛出异常,这些情况都称之为FutureTask的已完成状态。 21 | 22 | 下图总结了FutureTask的状态变化的过程: 23 | 24 | ![FutureTask状态迁移图.jpg](https://upload-images.jianshu.io/upload_images/2615789-1be841dedd7c1df8.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 25 | 26 | 27 | 由于FutureTask具有这三种状态,因此执行FutureTask的get方法和cancel方法,当前处于不同的状态对应的结果也是大不相同。这里对get方法和cancel方法做个总结: 28 | 29 | > get方法 30 | 31 | 当FutureTask处于未启动或已启动状态时,执行FutureTask.get()方法将导致调用线程阻塞。如果FutureTask处于已完成状态,调用FutureTask.get()方法将导致调用线程立即返回结果或者抛出异常 32 | 33 | > cancel方法 34 | 35 | 当FutureTask处于**未启动状态**时,执行FutureTask.cancel()方法将此任务永远不会执行; 36 | 37 | 当FutureTask处于**已启动状态**时,执行FutureTask.cancel(true)方法将以中断线程的方式来阻止任务继续进行,如果执行FutureTask.cancel(false)将不会对正在执行任务的线程有任何影响; 38 | 39 | 当**FutureTask**处于已完成状态时,执行FutureTask.cancel(...)方法将返回false。 40 | 41 | 对Future的get()方法和cancel()方法用下图进行总结 42 | 43 | ![FutureTask的get和cancel的执行示意图.jpg](https://upload-images.jianshu.io/upload_images/2615789-c4b523533f024362.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 44 | 45 | 46 | # 2. FutureTask的基本使用 # 47 | 48 | FutureTask除了实现Future接口外,还实现了Runnable接口。因此,FutureTask可以交给Executor执行,也可以由调用的线程直接执行(FutureTask.run())。另外,FutureTask的获取也可以通过ExecutorService.submit()方法返回一个FutureTask对象,然后在通过FutureTask.get()或者FutureTask.cancel方法。 49 | 50 | **应用场景:**当一个线程需要等待另一个线程把某个任务执行完后它才能继续执行,此时可以使用FutureTask。假设有多个线程执行若干任务,每个任务最多只能被执行一次。当多个线程试图执行同一个任务时,只允许一个线程执行任务,其他线程需要等待这个任务执行完后才能继续执行。 51 | 52 | > 参考文献 53 | 54 | 《java并发编程的艺术》 -------------------------------------------------------------------------------- /23.FutureTask基本操作总结/FutureTask状态迁移图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/23.FutureTask基本操作总结/FutureTask状态迁移图.jpg -------------------------------------------------------------------------------- /23.FutureTask基本操作总结/FutureTask的get和cancel的执行示意图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/23.FutureTask基本操作总结/FutureTask的get和cancel的执行示意图.jpg -------------------------------------------------------------------------------- /24.Java中atomic包中的原子操作类总结/Java中atomic包中的原子操作类总结.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 1. 原子操作类介绍 # 4 | 5 | 在并发编程中很容易出现并发安全的问题,有一个很简单的例子就是多线程更新变量i=1,比如多个线程执行i++操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchronized进行控制来达到线程安全的目的([关于synchronized可以看这篇文章](https://juejin.im/post/5ae6dc04f265da0ba351d3ff))。但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。实际上,在J.U.C下的atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在java中则是使用CAS操作具体实现。 6 | 7 | # 2. 预备知识--CAS操作 # 8 | 9 | 能够弄懂atomic包下这些原子操作类的实现原理,就要先明白什么是CAS操作。 10 | 11 | > 什么是CAS? 12 | 13 | 使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。 14 | 15 | > CAS的操作过程 16 | 17 | CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程 18 | 19 | CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。 20 | 21 | **Synchronized VS CAS** 22 | 23 | 元老级的Synchronized(未优化前)最主要的问题是:在存在线程竞争的情况下会出现线程阻塞和唤醒锁带来的性能问题,因为这是一种互斥同步(阻塞同步)。而CAS并不是武断的间线程挂起,当CAS操作失败后会进行一定的尝试,而非进行耗时的挂起唤醒的操作,因此也叫做非阻塞同步。这是两者主要的区别。 24 | 25 | 26 | > CAS的问题 27 | 28 | 1. ABA问题 29 | 因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3C。 30 | 31 | 2. 自旋时间过长 32 | 33 | 使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。 34 | 35 | 36 | # 3. 原子更新基本类型 37 | 38 | atomic包提高原子更新基本类型的工具类,主要有这些: 39 | 40 | 1. AtomicBoolean:以原子更新的方式更新boolean; 41 | 2. AtomicInteger:以原子更新的方式更新Integer; 42 | 3. AtomicLong:以原子更新的方式更新Long; 43 | 44 | 这几个类的用法基本一致,这里以AtomicInteger为例总结常用的方法 45 | 46 | 1. addAndGet(int delta) :以原子方式将输入的数值与实例中原本的值相加,并返回最后的结果; 47 | 2. incrementAndGet() :以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果; 48 | 3. getAndSet(int newValue):将实例中的值更新为新值,并返回旧值; 49 | 4. getAndIncrement():以原子的方式将实例中的原值加1,返回的是自增前的旧值; 50 | 51 | 还有一些方法,可以查看API,不再赘述。为了能够弄懂AtomicInteger的实现原理,以getAndIncrement方法为例,来看下源码: 52 | 53 | public final int getAndIncrement() { 54 | return unsafe.getAndAddInt(this, valueOffset, 1); 55 | } 56 | 57 | 可以看出,该方法实际上是调用了unsafe实例的getAndAddInt方法,unsafe实例的获取时通过UnSafe类的静态方法getUnsafe获取: 58 | 59 | private static final Unsafe unsafe = Unsafe.getUnsafe(); 60 | 61 | Unsafe类在sun.misc包下,Unsafer类提供了一些底层操作,atomic包下的原子操作类的也主要是通过Unsafe类提供的compareAndSwapInt,compareAndSwapLong等一系列提供CAS操作的方法来进行实现。下面用一个简单的例子来说明AtomicInteger的用法: 62 | 63 | public class AtomicDemo { 64 | private static AtomicInteger atomicInteger = new AtomicInteger(1); 65 | 66 | public static void main(String[] args) { 67 | System.out.println(atomicInteger.getAndIncrement()); 68 | System.out.println(atomicInteger.get()); 69 | } 70 | } 71 | 输出结果: 72 | 1 73 | 2 74 | 75 | 例子很简单,就是新建了一个atomicInteger对象,而atomicInteger的构造方法也就是传入一个基本类型数据即可,对其进行了封装。对基本变量的操作比如自增,自减,相加,更新等操作,atomicInteger也提供了相应的方法进行这些操作。但是,因为atomicInteger借助了UnSafe提供的CAS操作能够保证数据更新的时候是线程安全的,并且由于CAS是采用乐观锁策略,因此,这种数据更新的方法也具有高效性。 76 | 77 | AtomicLong的实现原理和AtomicInteger一致,只不过一个针对的是long变量,一个针对的是int变量。而boolean变量的更新类AtomicBoolean类是怎样实现更新的呢?核心方法是`compareAndSet`t方法,其源码如下: 78 | 79 | public final boolean compareAndSet(boolean expect, boolean update) { 80 | int e = expect ? 1 : 0; 81 | int u = update ? 1 : 0; 82 | return unsafe.compareAndSwapInt(this, valueOffset, e, u); 83 | } 84 | 85 | 86 | 可以看出,compareAndSet方法的实际上也是先转换成0,1的整型变量,然后是通过针对int型变量的原子更新方法compareAndSwapInt来实现的。可以看出atomic包中只提供了对boolean,int ,long这三种基本类型的原子更新的方法,参考对boolean更新的方式,原子更新char,doule,float也可以采用类似的思路进行实现。 87 | 88 | # 4. 原子更新数组类型 # 89 | 90 | atomic包下提供能原子更新数组中元素的类有: 91 | 92 | 1. AtomicIntegerArray:原子更新整型数组中的元素; 93 | 2. AtomicLongArray:原子更新长整型数组中的元素; 94 | 3. AtomicReferenceArray:原子更新引用类型数组中的元素 95 | 96 | 这几个类的用法一致,就以AtomicIntegerArray来总结下常用的方法: 97 | 98 | 1. addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值相加; 99 | 2. getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1; 100 | 3. compareAndSet(int i, int expect, int update):将数组中索引为i的位置的元素进行更新 101 | 102 | 可以看出,AtomicIntegerArray与AtomicInteger的方法基本一致,只不过在AtomicIntegerArray的方法中会多一个指定数组索引位i。下面举一个简单的例子: 103 | 104 | 105 | public class AtomicDemo { 106 | // private static AtomicInteger atomicInteger = new AtomicInteger(1); 107 | private static int[] value = new int[]{1, 2, 3}; 108 | private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value); 109 | 110 | public static void main(String[] args) { 111 | //对数组中索引为1的位置的元素加5 112 | int result = integerArray.getAndAdd(1, 5); 113 | System.out.println(integerArray.get(1)); 114 | System.out.println(result); 115 | } 116 | } 117 | 输出结果: 118 | 7 119 | 2 120 | 121 | 通过getAndAdd方法将位置为1的元素加5,从结果可以看出索引为1的元素变成了7,该方法返回的也是相加之前的数为2。 122 | # 5. 原子更新引用类型 # 123 | 124 | 如果需要原子更新引用类型变量的话,为了保证线程安全,atomic也提供了相关的类: 125 | 126 | 1. AtomicReference:原子更新引用类型; 127 | 2. AtomicReferenceFieldUpdater:原子更新引用类型里的字段; 128 | 3. AtomicMarkableReference:原子更新带有标记位的引用类型; 129 | 130 | 这几个类的使用方法也是基本一样的,以AtomicReference为例,来说明这些类的基本用法。下面是一个demo 131 | 132 | 133 | public class AtomicDemo { 134 | 135 | private static AtomicReference reference = new AtomicReference<>(); 136 | 137 | public static void main(String[] args) { 138 | User user1 = new User("a", 1); 139 | reference.set(user1); 140 | User user2 = new User("b",2); 141 | User user = reference.getAndSet(user2); 142 | System.out.println(user); 143 | System.out.println(reference.get()); 144 | } 145 | 146 | static class User { 147 | private String userName; 148 | private int age; 149 | 150 | public User(String userName, int age) { 151 | this.userName = userName; 152 | this.age = age; 153 | } 154 | 155 | @Override 156 | public String toString() { 157 | return "User{" + 158 | "userName='" + userName + '\'' + 159 | ", age=" + age + 160 | '}'; 161 | } 162 | } 163 | } 164 | 165 | 输出结果: 166 | User{userName='a', age=1} 167 | User{userName='b', age=2} 168 | 169 | 首先将对象User1用AtomicReference进行封装,然后调用getAndSet方法,从结果可以看出,该方法会原子更新引用的user对象,变为`User{userName='b', age=2}`,返回的是原来的user对象User`{userName='a', age=1}`。 170 | 171 | # 6. 原子更新字段类型 # 172 | 173 | 如果需要更新对象的某个字段,并在多线程的情况下,能够保证线程安全,atomic同样也提供了相应的原子操作类: 174 | 175 | 1. AtomicIntegeFieldUpdater:原子更新整型字段类; 176 | 2. AtomicLongFieldUpdater:原子更新长整型字段类; 177 | 3. AtomicStampedReference:原子更新引用类型,这种更新方式会带有版本号。而为什么在更新的时候会带有版本号,是为了解决CAS的ABA问题; 178 | 179 | 要想使用原子更新字段需要两步操作: 180 | 181 | 1. 原子更新字段类都是抽象类,只能通过静态方法`newUpdater`来创建一个更新器,并且需要设置想要更新的类和属性; 182 | 2. 更新类的属性必须使用`public volatile`进行修饰; 183 | 184 | 这几个类提供的方法基本一致,以AtomicIntegerFieldUpdater为例来看看具体的使用: 185 | 186 | public class AtomicDemo { 187 | 188 | private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age"); 189 | public static void main(String[] args) { 190 | User user = new User("a", 1); 191 | int oldValue = updater.getAndAdd(user, 5); 192 | System.out.println(oldValue); 193 | System.out.println(updater.get(user)); 194 | } 195 | 196 | static class User { 197 | private String userName; 198 | public volatile int age; 199 | 200 | public User(String userName, int age) { 201 | this.userName = userName; 202 | this.age = age; 203 | } 204 | 205 | @Override 206 | public String toString() { 207 | return "User{" + 208 | "userName='" + userName + '\'' + 209 | ", age=" + age + 210 | '}'; 211 | } 212 | } 213 | } 214 | 215 | 输出结果: 216 | 1 217 | 6 218 | 219 | 从示例中可以看出,创建`AtomicIntegerFieldUpdater`是通过它提供的静态方法进行创建,`getAndAdd`方法会将指定的字段加上输入的值,并且返回相加之前的值。user对象中age字段原值为1,加5之后,可以看出user对象中的age字段的值已经变成了6。 220 | 221 | 222 | 223 | 224 | 225 | 226 | -------------------------------------------------------------------------------- /25.大白话说java并发工具类-CountDownLatch,CyclicBarrier/CyclicBarrier执行示意图.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/25.大白话说java并发工具类-CountDownLatch,CyclicBarrier/CyclicBarrier执行示意图.jpg -------------------------------------------------------------------------------- /25.大白话说java并发工具类-CountDownLatch,CyclicBarrier/大白话说java并发工具类-CountDownLatch,CyclicBarrier.md: -------------------------------------------------------------------------------- 1 | 2 | # 1. 倒计时器CountDownLatch # 3 | 4 | 在多线程协作完成业务功能时,有时候需要等待其他多个线程完成任务之后,主线程才能继续往下执行业务功能,在这种的业务场景下,通常可以使用Thread类的join方法,让主线程等待被join的线程执行完之后,主线程才能继续往下执行。当然,使用线程间消息通信机制也可以完成。其实,java并发工具类中为我们提供了类似“倒计时”这样的工具类,可以十分方便的完成所说的这种业务场景。 5 | 6 | 为了能够理解CountDownLatch,举一个很通俗的例子,运动员进行跑步比赛时,假设有6个运动员参与比赛,裁判员在终点会为这6个运动员分别计时,可以想象没当一个运动员到达终点的时候,对于裁判员来说就少了一个计时任务。直到所有运动员都到达终点了,裁判员的任务也才完成。这6个运动员可以类比成6个线程,当线程调用CountDownLatch.countDown方法时就会对计数器的值减一,直到计数器的值为0的时候,裁判员(调用await方法的线程)才能继续往下执行。 7 | 8 | 下面来看些CountDownLatch的一些重要方法。 9 | 10 | 先从CountDownLatch的构造方法看起: 11 | 12 | public CountDownLatch(int count) 13 | 14 | 构造方法会传入一个整型数N,之后调用CountDownLatch的`countDown`方法会对N减一,知道N减到0的时候,当前调用`await`方法的线程继续执行。 15 | 16 | CountDownLatch的方法不是很多,将它们一个个列举出来: 17 | 18 | 19 | 1. await() throws InterruptedException:调用该方法的线程等到构造方法传入的N减到0的时候,才能继续往下执行; 20 | 2. await(long timeout, TimeUnit unit):与上面的await方法功能一致,只不过这里有了时间限制,调用该方法的线程等到指定的timeout时间后,不管N是否减至为0,都会继续往下执行; 21 | 3. countDown():使CountDownLatch初始值N减1; 22 | 4. long getCount():获取当前CountDownLatch维护的值; 23 | 24 | 下面用一个具体的例子来说明CountDownLatch的具体用法: 25 | 26 | public class CountDownLatchDemo { 27 | private static CountDownLatch startSignal = new CountDownLatch(1); 28 | //用来表示裁判员需要维护的是6个运动员 29 | private static CountDownLatch endSignal = new CountDownLatch(6); 30 | 31 | public static void main(String[] args) throws InterruptedException { 32 | ExecutorService executorService = Executors.newFixedThreadPool(6); 33 | for (int i = 0; i < 6; i++) { 34 | executorService.execute(() -> { 35 | try { 36 | System.out.println(Thread.currentThread().getName() + " 运动员等待裁判员响哨!!!"); 37 | startSignal.await(); 38 | System.out.println(Thread.currentThread().getName() + "正在全力冲刺"); 39 | endSignal.countDown(); 40 | System.out.println(Thread.currentThread().getName() + " 到达终点"); 41 | } catch (InterruptedException e) { 42 | e.printStackTrace(); 43 | } 44 | }); 45 | } 46 | System.out.println("裁判员发号施令啦!!!"); 47 | startSignal.countDown(); 48 | endSignal.await(); 49 | System.out.println("所有运动员到达终点,比赛结束!"); 50 | executorService.shutdown(); 51 | } 52 | } 53 | 输出结果: 54 | 55 | pool-1-thread-2 运动员等待裁判员响哨!!! 56 | pool-1-thread-3 运动员等待裁判员响哨!!! 57 | pool-1-thread-1 运动员等待裁判员响哨!!! 58 | pool-1-thread-4 运动员等待裁判员响哨!!! 59 | pool-1-thread-5 运动员等待裁判员响哨!!! 60 | pool-1-thread-6 运动员等待裁判员响哨!!! 61 | 裁判员发号施令啦!!! 62 | pool-1-thread-2正在全力冲刺 63 | pool-1-thread-2 到达终点 64 | pool-1-thread-3正在全力冲刺 65 | pool-1-thread-3 到达终点 66 | pool-1-thread-1正在全力冲刺 67 | pool-1-thread-1 到达终点 68 | pool-1-thread-4正在全力冲刺 69 | pool-1-thread-4 到达终点 70 | pool-1-thread-5正在全力冲刺 71 | pool-1-thread-5 到达终点 72 | pool-1-thread-6正在全力冲刺 73 | pool-1-thread-6 到达终点 74 | 所有运动员到达终点,比赛结束! 75 | 76 | 该示例代码中设置了两个CountDownLatch,第一个`endSignal`用于控制让main线程(裁判员)必须等到其他线程(运动员)让CountDownLatch维护的数值N减到0为止。另一个`startSignal`用于让main线程对其他线程进行“发号施令”,startSignal引用的CountDownLatch初始值为1,而其他线程执行的run方法中都会先通过 ` startSignal.await()`让这些线程都被阻塞,直到main线程通过调用`startSignal.countDown();`,将值N减1,CountDownLatch维护的数值N为0后,其他线程才能往下执行,并且,每个线程执行的run方法中都会通过`endSignal.countDown();`对`endSignal`维护的数值进行减一,由于往线程池提交了6个任务,会被减6次,所以`endSignal`维护的值最终会变为0,因此main线程在`latch.await();`阻塞结束,才能继续往下执行。 77 | 78 | 另外,需要注意的是,当调用CountDownLatch的countDown方法时,当前线程是不会被阻塞,会继续往下执行,比如在该例中会继续输出`pool-1-thread-4 到达终点`。 79 | 80 | 81 | # 2. 循环栅栏:CyclicBarrier # 82 | 83 | CyclicBarrier也是一种多线程并发控制的实用工具,和CountDownLatch一样具有等待计数的功能,但是相比于CountDownLatch功能更加强大。 84 | 85 | 为了理解CyclicBarrier,这里举一个通俗的例子。开运动会时,会有跑步这一项运动,我们来模拟下运动员入场时的情况,假设有6条跑道,在比赛开始时,就需要6个运动员在比赛开始的时候都站在起点了,裁判员吹哨后才能开始跑步。跑道起点就相当于“barrier”,是临界点,而这6个运动员就类比成线程的话,就是这6个线程都必须到达指定点了,意味着凑齐了一波,然后才能继续执行,否则每个线程都得阻塞等待,直至凑齐一波即可。cyclic是循环的意思,也就是说CyclicBarrier当多个线程凑齐了一波之后,仍然有效,可以继续凑齐下一波。CyclicBarrier的执行示意图如下: 86 | 87 | ![CyclicBarrier执行示意图.jpg](https://upload-images.jianshu.io/upload_images/2615789-5bacb4f757882e56.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/500) 88 | 89 | 90 | 91 | 当多个线程都达到了指定点后,才能继续往下继续执行。这就有点像报数的感觉,假设6个线程就相当于6个运动员,到赛道起点时会报数进行统计,如果刚好是6的话,这一波就凑齐了,才能往下执行。**CyclicBarrier在使用一次后,下面依然有效,可以继续当做计数器使用,这是与CountDownLatch的区别之一。**这里的6个线程,也就是计数器的初始值6,是通过CyclicBarrier的构造方法传入的。 92 | 93 | 下面来看下CyclicBarrier的主要方法: 94 | 95 | 96 | //等到所有的线程都到达指定的临界点 97 | await() throws InterruptedException, BrokenBarrierException 98 | 99 | //与上面的await方法功能基本一致,只不过这里有超时限制,阻塞等待直至到达超时时间为止 100 | await(long timeout, TimeUnit unit) throws InterruptedException, 101 | BrokenBarrierException, TimeoutException 102 | 103 | //获取当前有多少个线程阻塞等待在临界点上 104 | int getNumberWaiting() 105 | 106 | //用于查询阻塞等待的线程是否被中断 107 | boolean isBroken() 108 | 109 | 110 | //将屏障重置为初始状态。如果当前有线程正在临界点等待的话,将抛出BrokenBarrierException。 111 | void reset() 112 | 113 | 另外需要注意的是,CyclicBarrier提供了这样的构造方法: 114 | 115 | public CyclicBarrier(int parties, Runnable barrierAction) 116 | 117 | 可以用来,当指定的线程都到达了指定的临界点的时,接下来执行的操作可以由barrierAction传入即可。 118 | 119 | 120 | > 一个例子 121 | 122 | 下面用一个简单的例子,来看下CyclicBarrier的用法,我们来模拟下上面的运动员的例子。 123 | 124 | 125 | public class CyclicBarrierDemo { 126 | //指定必须有6个运动员到达才行 127 | private static CyclicBarrier barrier = new CyclicBarrier(6, () -> { 128 | System.out.println("所有运动员入场,裁判员一声令下!!!!!"); 129 | }); 130 | public static void main(String[] args) { 131 | System.out.println("运动员准备进场,全场欢呼............"); 132 | 133 | ExecutorService service = Executors.newFixedThreadPool(6); 134 | for (int i = 0; i < 6; i++) { 135 | service.execute(() -> { 136 | try { 137 | System.out.println(Thread.currentThread().getName() + " 运动员,进场"); 138 | barrier.await(); 139 | System.out.println(Thread.currentThread().getName() + " 运动员出发"); 140 | } catch (InterruptedException e) { 141 | e.printStackTrace(); 142 | } catch (BrokenBarrierException e) { 143 | e.printStackTrace(); 144 | } 145 | }); 146 | } 147 | } 148 | 149 | } 150 | 151 | 输出结果: 152 | 运动员准备进场,全场欢呼............ 153 | pool-1-thread-2 运动员,进场 154 | pool-1-thread-1 运动员,进场 155 | pool-1-thread-3 运动员,进场 156 | pool-1-thread-4 运动员,进场 157 | pool-1-thread-5 运动员,进场 158 | pool-1-thread-6 运动员,进场 159 | 所有运动员入场,裁判员一声令下!!!!! 160 | pool-1-thread-6 运动员出发 161 | pool-1-thread-1 运动员出发 162 | pool-1-thread-5 运动员出发 163 | pool-1-thread-4 运动员出发 164 | pool-1-thread-3 运动员出发 165 | pool-1-thread-2 运动员出发 166 | 167 | 从输出结果可以看出,当6个运动员(线程)都到达了指定的临界点(barrier)时候,才能继续往下执行,否则,则会阻塞等待在调用`await()`处 168 | 169 | 170 | # 3. CountDownLatch与CyclicBarrier的比较 # 171 | 172 | CountDownLatch与CyclicBarrier都是用于控制并发的工具类,都可以理解成维护的就是一个计数器,但是这两者还是各有不同侧重点的: 173 | 174 | 1. CountDownLatch一般用于某个线程A等待若干个其他线程执行完任务之后,它才执行;而CyclicBarrier一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行;CountDownLatch强调一个线程等多个线程完成某件事情。CyclicBarrier是多个线程互等,等大家都完成,再携手共进。 175 | 2. 调用CountDownLatch的countDown方法后,当前线程并不会阻塞,会继续往下执行;而调用CyclicBarrier的await方法,会阻塞当前线程,直到CyclicBarrier指定的线程全部都到达了指定点的时候,才能继续往下执行; 176 | 3. CountDownLatch方法比较少,操作比较简单,而CyclicBarrier提供的方法更多,比如能够通过getNumberWaiting(),isBroken()这些方法获取当前多个线程的状态,**并且CyclicBarrier的构造方法可以传入barrierAction**,指定当所有线程都到达时执行的业务功能; 177 | 4. CountDownLatch是不能复用的,而CyclicLatch是可以复用的。 -------------------------------------------------------------------------------- /26.大白话说java并发工具类-Semaphore,Exchanger/大白话说java并发工具类-Semaphore,Exchanger.md: -------------------------------------------------------------------------------- 1 | 2 | # 1. 控制资源并发访问--Semaphore # 3 | 4 | Semaphore可以理解为**信号量**,用于控制资源能够被并发访问的线程数量,以保证多个线程能够合理的使用特定资源。Semaphore就相当于一个许可证,线程需要先通过acquire方法获取该许可证,该线程才能继续往下执行,否则只能在该方法出阻塞等待。当执行完业务功能后,需要通过`release()`方法将许可证归还,以便其他线程能够获得许可证继续执行。 5 | 6 | 7 | Semaphore可以用于做流量控制,特别是公共资源有限的应用场景,比如数据库连接。假如有多个线程读取数据后,需要将数据保存在数据库中,而可用的最大数据库连接只有10个,这时候就需要使用Semaphore来控制能够并发访问到数据库连接资源的线程个数最多只有10个。在限制资源使用的应用场景下,Semaphore是特别合适的。 8 | 9 | 下面来看下Semaphore的主要方法: 10 | 11 | //获取许可,如果无法获取到,则阻塞等待直至能够获取为止 12 | void acquire() throws InterruptedException 13 | 14 | //同acquire方法功能基本一样,只不过该方法可以一次获取多个许可 15 | void acquire(int permits) throws InterruptedException 16 | 17 | //释放许可 18 | void release() 19 | 20 | //释放指定个数的许可 21 | void release(int permits) 22 | 23 | //尝试获取许可,如果能够获取成功则立即返回true,否则,则返回false 24 | boolean tryAcquire() 25 | 26 | //与tryAcquire方法一致,只不过这里可以指定获取多个许可 27 | boolean tryAcquire(int permits) 28 | 29 | //尝试获取许可,如果能够立即获取到或者在指定时间内能够获取到,则返回true,否则返回false 30 | boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException 31 | 32 | //与上一个方法一致,只不过这里能够获取多个许可 33 | boolean tryAcquire(int permits, long timeout, TimeUnit unit) 34 | 35 | //返回当前可用的许可证个数 36 | int availablePermits() 37 | 38 | //返回正在等待获取许可证的线程数 39 | int getQueueLength() 40 | 41 | //是否有线程正在等待获取许可证 42 | boolean hasQueuedThreads() 43 | 44 | //获取所有正在等待许可的线程集合 45 | Collection getQueuedThreads() 46 | 47 | 另外,在Semaphore的构造方法中还支持指定是够具有公平性,默认的是非公平性,这样也是为了保证吞吐量。 48 | 49 | 50 | > 一个例子 51 | 52 | 下面用一个简单的例子来说明Semaphore的具体使用。我们来模拟这样一样场景。有一天,班主任需要班上10个同学到讲台上来填写一个表格,但是老师只准备了5支笔,因此,只能保证同时只有5个同学能够拿到笔并填写表格,没有获取到笔的同学只能够等前面的同学用完之后,才能拿到笔去填写表格。该示例代码如下: 53 | 54 | 55 | 56 | public class SemaphoreDemo { 57 | 58 | //表示老师只有10支笔 59 | private static Semaphore semaphore = new Semaphore(5); 60 | 61 | public static void main(String[] args) { 62 | 63 | //表示50个学生 64 | ExecutorService service = Executors.newFixedThreadPool(10); 65 | for (int i = 0; i < 10; i++) { 66 | service.execute(() -> { 67 | try { 68 | System.out.println(Thread.currentThread().getName() + " 同学准备获取笔......"); 69 | semaphore.acquire(); 70 | System.out.println(Thread.currentThread().getName() + " 同学获取到笔"); 71 | System.out.println(Thread.currentThread().getName() + " 填写表格ing....."); 72 | TimeUnit.SECONDS.sleep(3); 73 | semaphore.release(); 74 | System.out.println(Thread.currentThread().getName() + " 填写完表格,归还了笔!!!!!!"); 75 | } catch (InterruptedException e) { 76 | e.printStackTrace(); 77 | } 78 | }); 79 | } 80 | service.shutdown(); 81 | } 82 | 83 | } 84 | 输出结果: 85 | 86 | pool-1-thread-1 同学准备获取笔...... 87 | pool-1-thread-1 同学获取到笔 88 | pool-1-thread-1 填写表格ing..... 89 | pool-1-thread-2 同学准备获取笔...... 90 | pool-1-thread-2 同学获取到笔 91 | pool-1-thread-2 填写表格ing..... 92 | pool-1-thread-3 同学准备获取笔...... 93 | pool-1-thread-4 同学准备获取笔...... 94 | pool-1-thread-3 同学获取到笔 95 | pool-1-thread-4 同学获取到笔 96 | pool-1-thread-4 填写表格ing..... 97 | pool-1-thread-3 填写表格ing..... 98 | pool-1-thread-5 同学准备获取笔...... 99 | pool-1-thread-5 同学获取到笔 100 | pool-1-thread-5 填写表格ing..... 101 | 102 | 103 | pool-1-thread-6 同学准备获取笔...... 104 | pool-1-thread-7 同学准备获取笔...... 105 | pool-1-thread-8 同学准备获取笔...... 106 | pool-1-thread-9 同学准备获取笔...... 107 | pool-1-thread-10 同学准备获取笔...... 108 | 109 | 110 | pool-1-thread-4 填写完表格,归还了笔!!!!!! 111 | pool-1-thread-9 同学获取到笔 112 | pool-1-thread-9 填写表格ing..... 113 | pool-1-thread-5 填写完表格,归还了笔!!!!!! 114 | pool-1-thread-7 同学获取到笔 115 | pool-1-thread-7 填写表格ing..... 116 | pool-1-thread-8 同学获取到笔 117 | pool-1-thread-8 填写表格ing..... 118 | pool-1-thread-1 填写完表格,归还了笔!!!!!! 119 | pool-1-thread-6 同学获取到笔 120 | pool-1-thread-6 填写表格ing..... 121 | pool-1-thread-3 填写完表格,归还了笔!!!!!! 122 | pool-1-thread-2 填写完表格,归还了笔!!!!!! 123 | pool-1-thread-10 同学获取到笔 124 | pool-1-thread-10 填写表格ing..... 125 | pool-1-thread-7 填写完表格,归还了笔!!!!!! 126 | pool-1-thread-9 填写完表格,归还了笔!!!!!! 127 | pool-1-thread-8 填写完表格,归还了笔!!!!!! 128 | pool-1-thread-6 填写完表格,归还了笔!!!!!! 129 | pool-1-thread-10 填写完表格,归还了笔!!!!!! 130 | 131 | 132 | 根据输出结果进行分析,Semaphore允许的最大许可数为5,也就是允许的最大并发执行的线程个数为5,可以看出,前5个线程(前5个学生)先获取到笔,然后填写表格,而6-10这5个线程,由于获取不到许可,只能阻塞等待。当线程`pool-1-thread-4`释放了许可之后,`pool-1-thread-9`就可以获取到许可,继续往下执行。对其他线程的执行过程,也是同样的道理。从这个例子就可以看出,**Semaphore用来做特殊资源的并发访问控制是相当合适的,如果有业务场景需要进行流量控制,可以优先考虑Semaphore。** 133 | 134 | 135 | # 2.线程间交换数据的工具--Exchanger # 136 | 137 | Exchanger是一个用于线程间协作的工具类,用于两个线程间能够交换。它提供了一个交换的同步点,在这个同步点两个线程能够交换数据。具体交换数据是通过exchange方法来实现的,如果一个线程先执行exchange方法,那么它会同步等待另一个线程也执行exchange方法,这个时候两个线程就都达到了同步点,两个线程就可以交换数据。 138 | 139 | Exchanger除了一个无参的构造方法外,主要方法也很简单: 140 | 141 | //当一个线程执行该方法的时候,会等待另一个线程也执行该方法,因此两个线程就都达到了同步点 142 | //将数据交换给另一个线程,同时返回获取的数据 143 | V exchange(V x) throws InterruptedException 144 | 145 | //同上一个方法功能基本一样,只不过这个方法同步等待的时候,增加了超时时间 146 | V exchange(V x, long timeout, TimeUnit unit) 147 | throws InterruptedException, TimeoutException 148 | 149 | 150 | > 一个例子 151 | 152 | Exchanger理解起来很容易,这里用一个简单的例子来看下它的具体使用。我们来模拟这样一个情景,在青春洋溢的中学时代,下课期间,男生经常会给走廊里为自己喜欢的女孩子送情书,相信大家都做过这样的事情吧 :)。男孩会先到女孩教室门口,然后等女孩出来,教室那里就是一个同步点,然后彼此交换信物,也就是彼此交换了数据。现在,就来模拟这个情景。 153 | 154 | public class ExchangerDemo { 155 | private static Exchanger exchanger = new Exchanger(); 156 | 157 | public static void main(String[] args) { 158 | 159 | //代表男生和女生 160 | ExecutorService service = Executors.newFixedThreadPool(2); 161 | 162 | service.execute(() -> { 163 | try { 164 | //男生对女生说的话 165 | String girl = exchanger.exchange("我其实暗恋你很久了......"); 166 | System.out.println("女孩儿说:" + girl); 167 | } catch (InterruptedException e) { 168 | e.printStackTrace(); 169 | } 170 | }); 171 | service.execute(() -> { 172 | try { 173 | System.out.println("女生慢慢的从教室你走出来......"); 174 | TimeUnit.SECONDS.sleep(3); 175 | //男生对女生说的话 176 | String boy = exchanger.exchange("我也很喜欢你......"); 177 | System.out.println("男孩儿说:" + boy); 178 | } catch (InterruptedException e) { 179 | e.printStackTrace(); 180 | } 181 | }); 182 | 183 | } 184 | } 185 | 186 | 输出结果: 187 | 188 | 女生慢慢的从教室你走出来...... 189 | 男孩儿说:我其实暗恋你很久了...... 190 | 女孩儿说:我也很喜欢你...... 191 | 192 | 这个例子很简单,也很能说明Exchanger的基本使用。当两个线程都到达调用exchange方法的同步点的时候,两个线程就能交换彼此的数据。 -------------------------------------------------------------------------------- /Java并发知识图谱.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/Java并发知识图谱.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | > 努力的意义,就是,在以后的日子里,放眼望去全是自己喜欢的人和事! 3 | 4 | 欢迎提issue和Pull request。所有的文档都是自己亲自码的,如果觉得不错,欢迎给star鼓励支持 :) 5 | 6 | 7 | 整个系列文章为**Java并发专题**,一是自己的兴趣,二是,这部分在实际理解上很有难度,另外在面试过程中也是经常被问到。所以在学习过程中,记录了Java并发相关的基础知识,一是自己对知识能够建立体系,同时也希望有幸能够对其他人有用。 8 | 9 | 10 | 关于Java并发专题: 11 | 12 | 13 | (1)包含了并发的基础知识,每个标题链接到一篇具体的文章; 14 | 15 | (2)包含了秋招面试的问题,弄懂了会让你有所收获(也祝大家都能找到心仪的工作 :) ) 16 | 17 | (3)在阅读过程中,如果有所帮助,麻烦点赞,算是对我码字的这份坚持的鼓励。 18 | 19 | **注:转载请标明原处,谢谢!** 20 | 21 | 1. **基础知识** 22 | 23 | 1.1 [并发编程的优缺点](https://juejin.im/post/5ae6c3ef6fb9a07ab508ac85) 24 | 25 | 知识点:(1)为什么要用到并发?(优点);(2)并发编程的缺点;(3)易混淆的概念 26 | 27 | 1.2 [线程的状态和基本操作](https://juejin.im/post/5ae6cf7a518825670960fcc2) 28 | 29 | 知识点:(1)如何新建线程;(2)线程状态的转换;(3)线程的基本操作;(4)守护线程Daemon; 30 | 31 | 2. **并发理论(JMM)** 32 | 33 | [java内存模型以及happens-before规则](https://juejin.im/post/5ae6d309518825673123fd0e) 34 | 35 | 知识点:(1)JMM内存结构;(2)重排序;(3)happens-before规则 36 | 37 | 38 | 3. **并发关键字** 39 | 40 | 3.1 [让你彻底理解Synchronized](https://juejin.im/post/5ae6dc04f265da0ba351d3ff) 41 | 42 | 知识点:(1)如何使用synchronized;(2)monitor机制;(3)synchronized的happens-before关系;(4)synchronized的内存语义;(5)锁优化;(6)锁升级策略 43 | 44 | 3.2 [让你彻底理解volatile](https://juejin.im/post/5ae9b41b518825670b33e6c4) 45 | 46 | 知识点:(1)实现原理;(2)happens-before的关系推导;(3)内存语义;(4)内存语义的实现 47 | 48 | 3.3 [你以为你真的了解final吗?](https://juejin.im/post/5ae9b82c6fb9a07ac3634941) 49 | 50 | 知识点:(1)如何使用;(2)final的重排序规则;(3)final实现原理;(4)final引用不能从构造函数中“溢出”(this逃逸) 51 | 52 | 3.4 [三大性质总结:原子性,有序性,可见性](https://juejin.im/post/5aeb022cf265da0b722af7b8) 53 | 54 | 知识点:(1)原子性:synchronized;(2)可见性:synchronized,volatile;(3)有序性:synchronized,volatile 55 | 56 | 4. **Lock体系** 57 | 58 | 4.1 [初识Lock与AbstractQueuedSynchronizer(AQS)](https://juejin.im/post/5aeb055b6fb9a07abf725c8c) 59 | 60 | 知识点:(1)Lock和synchronized的比较;(2)AQS设计意图;(3)如何使用AQS实现自定义同步组件;(4)可重写的方法;(5)AQS提供的模板方法; 61 | 62 | 4.2 [深入理解AbstractQueuedSynchronizer(AQS)](https://juejin.im/post/5aeb07ab6fb9a07ac36350c8) 63 | 64 | 知识点:(1)AQS同步队列的数据结构;(2)独占式锁;(3)共享式锁; 65 | 66 | 4.3 [再一次理解ReentrantLock](https://juejin.im/post/5aeb0a8b518825673a2066f0) 67 | 68 | 知识点:(1)重入锁的实现原理;(2)公平锁的实现原理;(3)非公平锁的实现原理;(4)公平锁和非公平锁的比较 69 | 70 | 4.4 [深入理解读写锁ReentrantReadWriteLock](https://juejin.im/post/5aeb0e016fb9a07ab7740d90) 71 | 72 | 知识点:(1)如何表示读写状态;(2)WriteLock的获取和释放;(3)ReadLock的获取和释放;(4)锁降级策略;(5)生成Condition等待队列;(6)应用场景 73 | 74 | 4.5 [详解Condition的await和signal等待/通知机制](https://juejin.im/post/5aeea5e951882506a36c67f0) 75 | 76 | 知识点:(1)与Object的wait/notify机制相比具有的特性;(2)与Object的wait/notify相对应的方法;(3)底层数据结构;(4)await实现原理;(5)signal/signalAll实现原理;(6)await和signal/signalAll的结合使用; 77 | 78 | 4.6 [LockSupport工具](https://juejin.im/post/5aeed27f51882567336aa0fa) 79 | 80 | 知识点:(1)主要功能;(2)与synchronized阻塞唤醒相比具有的特色; 81 | 82 | 83 | 5. **并发容器** 84 | 85 | 5.1 [并发容器之ConcurrentHashMap(JDK 1.8版本)](https://juejin.im/post/5aeeaba8f265da0b9d781d16) 86 | 87 | 知识点:(1)关键属性;(2)重要内部类;(3)涉及到的CAS操作;(4)构造方法;(5)put执行流程;(6)get执行流程;(7)扩容机制;(8)用于统计size的方法的执行流程;(9)1.8版本的ConcurrentHashMap与之前版本的比较 88 | 89 | 5.2 [并发容器之CopyOnWriteArrayList](https://juejin.im/post/5aeeb55f5188256715478c21) 90 | 91 | 知识点:(1)实现原理;(2)COW和ReentrantReadWriteLock的区别;(3)应用场景;(4)为什么具有弱一致性;(5)COW的缺点; 92 | 93 | 5.3 [并发容器之ConcurrentLinkedQueue](https://juejin.im/post/5aeeae756fb9a07ab11112af) 94 | 95 | 知识点:(1)实现原理;(2)数据结构;(3)核心方法;(4)HOPS延迟更新的设计意图 96 | 97 | 5.4 [并发容器之ThreadLocal](https://juejin.im/post/5aeeb22e6fb9a07aa213404a) 98 | 99 | 知识点:(1)实现原理;(2)set方法原理;(3)get方法原理;(4)remove方法原理;(5)ThreadLocalMap 100 | 101 | [一篇文章,从源码深入详解ThreadLocal内存泄漏问题](https://www.jianshu.com/p/dde92ec37bd1) 102 | 103 | 知识点:(1)ThreadLocal内存泄漏原理;(2)ThreadLocal的最佳实践;(3)应用场景 104 | 105 | 5.5 [并发容器之BlockingQueue](https://juejin.im/post/5aeebd02518825672f19c546) 106 | 107 | 知识点:(1)BlockingQueue的基本操作;(2)常用的BlockingQueue; 108 | 109 | [并发容器之ArrayBlockingQueue和LinkedBlockingQueue实现原理详解](https://juejin.im/post/5aeebdb26fb9a07aa83ea17e) 110 | 111 | 6. **线程池(Executor体系)** 112 | 113 | 6.1 [线程池实现原理](https://juejin.im/post/5aeec0106fb9a07ab379574f) 114 | 115 | 知识点:(1)为什么要用到线程池?(2)执行流程;(3)构造器各个参数的意义;(4)如何关闭线程池;(5)如何配置线程池; 116 | 117 | 6.2 [线程池之ScheduledThreadPoolExecutor](https://juejin.im/post/5aeec106518825670a10328a) 118 | 119 | 知识点:(1)类结构;(2)常用方法;(3)ScheduledFutureTask;(3)DelayedWorkQueue; 120 | 121 | 6.3 [FutureTask基本操作总结](https://juejin.im/post/5aeec249f265da0b886d5101) 122 | 123 | 知识点:(1)FutureTask的几种状态;(2)get方法;(3)cancel方法;(4)应用场景;(5)实现 Runnable接口 124 | 125 | 7. **原子操作类** 126 | 127 | 7.1 [Java中atomic包中的原子操作类总结](https://juejin.im/post/5aeec351518825670a103292) 128 | 129 | 知识点:(1)实现原理;(2)原子更新基本类型;(3)原子更新数组类型;(4)原子更新引用类型;(5)原子更新字段类型 130 | 131 | 8. **并发工具** 132 | 133 | 8.1 [大白话说java并发工具类-CountDownLatch,CyclicBarrier](https://juejin.im/post/5aeec3ebf265da0ba76fa327) 134 | 135 | 知识点:(1)倒计时器CountDownLatch;(2)循环栅栏CyclicBarrier;(3)CountDownLatch与CyclicBarrier的比较 136 | 137 | 8.2 [大白话说java并发工具类-Semaphore,Exchanger](https://juejin.im/post/5aeec49b518825673614d183) 138 | 139 | 知识点:(1)资源访问控制Semaphore;(2)数据交换Exchanger 140 | 141 | 9. **并发实践** 142 | 143 | 9.1 [一篇文章,让你彻底弄懂生产者--消费者问题](https://juejin.im/post/5aeec675f265da0b7c072c56) 144 | 145 | 146 | > JAVA并发知识图谱 147 | 148 | **可移动到新窗口,放大查看效果更好或者查看原图** 149 | 150 | [知识图谱原图链接,如果有用,可克隆给自己使用](https://www.processon.com/view/5ab5a979e4b0a248b0e026b3?fromnew=1) 151 | 152 | ![JAVA并发知识图谱.png](https://github.com/CL0610/Java-concurrency/blob/master/Java并发知识图谱.png) 153 | -------------------------------------------------------------------------------- /高级测试工程师-18362989059.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CL0610/Java-concurrency/ec21f5a4b0eba787b5794edca355287fd323cd69/高级测试工程师-18362989059.pdf --------------------------------------------------------------------------------