并发编程-java内存模型

 2019-12-10 15:44  阅读(797)
文章分类:Java Core

文/属衣

20191210001256\_1.png

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不为空而直接返回并在使用中发生错误。

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> 并发编程-java内存模型

相关推荐