并发编程核心
- 分工, 如何高效的拆解任务, 并分配给线程
- 同步, 线程之间如何协作, 一个线程执行完了任务, 如何通知执行后续任务的线程开工
线程协作问题: 当某个条件不满足时, 线程需要等待; 当某个条件满足时, 线程需要被唤醒执行
- 互斥, 即线程安全, 保证同一时刻只允许一个线程去访问共享资源
可见性, 原子性, 有序性
缓存导致的可见性问题
CPU执行速度>>内存读写速度>>I/O设备读写速度
因此程序整体的性能取决于最慢的操作—I/O设备
为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为:
- CPU 增加了缓存,以均衡与内存的速度差异;
- 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;
- 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。
一个线程对共享变量的修改, 另外一个线程能够立刻看到, 我们称之为可见性.
线程切换带来的原子性问题
一个或多个操作在CPU执行的过程中不被中断的特性称为原子性
CPU能保证的原子操作是CPU指令级别的, 而不是高级语言操作符
如图, 线程切换, 使得线程AB都执行了count+=1语句, 最后得到的结果还是1
编译优化带来的有序性问题
例子: 双重检查创建单例对象
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
解决方法: 使用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
// 以下代码来源于【参考1】
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里x会是多少呢?
}
}
}
4. 管程中锁的规则
对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
管程是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。
5. 线程 start() 规则
主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
Thread B = new Thread(()->{
// 主线程调用B.start()之前
// 所有对共享变量的修改,此处皆可见
// 此例中,var==77
});
// 此处对共享变量var修改
var = 77;
// 主线程启动子线程
B.start();
6. 线程 join() 规则
这条是关于线程等待的。它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。
换句话说就是,如果在线程 A 中,调用线程 B 的 join() 并成功返回,那么线程 B 中的任意操作 Happens-Before 于该 join() 操作的返回。具体可参考下面示例代码。
Thread B = new Thread(()->{
// 此处对共享变量var修改
var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程B可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用B.join()之后皆可见
// 此例中,var==66
final
final 修饰变量时,初衷是告诉编译器:这个变量生而不变,可以可劲儿优化
但要避免”逸出”, 在下面例子中,在构造函数里面将 this 赋值给了全局变量 global.obj,这就是“逸出”,线程通过 global.obj 读取 x 是有可能读到 0 的
// 以下代码来源于【参考1】
final int x;
// 错误的构造函数
public FinalFieldExample() {
x = 3;
y = 4;
// 此处就是讲this逸出,
global.obj = this;
}