java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的 Java 程序设计语言和 Java 平台(即 JavaEE(j2ee), JavaME(j2me), JavaSE(j2se))的总称。
这篇文章主要介绍了 Java 存储模型和共享对象详解的相关资料, 对 Java 存储模型,可见性和安全发布的问题是起源于 Java 的存储结构及共享对象安全,需要的朋友可以参考下
Java 存储模型和共享对象详解
很多程序员对一个共享变量初始化要注意可见性和安全发布(安全地构建一个对象,并其他线程能正确访问)等问题不是很理解,认为 Java 是一个屏蔽内存细节的平台,连对象回收都不需要关心,因此谈到可见性和安全发布大多不知所云。其实关键在于对 Java 存储模型,可见性和安全发布的问题是起源于 Java 的存储结构。
Java 存储模型原理
有很多书和文章都讲解过 Java 存储模型,其中一个图很清晰地说明了其存储结构:
由上图可知, 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() {
- 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 原则,有人整理得非常好,我摘抄如下:
有了原则还不够,Java 提供了以下工具和方法来保证变量的可见性和安全发布:
另外,一定要明确只有共享变量才会有以上那些问题,如果变量只是这个线程自己使用,就不用担心那么多问题了
搞清楚 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,但其实际结果会非常奇怪,可能会永远没有输出(因为 ready 为 false),可能会输出 0(因为重排序问题导致 ready=true 先执行)。再举一个更为常见的例子,大家都喜欢用只有 set 和 get 方法的 pojo 来设计领域模型,如下所示:
- @NotThreadSafe
- public class MutableInteger {
- private int value;
- public int get() { return value; }
- public void set(int value) { this.value = value; }
- }
但是,当有多个线程同时来存取某一个对象时,可能就会有类似的可见性问题。
为了保证变量的可见性,一般可以用锁、 synchronized 关键字、 volatile 关键字或直接设置为 final
共享变量发布
共享变量发布和我们常说的发布程序类似,就是说让本属于内部的一个变量变为一个可以被外部访问的变量。发布方式分为以下几种:
安全发布和保证可见性的方法类似,就是要同步发布动作,并使发布后的对象可见。
线程安全
其实当我们把这些变量封闭在本线程内访问,就可以从根本上避免以上问题,现实中存在很多例子通过线程封闭来安全使用本不是线程安全的对象,比如:
来源: http://www.phperz.com/article/17/1224/358154.html