出现并发问题的三个源头:
- 缓存导致多线程之间的可见性问题
- 线程之间切换带来的原子性问题
时间片:操作系统允许某个线程执行一小段时间,例如50毫秒,过了50毫秒操作系统就会重新选择一个线程来执行(我们称为“任务切换”),这个50毫秒称为“时间片”。
image
任务切换的时机大多数是在时间片结束的时候,我们现在基本都使用高级语言编程,高级语言里一条语句往往需要多条CPU指令完成,例如上面代码中的count += 1,至少需要三条CPU指令。
指令1:首先,需要把变量count从内存加载到CPU的寄存器;
指令2:之后,在寄存器中执行+1操作;
指令3:最后,将结果写入内存(缓存机制导致可能写入的是CPU缓存而不是内存)。
操作系统做任务切换,可以发生在任何一条CPU指令执行完,是的,是CPU指令,而不是高级语言里的一条语句。
- 编译优化带来的有序性问题
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
这看上去一切都很完美,无懈可击,但实际上这个getInstance()方法并不完美。问题出在哪里呢?出在new操作上,我们以为的new操作应该是:
1、分配一块内存M;
2、在内存M上初始化Singleton对象;
3、然后M的地址赋值给instance变量。
但是实际上优化后的执行路径却是这样的:
1、分配一块内存M;
2、将M的地址赋值给instance变量;
3、最后在内存M上初始化Singleton对象。
当然可以对instance进行volatile语义声明,就可以禁止指令重排序,避免该情况发生.但是这样又会出现volatile的写入屏障问题,也就是所谓happen-before问题。
什么是Java内存模型?Java如何解决可见性和有序性问题?
Java内存模型是个很复杂的规范,Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及八项 Happens-Before 规则。
Happens-Before 规则
很多网文也都爱按字面意思翻译成“先行发生”,
它真正要表达的是:前面一个操作的结果对后续操作是可见的。
//示例参考代码
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里x会是多少呢?
}
}
}
- 程序的顺序性规则
- volatile变量规则
- 传递性
这条规则是指如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C
image
- 管程中锁的规则
这条规则是指对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
管程是一种通用的同步原语,在Java中指的就是synchronized,synchronized是Java里对管程的实现。管程中的锁在Java里是隐式实现的.如下:
synchronized (this) { //此处自动加锁
// x是共享变量,初始值=10
if (this.x < 12) {
this.x = 12;
}
} //此处自动解锁
- 线程 start() 规则
它是指主线程A启动子线程B后,子线程B能够看到主线程在启动子线程B前的操作。
换句话说就是,如果线程A调用线程B的 start() 方法(即在线程A中启动线程B),那么该start()操作 Happens-Before 于线程B中的任意操作。
- 线程 join() 规则
如果在线程A中,调用线程B的 join() 并成功返回,那么线程B中的任意操作Happens-Before 于该 join() 操作的返回,线程A都能看到B线程的任意操作。
- 线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生
- 对象终结规则:
一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
Java内存模型底层怎么实现的?
主要是通过内存屏障(memory barrier)禁止重排序的,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。对于编译器而言,内存屏障将限制它所能做的重排序优化。而对于处理器而言,内存屏障将会导致缓存的刷新操作。比如,对于volatile,编译器将在volatile字段的读写操作前后各插入一些内存屏障。 [