Java内存模型-阿里云开发者社区

开发者社区> 小飞哥1112> 正文

Java内存模型

简介: 本博客为《深入理解java虚拟机》的学习笔记,所以大部分内容来自此书,另外一部分内容来自网络其他博客和源码分析。 主要内容探讨以下问题: Ø  Java内存模型、协议、规则。 Ø  volatile的可见性和禁止指令重排序是什么意思? Ø  Synchronized是如何做到线程安全的? Ø  先行发生原则。
+关注继续查看

本博客为《深入理解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。

 

 

 

五  参考博客

对象创建过程见博客

 

双重检查锁更多分析见博客

http://blog.csdn.net/zhangzeyuaaa/article/details/42673245

 

 

 


版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
怎么设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程
7260 0
阿里云服务器ECS远程登录用户名密码查询方法
阿里云服务器ECS远程连接登录输入用户名和密码,阿里云没有默认密码,如果购买时没设置需要先重置实例密码,Windows用户名是administrator,Linux账号是root,阿小云来详细说下阿里云服务器远程登录连接用户名和密码查询方法
3259 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
4569 0
windows server 2008阿里云ECS服务器安全设置
最近我们Sinesafe安全公司在为客户使用阿里云ecs服务器做安全的过程中,发现服务器基础安全性都没有做。为了为站长们提供更加有效的安全基础解决方案,我们Sinesafe将对阿里云服务器win2008 系统进行基础安全部署实战过程! 比较重要的几部分 1.
5512 0
腾讯云服务器 设置ngxin + fastdfs +tomcat 开机自启动
在tomcat中新建一个可以启动的 .sh 脚本文件 /usr/local/tomcat7/bin/ export JAVA_HOME=/usr/local/java/jdk7 export PATH=$JAVA_HOME/bin/:$PATH export CLASSPATH=.
2216 0
如何设置阿里云服务器安全组?阿里云安全组规则详细解说
阿里云安全组设置详细图文教程(收藏起来) 阿里云服务器安全组设置规则分享,阿里云服务器安全组如何放行端口设置教程。阿里云会要求客户设置安全组,如果不设置,阿里云会指定默认的安全组。那么,这个安全组是什么呢?顾名思义,就是为了服务器安全设置的。安全组其实就是一个虚拟的防火墙,可以让用户从端口、IP的维度来筛选对应服务器的访问者,从而形成一个云上的安全域。
4090 0
阿里云服务器ECS登录用户名是什么?系统不同默认账号也不同
阿里云服务器Windows系统默认用户名administrator,Linux镜像服务器用户名root
1155 0
+关注
小飞哥1112
java,架构相关技术专家
47
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载