内存模型描述的是程序中各变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的低层细节.
不同平台间的处理器架构将直接影响内存模型的结构. 在C或C++中, 可以利用不同操作平台下的内存模型来编写并发程序. 但是, 这带给开发人员的是, 更高的学习成本. 内存模型的特征: Java存储模型原理(优点类似于缓存) 根据Java Language Specification中的说明, jvm系统中存在一个主内存(Main Memory或Java Heap Memory),Java中所有变量都储存在主存中,对于所有线程都是共享的。 每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝,线程对所有变量的操作都是在工作内存中进行,线程之间无法相互直接访问,变量传递均需要通过主存完成: 由上图可知, jvm系统中存在一个主内存(Main Memory或Java 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() { } });
one.start(); other.start(); one.join(); other.join();
System.out.println("( "+ x + "," + y + ")"); } } 由于,变量x,y,a,b没有安全发布,导致会不以规定的操作顺序来执行这次四次赋值操作,有可能出现以下顺序:
出现这个问题也可以理解,因为既然这些对象不可见,也就是说本应该隔离在各个线程的工作区内,那么对于有些无关顺序的指令,打乱顺序执行在JVM看来也是可行的。 因此,总结起来,会有以下两种潜在问题: · 缓存不一致性 · 重排序执行 · 解决Java存储模型潜在的问题 为了能让开发人员安全正确地在Java存储模型上编程,JVM提供了一个happens-before原则。 a, 在程序顺序中, 线程中的每一个操作, 发生在当前操作后面将要出现的每一个操作之前.
为了实现 happends-before ordering原则, java及jdk提供的工具: 另外,一定要明确只有共享变量才会有以上那些问题,如果变量只是这个线程自己使用,就不用担心那么多问题了 搞清楚Java存储模型后,再来看共享对象可见性和安全发布的问题就较为容易了 共享对象的可见性 当对象在从工作内存同步到主内存之前,那么它就是不可见的。若有其他线程在存取不可见对象就会引发可见性问题,看下面一个例子: public class NoVisibility {
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) {
number = 42; ready = true; } } 按照正常逻辑,应该会输出42,但其实际结果会非常奇怪,可能会永远没有输出(因为ready为false),可能会输出0(因为重排序问题导致ready=true先执行)。再举一个更为常见的例子,大家都喜欢用只有set和get方法的pojo来设计领域模型,如下所示: @NotThreadSafe
public int get() { return value; } public void set(int value) { this.value = value; } } 但是,当有多个线程同时来存取某一个对象时,可能就会有类似的可见性问题。 为了保证变量的可见性,一般可以用锁、 synchronized关键字、 volatile关键字或直接设置为final。
这边最核心的就是第二步, 他同步了主内存,即前一个线程对变量改动的结果,可以被当前线程获知!(利用了happens-before ordering原则) 对比之前的例子 |
|