比尔云BierYun--阿里云最新优惠活动
阿里云优惠码丨阿里云代金券

Java内存模型

Java内存模型http://www.bieryun.com/1129.html

本博客为《深入理解java虚拟机》的学习笔记,所以大部分内容来自此书,另外一部分内容来自网络其他博客和源码分析。

主要内容探讨以下问题:

Ø  Java内存模型、协议、规则。

Ø  volatile的可见性和禁止指令重排序是什么意思?

Ø  Synchronized是如何做到线程安全的?

Ø  先行发生原则。

 

一  Java内存模型

1       模型

Java内存逻辑模型如下:

9be4c5b2263d64a0d71ccf4fa0a6e21752ac0f79

所有变量都存储在主内存中。

每个线程都有自己的工作内存,工作内存中保存了线程使用到的主内存中变量的副本。

线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接操作主内存。

不同线程之间无法访问对方的工作内存。

线程之间的值传递均需通过主内存来完成。

 

2       协议

操作

解释

作用域

说明

lock 锁定 主内存 把一个变量表示为一个线程独占的状态
unlock 解锁 主内存 把一个变量从线程独占的状态释放出来,释放后的变量才能被其他线程锁定
read 读取 主内存 把一个变量从主内存传输到工作内存中
load 载入 工作内存 把read操作的变量值放入工作内存的变量副本中。
use 使用 工作内存 把一个工作内存的变量传递给执行引擎,当虚拟机遇到一个需要使用变量值的字节码指令时会执行此操作
assign 赋值 工作内存 把从执行引擎收到的值赋给工作内存的变量,当虚拟机遇到一个给变量赋值的字节码指令时会执行此操作。
store 存储 工作内存 把一个变量的值传到主内存中
write 写入 主内存 把store操作的值放到主内存的变量中

如果需要将一个变量从主内存复制到工作内存,就需要顺序的执行read、load;如果需要讲一个变量从工作内存写回到主内存,就需要顺序的执行store、write。Java内存模型要求了这两对命令的顺序,但不要求其连续,即在read和load之间、store和write允许插入其他指令。

 

3       规则

不允许read和load、store和write单独出现。即不允许一个变量从主内存读取了但工作内存不接受的情况;不允许从工作内存回写了但主内存不接受的情况。

不允许一个线程丢弃掉它最近的assign操作。即变量在工作内存中修改之后,必须同步回主内存中。

不允许一个线程无原因的把数据从线程的工作内存同步回主内存。即对变量没有执行assgin操作则不能回写到主内存。

一个新的变量只能在主内存中创建,不允许在工作内存中直接使用一个违背初始化过的变量。即对一个变量use前必须load;对一个变量store前必须assign。

一个变量在同一时刻只允许一个线程对其进行lock操作,但一个线程可以多次执行lock操作,多次执行lock操作以后,只有执行相同次数的unlock,变量才被解锁。

如果一个线程没有被lock操作锁定,那么不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁住的变量。

对一个变量执行unlock操作前,必须先把此变量同步回主内存,即先执行store、write操作。

 

从上面的规则我们可以看到:因为一个变量同一时刻只有一个线程能对其进行lock操作,在unlock前必须将变量同步会主内存,所以使用lock可以保证并发情况下数据安全。

 

4       long和double的非原子协定

虚拟机允许没有被volatile修饰的64位数据的多些操作划分成2次32位操作进行,即允许虚拟机实现对64位的long、double的read、load、store、write不保证原子性,即long和double的非原子协定。

目前商用虚拟机基本上都对long、double保证原子操作。

 

 

二  volatile

1       volatile变量的特性

使用volatile修饰的变量具有两种特性:可见性、禁止指令重排序优化。

1)       可见性

可见性指一个线程修改了这个变量值,新值对其他线程来说是可以立即得知的;普通变量做不到这一点。注意这一点并不意味着使用volatile修饰的变量是线程安全。

 

2)       禁止指令重排序优化

普通的变量仅仅保证在执行过程中,所有依赖赋值结果的地方都能获取正确的结果,而不保证变量赋值操作的顺序与代码中的执行顺序一致,这就是java内存模型中的“线程内表现为串行的语义”;而使用volatile可以实现此点。

单例模式下,如果不使用volatile修饰,通过双重检查锁创建对象,并发场景中可能出现问题,具体见后面的分析。

 

2       volatile变量的特殊规则

说明:因为觉得原文中对于volatile规则的描述不好理解,所以我在这里换了一种描述方式,所以如果发现这里的描述和虚拟机规范不同,请不必疑惑。

假设T表示一个线程,V、W表示两个volatile类型的变量,那么拥有以下规则:

Ø  每次使用volatile修饰的变量前,必须先从主内存中获取最新的值

线程T对变量V的use动作和线程T对变量V的read、load的动作可以认为是相关联的,必须连续一起出现。即线程T对V的前一个动作是load时,线程T才能对变量V执行use操作;如果线程T对V的后一个动作是use时,线程T才能对变量V执行load操作。

此规则要求在工作内存中,每次使用V前必须先从主内存中刷新最新的值,用于保证能看到其他线程对变量V修改后的值。

 

Ø  每次使用volatile修饰的变量后,必须立即同步回主内存

线程T对变量V的assign动作和线程T对变量V的store、write的动作可以认为是相关联的,必须连续一起出现。即线程T对V的前一个动作是assign时,线程T才能对变量V执行store操作;如果线程T对V的后一个动作是store时,线程T才能对变量V执行assign操作。

此规则要求在工作内存中,每次使用V后必须立即同步回主内存,用于保证其他线程能看到当前线程对变量V的值所做的修改。

 

Ø  代码执行顺序和程序的顺序相同

假定动作UV是线程T对变量V执行的use动作,动作RV是与之相关联的read动作;假定动作UW是线程T对变量W的use动作,动作RW是与之相关联的read动作;如果UV先于UW,那么RV先于RW。

假定动作AV是线程T对变量V执行的assign动作,动作WV是与之相关联的write动作;假定动作AW是线程T对变量W的assign动作,动作WW是与之相关联的write动作;如果AV先于AW,那么WV先于WW。

此规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同

 

3       示例

a)       volalite修饰的变量不是线程安全的

以下示例代码输出的结果不会为100000;基本上都会比此值略小。

public class VolaliteTester {
 
    private static volatile int value;
 
    private static void inc() {
        value++;
    }
 
    public staticvoid main(String[] args) {
        int threadCount= 10;
        final int times = 10000;
 
        Thread[] threads= new Thread[threadCount];
        for (int i = 0; i < threadCount;i++) {
           Threadthread = new Thread(newRunnable() {
               @Override
               public void run() {
                   for (int j = 0; j < times;j++) {
                       inc();
                   }
               }
           });
           threads[i] = thread;
           thread.start();
        }
 
        if (Thread.activeCount() >1) {
           Thread.yield();
        }
        System.err.println("value=" + value);
    }
}

 

b)       双重检查锁失效

单例模式下创建实例对象时,可能出现双重检查锁失效的情况,即以下示例代码可能会创建多个实例instance对象。

public class Singleton {
    private static volatile Singleton instance;
 
   private Singleton() {
       super();
   }
 
   public staticSingleton getInstance() {
       if (instance == null){
           synchronized(Singleton.class) {
                // 如果在等待synchronized结束前已经有线程创建Instance则直接忽略。
               if(instance == null){
                   instance = newSingleton();
               }
           }
       }
       return instance;
   }
}

 

 

在执行instance = new Singleton()语句时,实际上分为分配内存、调用构造函数、instance指向分配的内存地址三个步骤。如下伪代码所示:

memery =allocate();    //为对象分配内存
ctorSingleton(memery);  //调用构造函数实例化对象
instance = memery;     //将instance指向新分配的内存

但是实际上有些虚拟机进行指令重排序以后会变成如下顺序(虚拟机的内存模型以及协议规则均没有限制不能进行这种操作)。

memery =allocate();    //为对象分配内存
instance = memery;      //将instance指向新分配的内存,注意此时instance为not null,但是此时对象并未实例化,如果此时执行非空判断,将返回true。
ctorSingleton(memery);  //调用构造函数实例化对象

 

三  原子性、可见性、有序性

1       原子性(Atomicity)

java内存模型直接对变量的read、load、use、assign、store、write操作的原子性(long、double的非原子协定基本是例外,但基本不会遇到)

通过synchronized关键字实现lock、unlock操作,保证同一时间段内只有一个线程访问同步快,所以可以实现代码块的原子性。

 

2       可见性(Visibility)

java内存模型是通过变量使用前从主内存读取、变量修改后将值同步回主内存来实现可见性的。

volalite的可见性是由:修改后的新值立即同步到主内存,使用前立即从主内存中读取新值这个规则决定的。volatite保证了多线程操作时变量的可见性,而普通变量却不行。

Synchronized的可见性是由:在unlock前必须将变量先同步到主内存这个规则决定的。

final的可见性是由:在构造函数中初始化后,不会将this的引用传递出去,以后将无法修改此值这个规则决定的。

 

3       有序性(Ordering)

如果在本线程内观察,所有操作都是有序的;如果在一个线程观察另外一个线程,所有操作都是无序的。前半句指:线程内变形为串行的语义;后半句指:指令重排序闲现象和工作内存与主内存同步延迟现象。

Volatile本身就有禁止指令重排序的语义,所以可以保证有序性。

Synchronized的有序性是由:同一时刻只允许一个线程对其进行lock操作这个规则决定的,这决定了synchronized的语句块只能串行进入,所以可以保证有序性。

 

四  先行发生原则

以下是java内存模型提供的“天然”的先行发生关系,这些先行发生关系不需任何同步协助就已经存在。如果两个操作之间的关系不在此列,并且无法通过这些规则推导出来,那么他们就没有顺序保证,虚拟机可能对他们随意的进行重排序。

1.      程序次序规则(Program Order Rule)

在一个线程中,按照代码顺序,书写在前的操作先行发生于书写在后的操作。确切的说,应该是控制流顺序而不是书写顺序,例如分支、循环机构。

 

2.      管程锁定规则(Monitor Lock Rule)

一个unlock操作先行发生于后面对同一个锁的lock操作。后面值的是时间上的先后顺序。

 

3.      volatile变量规则(Volatile Rule)

对一个volatile变量的写操作先行发生于后面对这个变量的读操作。。后面值的是时间上的先后顺序。

 

4.      线程启动规则(Thread Start Rule)

Thread对象的start方法先行发生于对此线程的每一个动作。

 

5.      线程终止规则(Thread Termination Rule)

线程中的所有操作都先行发生于对此项承德终止检测.可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到县城已经终止执行。

 

6.      线程中断规则(Thread Interruption Rule)

对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。可以通过Thread.interrupted()方法检测到是否发生中断。

 

7.      对象终结规则(Finalizer Rule)

一个对象的初始化完成(构造函数执行结束)先行发生于他的finalize()方法的开始。

 

8.      传递性(Transitivity)

如果操作A先行发生于操作,操作B先行发生于操作C,那么可以得出操作A先行发生于操作C。

未经允许不得转载:比尔云 » Java内存模型
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

强烈推荐

高性能SSD云服务器ECS抗攻击,高可用云数据库RDS