文/属衣
Java内存模型的抽象结构示意图
Java内存模型包含主内存和工作内存(本地内存),所有的变量都存储在主内存中。线程对变量的所有操作(读取,赋值)都必须在工作内存中进行。
A线程与B线程的通信过程:1)线程A把本地内存共享变量刷新到主内存 2)B线程读取A线程已更新过的共享变量
但实际情况,A线程与B线程的通信会出现“脏读”等问题。
1.原子性
原子性即一个操作或者多个操作要么全部成功执行,要么都不执行。
int i=10;① i–;② j=i; ③
只有语句①是原子性,其余需要先读取变量再进行赋值,所以是非原子性。
2.可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
int i=10;① j=i;②
线程A在执行语句①,并未将赋值后的结果刷新到主内存,线程B进行语句②操作时,导致读取到旧值,无法保证可见性。
3.有序性
有序性即程序执行的顺序按照代码的先后顺序执行。一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。
线程A:
context = loadContext();
①
inited =
true
;
②
线程B:
while
(!inited ){
sleep()
}
initConfig(context);
语句①
②
可能会被重排序,假如线程A先执行了语句②
,会导致线程B直接跳过while循环,导致
initConfig(context)
失败! Java内存模型具备一些先天的“有序性”,即happens-before原则(先行发生原则)。
volatile
volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。
1.使用volatile修饰的共享变量(类的成员变量、类的静态成员变量),保证了可见性与有序性(禁止进行指令重排序)。
线程A:
boolean
stop =
false
;
while
(!stop){
doSomething();
}
线程B:
stop =
true
;
以上代码可能出现死循环!线程B先将stop变量读到工作内存进行修改,然后一直没有将stop修改后的结果刷新到主内存,导致线程A 一直处于while循环中。 使用volatile修饰时,线程B的stop变量会被立即刷新到主内存,线程A会及时退出while循环。
2.volatile保证不了原子性。
public
class
Test {
public
volatile
int
inc =
0
;
public
void
increase() {
inc++;
}
public
static
void
main(String[] args) {
final
Test test =
new
Test();
for
(
int
i=
0
;i<
10
;i++){
new
Thread(){
public
void
run() {
for
(
int
j=
0
;j<
1000
;j++)
test.increase();
};
}.start();
}
while
(Thread.activeCount()>
1
)
//保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
大部分执行结果小于10000! 这是因为
increase
不是原子性的,假如线程1执行时inc=10,线程1先将inc读取到工作内存(此时保证读取到的是最新结果),此时被阻塞。线程2将inc读取到工作内存并进行+1操作,然后放入主内存inc=11,线程1接着进行inc+1操作(inc已经被读取过了inc=10),所以会将inc=11放入主内存。 可以选择java.util.concurrent.atomic包下的原子操作类保证原子性。
volatile的实现原理
1.可见性
处理器为了提高处理速度,不直接和内存进行通讯,而是将系统内存的数据独到内部缓存后再进行操作,但操作完后不知什么时候会写到内存。
如果对声明了volatile变量进行写操作时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了如果有其他线程对声明了volatile变量进行修改,则立即更新主内存中数据。
但这时候其他处理器的缓存还是旧的,所以在多处理器环境下,为了保证各个处理器缓存一致,每个处理会通过嗅探在总线上传播的数据来检查 自己的缓存是否过期,当处理器发现自己缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作时,会强制重新从系统内存把数据读到处理器缓存里。 这一步确保了其他线程获得的声明了volatile变量都是从主内存中获取最新的。
2.有序性
Lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成。
volatile的应用场景
1.状态标记
2.单例double check
class
Singleton{
private
volatile
static
Singleton instance =
null
;
private
Singleton() {
}
public
static
Singleton getInstance() {
if
(instance==
null
) {
synchronized
(Singleton.
class
) {
if
(instance==
null
)
instance =
new
Singleton();
}
}
return
instance;
}
}
instance = new Singleton();在jvm中经历了: 1)为instance分配内存 2)初始化成员变量 3)将instance对象指向分配的内存空间 在jvm的即时编译器中存在指令排序的优化,有可能出现1-3-2的顺序,当3执行完毕,另一个线程直接判断instance不为空而直接返回并在使用中发生错误。