并发编程核心

  • 分工, 如何高效的拆解任务, 并分配给线程
  • 同步, 线程之间如何协作, 一个线程执行完了任务, 如何通知执行后续任务的线程开工

线程协作问题: 当某个条件不满足时, 线程需要等待; 当某个条件满足时, 线程需要被唤醒执行

  • 互斥, 即线程安全, 保证同一时刻只允许一个线程去访问共享资源

并发理论基础(一) - 图1


可见性, 原子性, 有序性

缓存导致的可见性问题

CPU执行速度>>内存读写速度>>I/O设备读写速度
因此程序整体的性能取决于最慢的操作—I/O设备

为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:

  • CPU 增加了缓存,以均衡与内存的速度差异;
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。

一个线程对共享变量的修改, 另外一个线程能够立刻看到, 我们称之为可见性.

如图, 多核CPU导致线程A对变量V的操作对线程B不可见
image.png

线程切换带来的原子性问题

一个或多个操作在CPU执行的过程中不被中断的特性称为原子性
CPU能保证的原子操作是CPU指令级别的, 而不是高级语言操作符

如图, 线程切换, 使得线程AB都执行了count+=1语句, 最后得到的结果还是1
image.png

编译优化带来的有序性问题

例子: 双重检查创建单例对象

  1. public class Singleton {
  2. static Singleton instance;
  3. static Singleton getInstance(){
  4. if (instance == null) {
  5. synchronized(Singleton.class) {
  6. if (instance == null)
  7. instance = new Singleton();
  8. }
  9. }
  10. return instance;
  11. }
  12. }

image.png
解决方法: 使用volatile关键字修饰instance禁止重排序

Java内存模型

解决可见性, 有序性==>按需禁用缓存以及编译优化

volatile 禁用缓存以及编译优化


Happens-Before规则

前面一个操作的结果对于后续操作是可见的

1.程序的顺序性规则
这条规则是指在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作
2.volatile 变量规则
对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。
3.传递性
如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。

示例:如果A线程调用writer方法, B线程执行reader方法, 那x会是多少?
规则1使得第7行happens-before第8行
规则2使得第8行happens-before第11行
规则3使得第7行happens-before第11行, 因此最终拿到的x会是42
image.png

  1. // 以下代码来源于【参考1】
  2. class VolatileExample {
  3. int x = 0;
  4. volatile boolean v = false;
  5. public void writer() {
  6. x = 42;
  7. v = true;
  8. }
  9. public void reader() {
  10. if (v == true) {
  11. // 这里x会是多少呢?
  12. }
  13. }
  14. }

4. 管程中锁的规则
对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
管程是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。
5. 线程 start() 规则
主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

  1. Thread B = new Thread(()->{
  2. // 主线程调用B.start()之前
  3. // 所有对共享变量的修改,此处皆可见
  4. // 此例中,var==77
  5. });
  6. // 此处对共享变量var修改
  7. var = 77;
  8. // 主线程启动子线程
  9. B.start();

6. 线程 join() 规则
这条是关于线程等待的。它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。
换句话说就是,如果在线程 A 中,调用线程 B 的 join() 并成功返回,那么线程 B 中的任意操作 Happens-Before 于该 join() 操作的返回。具体可参考下面示例代码。

  1. Thread B = new Thread(()->{
  2. // 此处对共享变量var修改
  3. var = 66;
  4. });
  5. // 例如此处对共享变量修改,
  6. // 则这个修改结果对线程B可见
  7. // 主线程启动子线程
  8. B.start();
  9. B.join()
  10. // 子线程所有对共享变量的修改
  11. // 在主线程调用B.join()之后皆可见
  12. // 此例中,var==66

final

final 修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化

但要避免”逸出”, 在下面例子中,在构造函数里面将 this 赋值给了全局变量 global.obj,这就是“逸出”,线程通过 global.obj 读取 x 是有可能读到 0 的

  1. // 以下代码来源于【参考1】
  2. final int x;
  3. // 错误的构造函数
  4. public FinalFieldExample() {
  5. x = 3;
  6. y = 4;
  7. // 此处就是讲this逸出,
  8. global.obj = this;
  9. }