分享

jmm ---java存储模型

 碧海山城 2010-10-08

内存模型描述的是程序中各变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的低层细节.

 

 

不同平台间的处理器架构将直接影响内存模型的结构.

CC++, 可以利用不同操作平台下的内存模型来编写并发程序. 但是, 这带给开发人员的是, 更高的学习成本.
相比之下, java利用了自身虚拟机的优势, 使内存模型不束缚于具体的处理器架构, 真正实现了跨平台.
(
针对hotspot jvm, jrockit等不同的jvm, 内存模型也会不相同)

内存模型的特征
a, Visibility
可视性 (多核,多线程间数据的共享)
b, Ordering
有序性 (对内存进行的操作应该是有序的)

 

Java存储模型原理(优点类似于缓存)

根据Java Language Specification中的说明, jvm系统中存在一个主内存(Main MemoryJava Heap Memory)Java中所有变量都储存在主存中,对于所有线程都是共享的。

每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成:

由上图可知, jvm系统中存在一个主内存(Main MemoryJava Heap Memory)Java中所有变量都储存在主存中,对于所有线程都是共享的。 每条线程都有自己的工作内存(Working Memory)工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成

这个存储模型很像我们常用的缓存与数据库的关系,因此由此可以推断JVM如此设计应该是为了提升性能,提高多线程的并发能力,并减少线程之间的影响。

 

Java存储模型潜在的问题

一谈到缓存 我们立马想到会有缓存不一致性问题,就是说当有缓存与数据库不一致的时候,就需要有相应的机制去同步数据。同理,Java存储模型也有这个问题,当一个线程在自己工作内存里初始化一个变量,当还没来得及同步到主存里时,如果有其他线程来访问它,就会出现不可预知的问题。另外,JVM在底层设计上,对与那些没有同步到主存里的变量,可能会以不一样的操作顺序来执行指令,举个实际的例子:

public class PossibleReordering {

static int x = 0, y = 0;

    static int a = 0, b = 0;

public static void main(String[] args)throws InterruptedException {

 Thread one = new Thread(new Runnable() {
        public void run() {

           a = 1;
           
x = b;
         
}
       
});

       
 Thread other = new Thread(new Runnable() {
            public void run() {

                b = 1;

                y = a;

      }

});

 

one.start();

other.start();

one.join();  

other.join();

 

 System.out.println("( "+ x + "," + y + ")");

 }

}

由于,变量x,y,a,b没有安全发布,导致会不以规定的操作顺序来执行这次四次赋值操作,有可能出现以下顺序:



出现这个问题也可以理解,因为既然这些对象不可见,也就是说本应该隔离在各个线程的工作区内,那么对于有些无关顺序的指令,打乱顺序执行在
JVM看来也是可行的。

因此,总结起来,会有以下两种潜在问题:

·         缓存不一致性

·         重排序执行

·          

解决Java存储模型潜在的问题

为了能让开发人员安全正确地在Java存储模型上编程,JVM提供了一个happens-before原则。

 

a, 在程序顺序中, 线程中的每一个操作, 发生在当前操作后面将要出现的每一个操作之前.
b,
对象监视器的解锁发生在等待获取对象锁的线程之前.
c,
volitile关键字修饰的变量写入操作, 发生在对该变量的读取之前.
d,
对一个线程的 Thread.start() 调用 发生在启动的线程中的所有操作之前.
e,
线程中的所有操作 发生在从这个线程的 Thread.join()成功返回的所有其他线程之前.

 

为了实现 happends-before ordering原则, java及jdk提供的工具:
a, synchronized关键字
b, volatile关键字
c, final变量
d, java.util.concurrent.locks包(since jdk 1.5)
e, java.util.concurrent.atmoic包(since jdk 1.5)

 

 

另外,一定要明确只有共享变量才会有以上那些问题,如果变量只是这个线程自己使用,就不用担心那么多问题了

搞清楚Java存储模型后,再来看共享对象可见性和安全发布的问题就较为容易了

 

共享对象的可见性

当对象在从工作内存同步到主内存之前,那么它就是不可见的。若有其他线程在存取不可见对象就会引发可见性问题,看下面一个例子:

public class NoVisibility {


    private static boolean ready;

private static int number;

 

private static class ReaderThread extends Thread {

public void run() {

while (!ready)

Thread.yield();

System.out.println(number);

}

}

 

public static void main(String[] args) {


        new ReaderThread().start();

number = 42;

ready = true;

}

}

按照正常逻辑,应该会输出42,但其实际结果会非常奇怪,可能会永远没有输出(因为readyfalse),可能会输出0(因为重排序问题导致ready=true先执行)。再举一个更为常见的例子,大家都喜欢用只有setget方法的pojo来设计领域模型,如下所示:

 

@NotThreadSafe

public class MutableInteger {


    private int value;

 

public int  get() { return value; }

public void set(int value) { this.value = value; }

 

}

但是,当有多个线程同时来存取某一个对象时,可能就会有类似的可见性问题。

为了保证变量的可见性,一般可以用锁、 synchronized关键字、 volatile关键字或直接设置为final




这边最核心的就是第二步
, 他同步了主内存,即前一个线程对变量改动的结果,可以被当前线程获知!(利用了happens-before ordering原则)

对比之前的例子
如果多个线程同时执行一段未经锁保护的代码段,很有可能某条线程已经改动了变量的值,但是其他线程却无法看到这个改动,依然在旧的变量值上进行运算,最终导致不可预料的运算结果。

 

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多