元数据
Java并发实现原理:JDK源码剖析
- 书名: Java并发实现原理:JDK源码剖析
- 作者: 余春龙
- 简介: 本书全面而系统地剖析了Java Concurrent包中的每一个部分,对并发的实现原理进行了深刻的探讨。全书分为8章,第1章从最基础的多线程知识讲起,理清多线程中容易误解的知识点,探究背后的原理,包括内存重排序、happen-before、内存屏障等;第2~8章,从简单到复杂,逐个剖析Concurrent包的每个部分,包括原子类、锁、同步工具类、并发容器、线程池、ForkJoinPool、CompletableFuture共7个部分。本书遵循层层递进的逻辑,后一章建立在前一章的知识点基础之上,建议读者由浅入深,逐步深入阅读。本书适合有一定Java开发经验的工程师、架构师阅读。通过本书,读者可以对多线程编程形成一个“深刻而直观”的认识,而不是再仅仅停留在概念和理论层面。
- 出版时间 2020-03-01 00:00:00
- ISBN: 9787121379727
- 分类: 计算机-编程设计
- 出版社: 电子工业出版社
高亮划线
第1章 多线程基础
- 📌 当在一个JVM进程里面开多个线程时,这些线程被分成两类:守护线程和非守护线程。默认开的都是非守护线程。在Java中有一个规定:当所有的非守护线程退出后,整个JVM进程就会退出。 ^31186368-5-1570-1687
- ⏱ 2022-01-11 13:43:25
1.2 InterruptedException()函数与interrupt()函数
-
📌 只有那些声明了会抛出InterruptedException 的函数才会抛出异常 ^31186368-6-1387-1427
- ⏱ 2022-01-11 13:51:16
-
📌 t.interrupted()的精确含义是“唤醒轻量级阻塞”,而不是字面意思“中断一个线程”。 ^31186368-6-2642-2689
- ⏱ 2022-01-11 13:56:11
-
📌 能够被中断的阻塞称为轻量级阻塞,对应的线程状态是WAITING或者TIMED_WAITING;而像synchronized 这种不能被中断的阻塞称为重量级阻塞,对应的状态是BLOCKED。 ^31186368-6-1769-1863
- ⏱ 2022-01-11 13:56:27
-
📌 t.isInterrupted()与Thread.interrupted()的区别 ^31186368-6-2789-2831
- ⏱ 2022-01-11 13:58:28
-
📌 ,前者只是读取中断状态,不修改状态;后者不仅读取中断状态,还会重置中断标志位。 ^31186368-6-3124-3163
- ⏱ 2022-01-11 13:58:42
1.3 synchronized关键字
- 📌 synchronized关键字其实是“给某个对象加了把锁” ^31186368-7-657-686
- ⏱ 2022-01-11 13:59:43
1.4 wait()与notify()
-
📌 生产者-消费者模型 ^31186368-8-537-546
- ⏱ 2022-01-11 14:07:17
-
📌 要实现这样一个编程模型,需要做下面几件事情:(1)内存队列本身要加锁 ^31186368-8-922-985
- ⏱ 2022-01-11 14:07:36
-
📌 (2)阻塞。 ^31186368-8-1024-1030
- ⏱ 2022-01-11 14:07:43
-
📌 (3)双向通知。 ^31186368-8-1105-1113
- ⏱ 2022-01-11 14:07:55
-
📌 在wait()的内部,会先释放锁obj1,然后进入阻塞状态,之后,它被另外一个线程用notify()唤醒,去重新拿锁! ^31186368-8-3221-3280
- ⏱ 2022-01-11 14:19:01
-
📌 wait()内部的伪代码如下: ^31186368-8-3362-3562
- ⏱ 2022-01-11 14:22:27
1.5 volatile关键字
-
📌 64位写入的原子性(Half Write) ^31186368-9-720-741
- ⏱ 2022-01-11 14:38:44
-
📌 因为JVM的规范并没有要求64位的long或者double的写入是原子的。在32位的机器上,一个64位变量的写入可能被拆分成两个32位的写操作来执行。 ^31186368-9-1107-1182
- ⏱ 2022-01-11 14:38:59
-
📌 内存可见性 ^31186368-9-1335-1340
- ⏱ 2022-01-11 14:39:07
-
📌 “内存可见性”,指的是“写完之后立即对其他线程可见 ^31186368-9-1741-1766
- ⏱ 2022-01-11 14:39:36
-
📌 单例模式的线程安全的写法不止一种,常用写法为DCL(Double Checking Locking) ^31186368-9-2026-2076
- ⏱ 2022-01-11 14:41:10
-
📌 volatile的三重功效:64位写入的原子性、内存可见性和禁止重排序。 ^31186368-9-2706-2742
- ⏱ 2022-01-11 14:44:56
-
📌 重排序:DCL问题 ^31186368-9-1987-1996
- ⏱ 2022-01-11 14:46:36
-
📌 在这三个操作中,操作(2)和操作(3)可能重排序,即先把instance指向内存,再初始化成员变量,因为二者并没有先后的依赖关系。此时,另外一个线程可能拿到一个未完全初始化的对象。这时,直接访问里面的成员变量,就可能出错。这就是典型的“构造函数溢出”问题。解决办法也很简单,就是为instance变量加上volatile修饰。 ^31186368-9-2501-2664
- ⏱ 2022-01-11 14:55:37
1.6 JMM与happen-before
-
📌 L1、L2、L3和主内存之间是同步的,有缓存一致性协议的保证,但是Store Buffer、Load Buffer和L1之间却是异步的。也就是说,往内存中写入一个变量,这个变量会保存在Store Buffer里面,稍后才异步地写入L1中,同时同步写入主内存中。 ^31186368-10-1461-1591
- ⏱ 2022-01-11 16:23:41
-
📌 操作系统内核视角下的CPU缓存模型 ^31186368-10-2021-2038
- ⏱ 2022-01-11 16:24:17
-
📌 CPU内存重排序。CPU有自己的缓存,指令的执行顺序和写入主内存的顺序不完全一致。 ^31186368-10-2929-2970
- ⏱ 2022-01-11 16:26:41
-
📌 happen-before只确保如果A在B之前执行,则A的执行结果必须对B可见。 ^31186368-10-6746-6786
- ⏱ 2022-01-11 19:17:55
-
📌 volatile 变量不能重排序 ^31186368-10-7229-7245
- ⏱ 2022-01-11 19:17:57
-
📌 这个代码在C++中是错误的,但在Java中却是正确的。因为Java中的volatile关键字不仅具有内存可见性,还会禁止volatile变量写入和非volatile变量写入的重排序,但C++中的volatile关键字不会禁止这种重排序。 ^31186368-10-9697-9815
- ⏱ 2022-01-11 19:24:04
1.7 内存屏障
-
📌 内存屏障(Memory Barrier)。这也正是JMM和happen-before规则的底层实现原理。 ^31186368-11-495-547
- ⏱ 2022-01-11 19:24:08
-
📌 编译器的内存屏障,只是为了告诉编译器不要对指令进行重排序。当编译完成之后,这种内存屏障就消失了,CPU并不会感知到编译器中内存屏障的存在。而CPU的内存屏障是CPU提供的指令,可以由开发者显示调用。 ^31186368-11-576-704
- ⏱ 2022-01-11 19:24:12
1.8 final关键字
-
📌 对于构造函数溢出,通俗来讲,就是一个对象的构造并不是“原子的”,当一个线程正在构造对象时,另外一个线程却可以读到未构造好的“一半对象”。 ^31186368-12-1047-1115
- ⏱ 2022-01-11 19:17:55
-
📌 通过这种happen-before语义的限定,保证了final域的赋值,一定在构造函数之前完成 ^31186368-12-1736-1783
- ⏱ 2022-01-11 19:17:57
-
📌 happen-before规则总结 ^31186368-12-2060-2077
- ⏱ 2022-01-11 19:24:14
1.9 综合应用:无锁编程
-
📌 只需要对head 指针进行CAS 操纵,就能实现多线程的入栈和出栈。 ^31186368-13-2072-2106
- ⏱ 2022-01-11 19:17:56
-
📌 一写多读的无锁队列:volatile关键字 ^31186368-13-998-1019
- ⏱ 2022-01-11 19:24:09
-
📌 一写一读的无锁队列:内存屏障 ^31186368-13-805-819
- ⏱ 2022-01-11 19:24:10
-
📌 利用volatile 关键字可以实现一写多读的线程安全。具体来说,就是RingBuffer有一个头指针,对应一个生产者线程;多个尾指针对应多个消费者线程。每个消费者线程只会操作自己的尾指针。所有这些指针的类型都是volatile变量,通过头指针和尾指针的比较,判断队列是否为空。 ^31186368-13-1396-1535
- ⏱ 2022-01-11 19:24:12
-
📌 基于CAS和链表,可以实现一个多写多读的队列。具体来说,就是链表有一个头指针head和尾指针tail。入队列,通过对tail进行CAS操作完成;出队列,对head进行CAS操作完成。 ^31186368-13-1769-1860
- ⏱ 2022-01-11 19:24:13
-
📌 无锁栈 ^31186368-13-2024-2027
- ⏱ 2022-01-11 19:24:15
-
📌 多写多读的无锁队列:CAS ^31186368-13-1636-1649
- ⏱ 2022-01-11 19:24:16
第2章 Atomic类
- 📌 整个Concurrent包的层次体系 ^31186368-14-757-775
- ⏱ 2022-01-11 19:24:05
2.1 AtomicInteger和AtomicLong
-
📌 对于悲观锁,作者认为数据发生并发冲突的概率很大,所以读操作之前就上锁。 ^31186368-15-1439-1474
- ⏱ 2022-01-11 19:24:05
-
📌 对于多CPU或者多核,策略2就很有用了,因为没有线程切换的开销。 ^31186368-15-4046-4078
- ⏱ 2022-01-11 19:24:06
-
📌 判断数据是否被修改,同时写回新值,这两个操作要合成一个原子操作,也就是CAS(Compare And Set)。 ^31186368-15-1655-1711
- ⏱ 2022-01-11 19:24:07
-
📌 这两种策略并不是互斥的,可以结合使用。如果拿不到锁,先自旋几圈;如果自旋还拿不到锁,再阻塞,synchronized关键字就是这样的实现策略。 ^31186368-15-4184-4255
- ⏱ 2022-01-11 19:24:09
-
📌 对于乐观锁,作者认为数据发生并发冲突的概率比较小,所以读操作之前不上锁。等到写操作的时候,再判断数据在此期间是否被其他线程修改了。 ^31186368-15-1550-1615
- ⏱ 2022-01-11 19:24:11
-
📌 一个线程拿不到锁的时候,有以下两种基本的等待策略。策略1:放弃CPU,进入阻塞状态,等待后续被唤醒,再重新被操作系统调度。策略2:不放弃CPU,空转,不断重试,也就是所谓的“自旋”。 ^31186368-15-3816-3965
- ⏱ 2022-01-11 19:24:14
2.3 AtomicStampedReference和AtomicMarkableReference
- 📌 如果另外一个线程把变量的值从A改为B,再从B改回到A,那么尽管修改过两次,可是在当前线程做CAS操作的时候,却会因为值没变而认为数据没有被其他线程修改过,这就是所谓的ABA问题。要解决ABA 问题,不仅要比较“值”,还要比较“版本号” ^31186368-17-631-777
- ⏱ 2022-01-11 19:24:07
2.6 Striped64与LongAdder
-
📌 把一个Long型拆成一个base变量外加多个Cell,每个Cell包装了一个Long型变量。当多个线程并发累加的时候,如果并发度低,就直接加到base变量上;如果并发度高,冲突大,平摊到这些Cell上。在最后取值的时候,再把base和这些Cell求sum运算。 ^31186368-20-1270-1400
- ⏱ 2022-01-11 19:20:00
-
📌 缓存与主内存进行数据交换的基本单位叫Cache Line(缓存行)。在64位x86架构中,缓存行是64字节,也就是8个Long型的大小。 ^31186368-20-2908-2976
- ⏱ 2022-01-11 19:57:42
-
📌 如图2-4所示,主内存中有变量X、Y、Z(假设每个变量都是一个Long型),被CPU1和CPU2分别读入自己的缓存,放在了同一行Cache Line里面。当CPU1修改了X变量,它要失效整行Cache Line ^31186368-20-3037-3254
- ⏱ 2022-01-11 20:19:35
-
📌 Y、Z变量的数据没有修改,本应该很好地被CPU1和CPU2共享,却没做到,这就是所谓的“伪共享问题”。 ^31186368-20-3879-3965
- ⏱ 2022-01-11 20:19:45
-
📌 要解决这个问题,需要用到所谓的“缓存行填充”,分别在X、Y、Z后面加上7个无用的Long型,填充整个缓存行,让X、Y、Z处在三行不同的缓存行中 ^31186368-20-4111-4350
- ⏱ 2022-01-11 20:20:55
第3章 Lock与Condition
-
📌 “可重入锁”是指当一个线程调用object.lock()拿到锁,进入互斥区后,再次调用object.lock(),仍然可以拿到该锁。很显然,通常的锁都要设计成可重入的,否则就会发生死锁。 ^31186368-21-714-807
- ⏱ 2022-01-11 20:37:51
-
📌 默认为非公平的。 ^31186368-21-2581-2589
- ⏱ 2022-01-11 20:43:25
-
📌 一个新的线程来了之后,看到有很多线程在排队,自己排到队伍末尾,这叫公平;线程来了之后直接去抢锁,这叫作不公平。 ^31186368-21-2943-2998
- ⏱ 2022-01-11 20:44:10
-
📌 列同步器(AQS) ^31186368-21-3272-3281
- ⏱ 2022-01-11 20:51:25
-
📌 unpark(Thread t),它实现了一个线程对另外一个线程的“精准唤醒”。前面讲到的wait()/notify(),notify也只是唤醒某一个线程,但无法指定具体唤醒哪个线程。 ^31186368-21-4931-5023
- ⏱ 2022-01-11 20:56:46
3.2 读写锁
-
📌 读写锁也是用state变量来表示锁状态的。 ^31186368-22-2109-2130
- ⏱ 2022-01-12 13:48:26
-
📌 把state 变量拆成两半,低16位,用来记录写锁。 ^31186368-22-2405-2431
- ⏱ 2022-01-12 13:48:50
3.4 StampedLock
- 📌 StampedLock引入了“乐观读”策略,读的时候不加读锁,读出来发现数据被修改了,再升级为“悲观读”,相当于降低了“读”的地位,把抢锁的天平往“写”的一方倾斜了一下,避免写线程被饿死。 ^31186368-24-1210-1304
- ⏱ 2022-01-12 19:18:33
5.1 BlockingQueue
- 📌 add(..)和offer(..)的区别不大,当队列为满的时候,前者会抛出异常,后者则直接返回false。 ^31186368-32-1350-1403
- ⏱ 2022-01-12 21:31:23
5.5 ConcurrentHashMap
-
📌 在JDK7中,一个HashMap被拆分为多个子HashMap。每一个子HashMap称作一个Segment,多个线程操作多个Segment相互独立 ^31186368-36-718-791
- ⏱ 2022-01-12 22:37:22
-
📌 如果头节点是Node类型,则尾随它的就是一个普通的链表;如果头节点是TreeNode类型,它的后面就是一颗红黑树,TreeNode是Node的子类。链表和红黑树之间可以相互转换:初始的时候是链表,当链表中的元素超过某个阈值时,把链表转换成红黑树;反之,当红黑树中的元素个数小于某个阈值时,再转换为链表。 ^31186368-36-7286-7466
- ⏱ 2022-01-12 22:56:44
-
📌 也就是把tab[i]位置的链表或红黑树重新组装成两部分,一部分链接到nextTab[i]的位置,一部分链接到nextTab[i+n]的位置,如图5-11所示。然后把tab[i]的位置指向一个ForwardingNode节点。 ^31186368-36-15137-15249
- ⏱ 2022-01-12 23:13:37
5.6 ConcurrentSkipListMap/Set
-
📌 在Java的util包中,有一个非线程安全的HashMap,也就是TreeMap,是key有序的,基于红黑树实现。而在Concurrent包中,提供的key有序的HashMap,也就是ConcurrentSkipListMap,是基于SkipList(跳查表)来实现的。 ^31186368-37-794-958
- ⏱ 2022-01-12 23:27:11
-
📌 图5-17所示,把节点10的删除分为两2步:第一步,把节点10的next指针,mark成删除,即软删除;第二步,找机会,物理删除。 ^31186368-37-3101-3224
- ⏱ 2022-01-13 01:01:33
-
📌 做标记之后,当线程再往节点10后面插入节点20的时候,便可以先进行判断,节点10是否已经被删除,从而避免在一个删除的节点10后面插入节点20。这个解决方法有一个关键点:“把节点10的next指针指向节点20(插入操作)”和“判断节点10本身是否已经删除(判断操作)”,必须是原子的,必须在1个CAS操作里面完成! ^31186368-37-3502-3833
- ⏱ 2022-01-13 01:02:47
-
📌 我们的目的是标记节点10已经删除,也就是标记它的next字段。那么可以新造一个marker节点,使节点10的next指针指向该Marker节点。这样,当向节点10的后面插入节点20的时候,就可以在插入的同时判断节点10的next指针是否执行了一个Marker节点,这两个操作可以在一个CAS操作里面完成。 ^31186368-37-4123-4323
- ⏱ 2022-01-13 01:04:22
6.3 ThreadPoolExector
-
📌 shutdown()和shutdownNow()的区别:(1)前者不会清空任务队列,会等所有任务执行完成,后者再清空任务队列。(2)前者只会中断空闲的线程,后者会中断所有线程。 ^31186368-40-5498-5644
- ⏱ 2022-01-13 12:16:34
-
📌 tryLock()如果调用成功,说明线程处于空闲状态,向其发送中断信号 ^31186368-40-6185-6220
- ⏱ 2022-01-13 12:19:13
6.4 Callable与Future
-
📌 [插图] ^31186368-41-876-877
- ⏱ 2022-01-13 12:23:35
-
📌 图6-6 FutureTask对象的线程同步示意图 ^31186368-41-2612-2637
- ⏱ 2022-01-13 12:30:33
7.3 工作窃取队列
-
📌 工作窃取算法,是指一个Worker线程在执行完毕自己队列中的任务之后,可以窃取其他线程队列中的任务来执行,从而实现负载均衡,以防有的线程很空闲,有的线程很忙。 ^31186368-46-816-895
- ⏱ 2022-01-13 13:37:45
-
📌 多个线程同时在读写这个队列,如何实现在不加锁的情况下一边读写、一边扩容呢?通过分析工作窃取队列的特性,我们会发现:在queueBase一端,是多线程访问的,但它们只会使queueBase变大,也就是使队列中的元素变少。所以队列为满,一定发生在queueTop一端,对queueTop进行累加的时候,这一端却是单线程的! ^31186368-46-2104-2292
- ⏱ 2022-01-13 14:04:24
-
📌 扩容之后的新数组还是空的时候,就已经赋给了成员变量queue。而queueTop、queueBase的值是不变的,这意味着,其他窃取线程若此时来窃取任务,取到的将全是null,即取不到任务。不过,虽然此时窃取不到,可以阻塞一会儿,待扩容完成就可以窃取到了,不会影响整个算法的正确性。 ^31186368-46-3074-3215
- ⏱ 2022-01-13 14:08:29
-
📌 (1)Worker线程自己,在队列头部,通过对queueTop指针执行加、减操作,实现入队或出队,这是单线程的。(2)其他Worker线程,在队列尾部,通过对queueBase进行累加,实现出队操作,也就是窃取,这是多线程的,需要通过CAS操作。正因为如此,在上面的数据结构定义中,queueTop不是volatile的,queueBase 是volatile类型。 ^31186368-46-1241-1482
- ⏱ 2022-01-13 14:11:02
7.7 工作窃取算法:任务的执行过程分析
-
📌 所以,对于写线程而言,发现sequence number是奇数,就不能修改共享数据了。对于读线程而言,发现sequence number是奇数,也不能再读取数据;如果发现sequence number是偶数,那么在读取数据前后分别读取一次sequence number,如果两次的值相同,则读取成功,否则重新读取。 ^31186368-50-2439-2596
- ⏱ 2022-01-13 14:42:04
-
📌 如果sequence number是奇数,说明当前某个写线程正在修改数据,其他写线程被互斥了。 ^31186368-50-2363-2410
- ⏱ 2022-01-13 14:42:28
-
📌 通过一个sequence number来控制对共享数据的访问,具体来说,就是: ^31186368-50-1978-2017
- ⏱ 2022-01-13 14:42:54
-
📌 顺序锁 ^31186368-50-1941-1944
- ⏱ 2022-01-13 14:43:00
-
📌 (1)读线程在读取共享数据之前先读取sequence number,在读取数据之后再读一次sequence number,如果两次的值不同,说明在此期间有其他线程修改了数据,此次读取数据无效,重新读取;(2)写线程,在写入数据之前,累加一次sequence number,在写入数据之后,再累加一次sequence number。 ^31186368-50-2046-2240
- ⏱ 2022-01-13 14:43:14
7.9 ForkJoinPool的优雅关闭
- 📌 shutdown()只拒绝新提交的任务;shutdownNow()会取消现有的全局队列和局部队列中的任务,同时唤醒所有空闲的线程,让这些线程自动退出。 ^31186368-52-2934-3009
- ⏱ 2022-01-13 14:51:09
8.4 CompletableFuture内部原理
- 📌 CompletableFuture中任务的执行同样依靠ForkJoinPool, ^31186368-57-606-646
- ⏱ 2022-01-13 15:09:49
