多线程的相关概念

volatile关键字

1.保证线程可见性;(MESI; 本质上是使用了CPU的缓存一致性协议)
2.禁止指令重排序;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public class T01_HelloVolatile {
/*volatile*/ boolean running = true;
void m() {
System.out.println("m start");
while(running) {
}
System.out.println("m end!");
}

public static void main(String[] args) {
T01_HelloVolatile t = new T01_HelloVolatile();

new Thread(t::m, "t1").start();

try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}

t.running = false;
}
}

// 加上synchronized
public class T05_VolatileVsSync {
/*volatile*/ int count = 0;

synchronized void m() {
for (int i = 0; i < 10000; i++)
count++;
}

public static void main(String[] args) {
T05_VolatileVsSync t = new T05_VolatileVsSync();

List<Thread> threads = new ArrayList<Thread>();

for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}

threads.forEach((o) -> o.start());

threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});

System.out.println(t.count);
}
}

###Volatile不能保证原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class T04_VolatileNotSync {
volatile int count = 0;
void m() {
for(int i=0; i<10000; i++) count++;
}

public static void main(String[] args) {
T04_VolatileNotSync t = new T04_VolatileNotSync();

List<Thread> threads = new ArrayList<Thread>();

for(int i=0; i<10; i++) {
threads.add(new Thread(t::m, "thread-"+i));
}

threads.forEach((o)->o.start());

threads.forEach((o)->{
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println(t.count);
}
}

// 加上synchronized
public class T05_VolatileVsSync {
/*volatile*/ int count = 0;

synchronized void m() {
for (int i = 0; i < 10000; i++)
count++;
}

public static void main(String[] args) {
T05_VolatileVsSync t = new T05_VolatileVsSync();

List<Thread> threads = new ArrayList<Thread>();

for (int i = 0; i < 10; i++) {
threads.add(new Thread(t::m, "thread-" + i));
}

threads.forEach((o) -> o.start());

threads.forEach((o) -> {
try {
o.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});

System.out.println(t.count);
}
}

###流水线技术
在CPU工作中汇编指令分多步完成,每一部涉及到的硬件可能不同,于是有了流水线技术来执行指令。
没有流水线技术前,如果同时两个指令过来执行 一个需要5秒,那么两个就需要10秒;有了流水线技术之后,可能就只要6秒。多个指令同时执行时性能显著提升。
这个和指令重排有啥关系?
因为流水线满载后一旦中断,所有的硬件设备会进入一个停顿期,再次满载,需要几个周期,对性能损失比较大。指令重排就是一种减少流水线中断的技术。
流水线技术并不是说让多个指令并行执行,可能还是需要等他其他指令执行完才可以执行,那么这个时候等待就有一个停顿,我们可以让和这个指令后面不相干的指令继续执行,这就是指令重排。
指令重排有可能带来一个问题——乱序
乱序也必须保证程序上下文的因果关系不发生改变,如果无法保证,那么就应该遵守hapen-before原则,不能指令重排。

###指令重排序的好处

###指令重排序的分类
1.编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
2.指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。
如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3.内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

###编译期重排序的好处
CPU计算的时候要访问值,如果常常利用到寄存器中已有的值就不用去内存读取了

###处理器为啥要重排序
因为一个汇编指令也会涉及到很多步骤,每个步骤可能会用到不同的寄存器,现在的CPU一般采用流水线来执行指令,也就是说,CPU有多个功能单元(如获取、解码、运算和结果),一个指令的执行被分成:取指、译码、访存、执行、写回、等若干个阶段,流水线是并行的, 第一条指令执行还没完毕,就可以执行第二条指令,前提是这两条指令功能单元相同或类似,所以一般可以通过指令重排使得具有相似功能单元的指令接连执行来减少流水线中断的情况

###as-if-serial 语义
as-if-serial的意思是:不管指令怎么重排序,在单线程下执行结果不能被改变。不管是编译器级别还是处理器级别的重排序都必须遵循as-if-serial语义。

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。但是as-if-serial规则允许对有控制依赖关系的指令做重排序,因为在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果,但是多线程下确有可能会改变结果。

###happens-before 语义
JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个 操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一 个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。

###happens-before 部分规则
程序顺序规则: 一个线程中的每个操作,happens-before于该线程中的任意后续操作。
主要含义是:在一个线程内不管指令怎么重排序,程序运行的结果都不会发生改变。和as-if-serial 比较像。

监视器锁规则: 对一个锁的解锁,happens-before于随后对这个锁的加锁。
主要含义是:同一个锁的解锁一定发生在加锁之后

管程锁定规则: 一个线程获取到锁后,它能看到前一个获取到锁的线程所有的操作结果。
主要含义是:无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)

volatile变量规则: 对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
主要含义是:如果一个线程先去写一个volatile变量,然后另一个线程又去读这个变量,那么这个写操作的结果一定对读的这个线程可见。

传递性: 如果A happens-before B,且B happens-before C,那么A happens-before C。

start()规则: 如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
主要含义是:线程A在启动子线程B之前对共享变量的修改结果对线程B可见。

join()规则: 如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
主要含义是:如果在线程A执行过程中调用了线程B的join方法,那么当B执行完成后,在线程B中所有操作结果对线程A可见。

线程中断规则: 对线程interrupt方法的调用happens-before于被中断线程的代码检测到中断事件的发生。
主要含义是:响应中断一定发生在发起中断之后。

对象终结规则: 就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。

一个happens-before规则对应于一个或多个编译器和处理器重排序规则。

as-if-serial和happens-before的主要作用都是:在保证不改变程序运行结果的前提下,允许部分指令的重排序,最大限度的提升程序执行的效率。

###内存屏障

###如何解决指令重排序带来的问题
一种是使用内存屏障(volatile)
另一种使用临界区(synchronized )

如果我们使用内存屏障,那么JMM的处理器,会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为 Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

###内存屏障的类型
LoadLoad屏障:
抽象场景:Load1; LoadLoad; Load2
Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:
抽象场景:Store1; StoreStore; Store2
Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见

LoadStore屏障:
抽象场景:Load1; LoadStore; Store2
在Store2被写入前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障:
抽象场景:Store1; StoreLoad; Load2
在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂 贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。

###临界区synchronized
JMM使用了临界区(加锁)来保证程序的顺序执行,但是在临界区内是允许出现指令重排的(JMM不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。

###volatile的内存语义
可见性: 对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入
原子性: 对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性(++不具备原子性,volatile直接赋值具备原子性)

###volatile写和读的内存语义
volatile写的内存语义: 当写一个volatile变量时,JMM会把该线程对应的本地内存中的所有共享变量值刷新到主内存
volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取所有共享变量

###volatile内存语义的实现
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序
当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile写之前的操作不会被编译器重排序到volatile写之后。
当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile读之后的操作不会被编译器重排序到volatile读之前。
当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

###具体插入的内存屏障
在每个volatile写操作的前面插入一个StoreStore屏障。在每个volatile写操作的后面插入一个StoreLoad屏障。
在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。

###锁的内存语义
当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A 对共享变量所做修改的)消息。

当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共 享变量所做修改的)消息。

线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

###对象半初始化
双重检查锁,在前一个线程获取到锁进行对象初始化的时候,对象初始化的正确顺序(1:分配对象的内存空间;
2:初始化对象;3:设置instance指向刚分配的内存地址), 此时发生了指令重排序,先将对象引用指向了内存地址,还没有初始化
象但是此时对象还没有赋值,对象引用已经指向了对象的内存地址,这时候其他线程检查对象是否为null的时候,
发现对象不为null,此时其他线程使用的是这个半初始化状态的对象从而引发一些问题;

###final的内存语义
1.在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。也就是说只有将对象实例化完成后,才能将对象引用赋值给变量。
2.初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。也就是下面示例的4和5不能重排序。
3.当final域为引用类型时,在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

###final语义在处理器中的实现
会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore障屏。
读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。