1. 主内存与工作内存
Java线程之间通信的话,必须经过2个步骤
- 1)线程A把本地内存中更新过的共享变量刷新到主内存中去
- 2)线程B到主内存中去读取线程A之前更新过的共享变量
2.从源码到指令序列的重排序
- 2.1:编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
- 2.2:指令级并行重排序:现在处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应及其指令的执行顺序。
- 2.3:内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行。
这些重排序可能会导致多线程程序出现内存可见性问题。对于编译器(2.1),JMM的编译器规则会禁止特定类型的编译器重排序。对于处理器的重排序(2.2和2.3),JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Fence)指令来禁止特定类型的处理器重排序。
3.as-if-serial 和 happens-before
语义的意思是:不管怎么重排序,单线程程序的执行结果不能被改变
PS:为了遵循该语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果
语义规则:
- 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
- 监视器锁规则:对一个锁的解锁,happens-before于随后对这种锁的枷锁。
- volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
- 传递性:如果A happens-before B,且B happens-before C, 那么A happens-before C。
- start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
- join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
的关系定义:
- 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
- 两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种操作也是被允许的(即:不是按照程序员编写的代码顺序执行的),。
:
- as-if-serial语义的编译器、runtime和处理器为编写单线程程序员创建了一个幻境:单线程程序是按程序的顺序来执行的
- happens-before语义给编写正确同步的多线程程序的程序员创造一个幻境:正确同步的多线程程序是按happens-before指定的顺序来执行的
4.volatile的内存语义
- 线程A写一个volatile变量,实质上是线程A向接下来将要读这个volatile变量的某个线程发出(其对共享变量所做修改的)消息。
- 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息
- 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息
PS:参照缓存一致性协议:https://www.jianshu.com/p/690b0e4baf61
4.1.volatile内存语义的实现
- 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile之后
- 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之后。
- 当第一个操作是volatile写时,第二个操作是volatile读时,不能重排序。
JMM采取保守策略,如下
- 在每个volatile写操作的前面插入一个StoreStore屏障。
- 在每个volatile写操作的后面插入一个StoreLoad屏障。
- 在每个volatile读操作的后面插入一个LoadLoad屏障。
- 在每个volatile读操作的后面插入一个LoadStore屏障。
5.锁的内存语义
- 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息
- 线程B获取一个锁,实质上是线程B接收了之前某个线程发出(在释放这个锁之前对共享变量所做修改的)消息
- 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息
//锁释放-获取的示例代码
class MonitorExample {
int a = 0;
public synchronized void writer(){ //1
a++; //2
} //3
public synchronized void reader(){ //4
int i = a; //5
....... //6
}
}
1)根据程序次序规则,1 happens-before 2, 2 h-b 3, 4 h-b 5, 5 h-b 6
2)根据监视器锁规则, 3 h-b 4
3)根据h-b的传递性, 2 h-b 5
//未使用监视器
class MonitorExample {
int a = 0;
public void writer(){ //1
a++; //2
Thread.sleep(10); //睡眠10毫秒,等待reader()的执行
} //3
public void reader(){ //4
int i = a; //5
if(i == 0){ //6
System.out.println("当前i值为" + i + ",当前主内存中a值" + a);
}
}
public static void main(String[] args){
for(;;){
MonitorExample me = new MonitorExample();
Thread t1 = new Thread(() ->{
me.writer();
});
Thread t2 = new Thread(() ->{
me.reader();
});
t1.setPriority(10);//优先让writer()方法执行
t1.start();
t2.setPriority(1);
t2.start();
}
}
}
//出现不可见问题:当前i值为0,当前主内存a值为1
6.final域的内存语义
6.1 final域的重排序规则
对于final域,编译器和处理器要遵守两个重排序规则
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序
- 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序
6.2 final语义在处理器中的实现(以X86为例) - 写final域的重排序规则会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore屏障。读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。
synchronized和volatile最终都是使用lock执行
synchronzied使用lock cmpxchg
valatile使用lock add$空指令
CAS
最终实现:cmpxchg
多核情况下会多添加lock指令,即 lock cmpxchg
硬件层面(intel):
- 确保对内存的读-改-写操作原子执行,带lock前缀的指令在执行期间会使用缓存锁定来保证指令执行的原子性,PS:锁住一个北桥信息(电信号,新版的处理器不采用锁总线的方式)
- 禁止该指令,与之前和之后的读写指令重排序
- 把写缓冲区中的所有数据刷新到内存中
参考书籍:Java并发编程的艺术(方腾飞)