您的位置:首页 > 编程语言 > Java开发

深入理解Java并发1——Java内存模型和volatile型变量

2017-03-07 21:21 971 查看
一 概念

(1)Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量(线程共享的)存储到内存和从内存中取出变量这样的底层细节。 

(2)Java内存模型规定了所有的变量都存储在主内存 ,每条线程还有自己的工作内存 ,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、 赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。

(3)不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成 。



图片来源于《深入理解Java虚拟机》

二 内存间交互操作

Java内存模型中定义了以下8种操作 完成内存间交互(加粗显示的为作用于主内存),每一种操作都是原子的、 不可再分的 :

(1)lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
(2)unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
(3)read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
(4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
(5)use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行           这个操作。
(6)assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这            个操作。
(7)store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
(8)write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。

关于上述操作由一些了规定,间《深入理解Java虚拟机》393页。

三  volatile型变量

关键字volatile是Java虚拟机提供的最轻量级的同步机制,当一个变量定义为volatile之后,它将具备两种特性


3.1 保证此变量对所有线程的可见性

这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。 而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。volatile变量也是如此 ,但它保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新 。

所以,volatile变量在各个线程的工作内存中不存在一致性问题(在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题),但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全的 。

public class VolatileTest{
public static volatile int race=0;
public static void increase(){
race++;
}
private static final int THREADS_COUNT=10;
public static void main(String[]args){
Thread[]threads=new Thread[THREADS_COUNT];
for(int i=0;i<THREADS_COUNT;i++){
threads[i]=new Thread(new Runnable(){
public void run(){
for(int i=0;i<1000;i++){
increase();
}
}
});
threads[i].start();
}
while(Thread.activeCount()>1)//等待所有累加线程都结束再继续主线程
Thread.yield();
System.out.println(race);//某次输出为9976
}
}


上述代码正常执行的话,输出应为10000,但实际结果却是不确定的,我们将上述代码反汇编一下(javap -c VolatileTest):

其中increase()方法指令如下:

public static void increase();
Code:
0: getstatic     #2                  // Field race:I
3: iconst_1
4: iadd
5: putstatic     #2                  // Field race:I
8: return
包括四条指令,指令0取得race的值,volatile保证值在此时是正确的,但由于上述四条指令并非原子操作,所以在执行下边指令时,其他线程可能已经修改了race的值,当前线程对race执行加1后,其它线程立即得知该值,而此时真正的race可能已经被操作过,但这些操作都将被掩盖,该线程通过指令5把race的值写入主内存,如此便导致了错误。

既然volatile存在上述问题,那应该何时用呢?

volatile boolean shutdownRequested;
public void shutdown(){
shutdownRequested=true;
}
public void doWork(){
while(!shutdownRequested){
//do stuff
}
}


当运算结果并不依赖与变量的当前值时,即可用,如上述代码,shutdown()方法被调用时,能保证所有线程中执行的doWork()方法都立即停下来。

3.2 禁止指令重排序优化

普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致

如下所示实现单例模式的代码:

public class Singleton{
private volatile static Singleton instance;
public static Singleton getInstance(){
if(instance==null){
synchronized(Singleton.class){
if(instance==null){
instance=new Singleton();
}
}
}
return instance;
}
public static void main(String[]args){
Singleton.getInstance();
}
}
其反汇编为机器码后如下:

0x01a3de0f:mov$0x3375cdb0,%esi;……beb0cd75 33
;{oop('Singleton')}
0x01a3de14:mov%eax,0x150(%esi);……89865001 0000
0x01a3de1a:shr$0x9,%esi;……c1ee09
0x01a3de1d:movb$0x0,0x1104800(%esi);……c6860048 100100
0x01a3de24:lock addl$0x0,(%esp);……f0830424 00
;*putstatic instance
;-
Singleton:getInstance@24

与没有加volatile关键字的代码相比,多了一条lock指令,lock指令在多核处理器下会引发两件事,(1)将当前工作内存中的数据写回到主内存;(2)该写回操作会使其他线程工作内存中缓存的该内存地址的数据无效。该操作相当于一个内存屏障,指令重排序时不能把后面的指令重排序到内存屏障之前的位置,若,允许指令重排会发生什么呢?

instance=new Singleton();语句new了一个Singleton实例,new 一个实例的过程如下:

(1)检查类Singleton是否已被加载。

(2)为对象分配内存空间。

(3)初始化对象内存空间为零值。

(4)设置对象头。

(5)执行<init>初始化按程序员的意愿初始化对象。

(5)让栈中的instance指向堆中刚分配的内存

如果允许指令重排的话,步骤(5)将可以放在步骤(2)之后的任何一步执行!假设线程1执行到步骤(2)后执行步骤(5),此时若线程2执行

if(instance==null)将返回false,所以当前未被初始化的对象将被返回给线程2,线程2拿到了一个不完整的实例!虽然线程1和线程2都指向同样的实例,但如果线程1被阻塞在第(2)步,而线程2顺利执行完,并接着执行后续操作,将会使用这个不完整的实例去进行后续操作,导致错误。所以此处必须加volatile关键字禁止指令重排序。(以上仅为个人想法并不知道真理是不是如此)。

四 Java内存模型的特征

4.1 原子性

由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的 。

Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块——synchronized关键字,因此在synchronized块

之间的操作也具备原子性。

4.2 可见性

Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的。

除了volatile之外,Java还有两个关键字能实现可见性,即synchronized(同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中这条规则获得的)和final。  

4.3 有序性

Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字通过禁止指令重排序获得有序性,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。 

4.4 总结

由上述分析知synchronized关键字在需要这3种特性的时候都可以作为其中一种的解决方案 。

 

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
相关文章推荐