|
| 1 | +--- |
| 2 | +title: Java 并发编程的艺术笔记 |
| 3 | +date: 2018/05/09 |
| 4 | +categories: |
| 5 | +- java |
| 6 | +tags: |
| 7 | +- java |
| 8 | +- javase |
| 9 | +- concurrent |
| 10 | +--- |
| 11 | + |
| 12 | +# Java 并发编程的艺术笔记 |
| 13 | + |
| 14 | +<!-- TOC depthFrom:2 depthTo:3 --> |
| 15 | + |
| 16 | +- [第 1 章 Java 并发编程的挑战](#第-1-章-java-并发编程的挑战) |
| 17 | + - [1.1 上下文切换](#11-上下文切换) |
| 18 | + - [1.2 死锁](#12-死锁) |
| 19 | + - [1.3 资源限制的挑战](#13-资源限制的挑战) |
| 20 | +- [第 2 章 Java 并发机制的底层实现原理](#第-2-章-java-并发机制的底层实现原理) |
| 21 | + - [2.1 volatile](#21-volatile) |
| 22 | + - [2.2 synchronized](#22-synchronized) |
| 23 | + - [2.3 原子操作的实现原理](#23-原子操作的实现原理) |
| 24 | +- [第 3 章 Java 内存模型](#第-3-章-java-内存模型) |
| 25 | + - [3.1 Java 内存模型的基础](#31-java-内存模型的基础) |
| 26 | + - [3.2 重排序](#32-重排序) |
| 27 | + - [3.3 顺序一致性](#33-顺序一致性) |
| 28 | +- [第 4 章 多线程技术的好处](#第-4-章-多线程技术的好处) |
| 29 | +- [第 5 章 Java 并发包中与锁相关的 API 和组件](#第-5-章-java-并发包中与锁相关的-api-和组件) |
| 30 | +- [第 6 章 Java 并发容器](#第-6-章-java-并发容器) |
| 31 | +- [第 7 章 Java 原子操作类](#第-7-章-java-原子操作类) |
| 32 | +- [第 8 章 Java 并发工具类](#第-8-章-java-并发工具类) |
| 33 | +- [第 9 章 Java 线程池](#第-9-章-java-线程池) |
| 34 | +- [第 10 章 Executor 框架](#第-10-章-executor-框架) |
| 35 | +- [第 11 章 Java 并发编程实战](#第-11-章-java-并发编程实战) |
| 36 | + |
| 37 | +<!-- /TOC --> |
| 38 | + |
| 39 | +## 第 1 章 Java 并发编程的挑战 |
| 40 | + |
| 41 | +### 1.1 上下文切换 |
| 42 | + |
| 43 | +* 即使是单核处理器也支持多线程执行代码,CPU 通过给每个线程分配 CPU 时间片来实现这个机制。时间片是 CPU 分配给各个线程的时间,因为时间片非常短,所以 CPU 通过不停地切换线程执行,让我们感觉多个线程是同时执行的,时间片一般是几十毫秒(ms)。 |
| 44 | +* CPU 通过时间片分配算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保存上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以任务从保存到再加载的过程就是一次上下文切换。 |
| 45 | +* 上下文切换也会影响多线程的执行速度。 |
| 46 | + * 并发执行**不一定**比串行执行快。因为线程有创建和上下文切换的开销。 |
| 47 | +* 减少上下文切换的方法 |
| 48 | + * 无锁并发编程 - 多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的 ID 按照 Hash 算法取模分段,不同的线程处理不同段的数据。 |
| 49 | + * CAS 算法 - Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。 |
| 50 | + * 使用最少线程 - 避免创建不需要的线程,比如任务很少,但是创建了很多线程来处理,这样会造成大量线程都处于等待状态。 |
| 51 | + * 使用协程 - 在单线程里实现多任务的调度,并在单线程里维持多个任务间的切换。 |
| 52 | + |
| 53 | +### 1.2 死锁 |
| 54 | + |
| 55 | +* 什么是死锁 |
| 56 | + * 多个线程互相等待对方释放锁。 |
| 57 | +* 避免死锁的方法 |
| 58 | + * 避免一个线程同时获取多个锁。 |
| 59 | + * 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。 |
| 60 | + * 尝试使用定时锁,使用 lock.tryLock(timeout)来替代使用内部锁机制。 |
| 61 | + * 对于数据库锁,加锁和解锁必须在一个数据库连接里,否则会出现解锁失败的情况。 |
| 62 | + |
| 63 | +### 1.3 资源限制的挑战 |
| 64 | + |
| 65 | +* 什么是资源限制 |
| 66 | + * 资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。 |
| 67 | +* 资源限制引发的问题 |
| 68 | + * 在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。 |
| 69 | +* 如何解决资源限制的问题 |
| 70 | + * 对于硬件资源限制,可以考虑使用集群并行执行程序。 |
| 71 | + * 对于软件资源限制,可以考虑使用资源池将资源复用。 |
| 72 | +* 在资源限制情况下进行并发编程 |
| 73 | + * 根据不同的资源限制调整程序的并发度 |
| 74 | + |
| 75 | +## 第 2 章 Java 并发机制的底层实现原理 |
| 76 | + |
| 77 | +Java 中所使用的并发机制依赖于 JVM 的实现和 CPU 的指令。 |
| 78 | + |
| 79 | +### 2.1 volatile |
| 80 | + |
| 81 | +* volatile 是轻量级的 synchronized,它在多处理器开发中保证了共享变量的“可见性”。 |
| 82 | +* 可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。 |
| 83 | +* volatile 比 synchronized 的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。 |
| 84 | +* 如果一个字段被声明成 volatile,Java 线程内存模型确保所有线程看到这个变量的值是一致的。 |
| 85 | +* volatile 的两条实现原则 |
| 86 | + * Lock 前缀指令会引起处理器缓存回写到内存。 |
| 87 | + * 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。 |
| 88 | + |
| 89 | +### 2.2 synchronized |
| 90 | + |
| 91 | +* synchronized 实现同步的基础是:Java 中的每一个对象都可以作为锁。 |
| 92 | + * 对于普通同步方法,锁是当前实例对象。 |
| 93 | + * 对于静态同步方法,锁是当前类的 Class 对象。 |
| 94 | + * 对于同步方法块,锁是 Synchonized 括号里配置的对象。 |
| 95 | +* synchronized 用的锁是存在 Java 对象头里的。 |
| 96 | + |
| 97 | +### 2.3 原子操作的实现原理 |
| 98 | + |
| 99 | +* 原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。 |
| 100 | +* 在 Java 中可以通过锁和循环 CAS 的方式来实现原子操作。 |
| 101 | + |
| 102 | +## 第 3 章 Java 内存模型 |
| 103 | + |
| 104 | +### 3.1 Java 内存模型的基础 |
| 105 | + |
| 106 | +* 并发编程模型的两个关键问题 |
| 107 | + * 线程之间如何通信 |
| 108 | + * 线程之间如何同步 |
| 109 | +* 线程之间的通信机制 |
| 110 | + * 共享内存 - 线程之间共享程序的公共状态,通过读写内存中的公共状态进行隐式通信。 |
| 111 | + * 消息传递 - 线程之间没有公共状态,线程之间必须通过发送消息来显式进行通信。 |
| 112 | +* 线程之间的同步 |
| 113 | + * 同步是指程序中用于控制不同线程间操作发生相对顺序的机制。 |
| 114 | + * 在共享内存并发模型里,同步是显式进行的。 |
| 115 | + * 在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。 |
| 116 | +* Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。 |
| 117 | +* Java 内存模型的抽象结构 |
| 118 | + * 在 Java 中,所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。 |
| 119 | + * 局部变量(Local Variables),方法定义参数(Java 语言规范称之为 Formal Method Parameters)和异常处理器参数(Exception Handler Parameters)不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。 |
| 120 | + * Java 线程之间的通信由 Java 内存模型控制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见。 |
| 121 | +* 从源代码到指令序列的重排序 |
| 122 | + * 重排序类型 |
| 123 | + * 编译器优化的重排序 |
| 124 | + * 指令级并行的重排序 |
| 125 | + * 内存系统的重排 |
| 126 | +* Java 源代码到最终实际执行的指令序列的执行流程 |
| 127 | +* 源代码 > 编译器优化的重排序 > 指令级并行的重排序 > 内存系统的重排 > 最终执行的指令序列 |
| 128 | +* 现代的处理器使用写缓冲区临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。 |
| 129 | +* happens-before 规则 |
| 130 | + * 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。 |
| 131 | + * 监视器锁规则:对一个锁的解锁,happens-before 于随后对这个锁的加锁。 |
| 132 | + * volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的 |
| 133 | + * 读。 |
| 134 | + * 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。 |
| 135 | + * 两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行。 |
| 136 | + |
| 137 | +### 3.2 重排序 |
| 138 | + |
| 139 | +* 重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。 |
| 140 | +* 数据依赖性 - 如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。 |
| 141 | +* as-if-serial 语义 - 不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime 和处理器都必须遵守 as-if-serial 语义。 |
| 142 | + * 为了遵守 as-if-serial 语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。 |
| 143 | +* 程序顺序规则:A > B 且 B > C,所以 A > C。 |
| 144 | + |
| 145 | +### 3.3 顺序一致性 |
| 146 | + |
| 147 | +* 数据竞争:在一个线程中写一个变量,在另一个线程读同一个变量,而且写和读没有通过同步来排序。 |
| 148 | +* 如果程序是正确同步的,程序的执行将具有顺序一致性。 |
| 149 | +* 顺序一致性内存模型有两大特性 |
| 150 | + * 一个线程中的所有操作必须按照程序的顺序来执行。 |
| 151 | + * (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。 |
| 152 | +* 同步程序的顺序一致性效果 |
| 153 | + * 顺序一致性模型中,所有操作完全按程序的顺序串行执行。 |
| 154 | + * 在 JMM 中,临界区内的代码可以重排序(但 JMM 不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。 |
| 155 | +* 未同步程序的执行特性 |
| 156 | + * 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而 JMM 不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。 |
| 157 | + * 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而 JMM 不保证所有线程能看到一致的操作执行顺序。 |
| 158 | + * JMM 不保证对 64 位的 long 型和 double 型变量的写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。 |
| 159 | +* 每次处理器和内存之间的数据传递都是通过一系列步骤来完成的,这一系列步骤称之为总线事务(Bus Transaction)。 |
| 160 | +* 总线事务包括读事务(Read Transaction)和写事务(Write Transaction)。 |
| 161 | + |
| 162 | +## 第 4 章 多线程技术的好处 |
| 163 | + |
| 164 | +## 第 5 章 Java 并发包中与锁相关的 API 和组件 |
| 165 | + |
| 166 | +## 第 6 章 Java 并发容器 |
| 167 | + |
| 168 | +## 第 7 章 Java 原子操作类 |
| 169 | + |
| 170 | +## 第 8 章 Java 并发工具类 |
| 171 | + |
| 172 | +## 第 9 章 Java 线程池 |
| 173 | + |
| 174 | +## 第 10 章 Executor 框架 |
| 175 | + |
| 176 | +## 第 11 章 Java 并发编程实战 |
0 commit comments