现在的位置: 首页 > 综合 > 正文

探索并发编程(三)——Java存储模型和共享对象

2014年03月24日 ⁄ 综合 ⁄ 共 1962字 ⁄ 字号 评论关闭

很多程序员对一个共享变量初始化要注意可见性和安全发布(安全地构建一个对象,并其他线程能正确访问)等问题不是很理解,认为Java是一个屏蔽内存细节的平台,连对象回收都不需要关心,因此谈到可见性和安全发布大多不知所云。其实关键在于对Java存储模型,可见性和安全发布的问题是起源于 Java的存储结构。

Java存储模型原理

有很多书和文章都讲解过Java存储模型,其中一个图很清晰地说明了其存储结构:

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

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

Java存储模型潜在的问题

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

双击代码全选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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原则,有人整理得非常好,我摘抄如下:

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

有了原则还不够,Java提供了以下工具和方法来保证变量的可见性和安全发布:

  • 使用 synchronized来同步变量初始化。此方式会立马把工作内存中的变量同步到主内存中
  • 使用 volatile关键字来标示变量。此方式会直接把变量存在主存中而不是工作内存中
  • final变量。常量内也是存于主存中

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

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

共享对象的可见性

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

双击代码全选

抱歉!评论已关闭.