`
zhhphappy
  • 浏览: 120080 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Java 内存可见性与volatile

    博客分类:
  • java
 
阅读更多

在多核系统中,处理器一般有一层或者多层的缓存,这些的缓存通过加速数据访问(因为数据距离处理器更近)和降低共享内存在总线上的通讯(因为本地缓存能够满足许多内存操作)来提高CPU性能。如图:处理器的多层缓存模型



 JVM需要实现跨平台的支持,它需要有一套自己的同步协议来屏蔽掉各种底层硬件和操作系统的不同,因此就引入了Java内存模型JMM

JMMJava Memory Model)主要是为了规定了线程和内存之间的一些关系。系统存在一个主内存(Main Memory)Java中所有变量都储存在主存中,对于所有线程都是共享的。每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成。如图:Java内存模型



在java内存模型中,共享变量会在多线程中存在可见性的问题。如下面代码中的例子:
private static boolean ready
…..
Thread1,2,3…
while (!ready) {
  // do something unready…
}
// do something ready
……
Thread x….
ready = true;
 当Thread x中设置ready = true时,会将该值写入工作内存,并同步到主内存,但其他线程有可能还是读取到自己工作内存中缓存的老数据,从而导致其他线程可能看不到ready=true而不跳出循环,以上就是一个典型的java内存可见性问题。
 
当然java内存模型也定义了一系列解决可见性(工作内存和主内存交互协议)方法,包括volatile,synchronized,锁等方式,这里主要用volatile来说明可见性问题。
先看volatile的定义:
 java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。

调整前面示例代码中共享变量为volatile并写出完整的测试代码:

public class VisibilityDemo {

    private static volatile boolean ready;

    static class Reader extends Thread {
        @Override
        public void run() {
            long tryTimes = 0L;
            while (!ready) {
                ++tryTimes;
            }
            System.out.println("ready! try times : " + tryTimes);
        }
    }

    static class Writer extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 100000; i++) {
                ready = true;
            }
        }
    }

    public static void main(String[] args) throws Exception{
        new Reader().start();

        Thread.sleep(100L);

        new Writer().start();
    }
}

 加了volatile后Reader线程能成功退出并打印出tryTimes。

用javap –c –l –s –verbose VisibilityDemo 查看增加volatile前后的字节码没有区别,直接看JIT运行时汇编码:

环境:

~$ file /sbin/init
/sbin/init: ELF 64-bit LSB  shared object, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=7d9cc5d4d6cb68aede9400492a7c5942c55c7598, stripped
~$ java -version
java version "1.7.0_55"
Java(TM) SE Runtime Environment (build 1.7.0_55-b13)
Java HotSpot(TM) 64-Bit Server VM (build 24.55-b03, mixed mode)

 打印JIT汇编码:

Java -client -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly VisibilityDemo > ./ VisibilityDemo.assemblycode

 

0x00007f02c8c3b2d6: mov    %sil,0x70(%r10)
0x00007f02c8c3b2da: lock addl $0x0,(%rsp)     ;*putstatic ready

 

 有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情:
将当前处理器缓存行的数据会写回到系统内存。
这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
 
处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。
 
Lock前缀指令会引起处理器缓存回写到内存。Lock前缀指令导致在执行指令期间,声言处理器的 LOCK# 信号。在多处理器环境中,LOCK# 信号确保在声言该信号期间,处理器可以独占使用任何共享内存。(因为它会锁住总线,导致其他CPU不能访问总线,不能访问总线就意味着不能访问系统内存),但是在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销比较大。但在P6和最近的处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反地,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。

 

 

参考文章:http://ifeve.com/volatile/

分析生成的汇编代码 http://blog.csdn.net/hengyunabc/article/details/26898657

  • 大小: 19.4 KB
  • 大小: 21 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics