Java基础:并发环境下指令重排带来的问题

标签: java 基础 并发 | 发表时间:2016-01-24 08:11 | 作者:jiq408694711
出处:http://blog.csdn.net

JVM内存模型 - 主内存和线程独立的工作内存

Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存,线程只能访问自己的工作内存,不可以访问其它线程的工作内存。工作内存中保存了主内存共享变量的 副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中。

如何保证多个线程操作主内存的数据完整性是一个难题,Java内存模型也规定了工作内存与主内存之间交互的协议,首先是定义了8种原子操作:

(1) lock:将主内存中的变量锁定,为一个线程所独占

(2) unclock:将lock加的锁定解除,此时其它的线程可以有机会访问此变量

(3) read:将主内存中的变量值读到工作内存当中

(4) load:将read读取的值保存到工作内存中的变量副本中。

(5) use:将值传递给线程的代码执行引擎

(6) assign:将执行引擎处理返回的值重新赋值给变量副本

(7) store:将变量副本的值存储到主内存中。

(8) write:将store存储的值写入到主内存的共享变量当中。


1. 内存可见性

1.1 概念

通过上面Java内存模型的概述,我们会注意到这么一个问题,每个线程在获取锁之后会在自己的工作内存来操作共享变量,操作完成之后将工作内存中的副本回写到主内存,并且在其它线程从主内存将变量同步回自己的工作内存之前,共享变量的改变对其是不可见的。


1.2 内存可见性带来的问题

很多时候我们需要一个线程对共享变量的改动,其它线程也需要立即得知这个改动该怎么办呢?比如以下的情景,有一个全局的状态变量open:

boolean open=true;

这个变量用来描述对一个资源的打开关闭状态,true表示打开,false表示关闭,假设有一个线程A,在执行一些操作后将open修改为false:

 

//线程A

resource.close();

open = false;

 

线程B随时关注open的状态,当open为true的时候通过访问资源来进行一些操作:

//线程B 

while(open) {

       doSomethingWithResource(resource);

}

 

当A把资源关闭的时候,open变量对线程B不可见,如果此时open变量的改动尚未同步到线程B的工作内存中,那么线程B就会用一个已经关闭了的资源去做一些操作,因此产生错误。


1.3 volatile关键字

所以对于上面的情景,要求一个线程对open的改变,其他的线程能够立即可见,Java为此提供了volatile关键字,在声明open变量的时候加入volatile关键字就可以保证open的内存可见性,即open的改变对所有的线程都是立即可见的。

volatile保证可见性的原理是在 每次访问变量时都会进行一次刷新,因此每次访问都是主内存中最新的版本。所以volatile关键字的作用之一就是 保证变量修改的实时可见性

 

2. 指令重排

2.1 概念

指令重排序是JVM为了优化指令,提高程序运行效率。指令重排序包括编译器重排序和运行时重排序。JVM规范规定,指令重排序可以在不影响单线程程序执行结果前提下进行。


2.2 指令重排带来的问题

例子1:简单指令重排

假设有这么两个共享变量a和b: 

private int a;

private int b;

 

在线程A中有两条语句对这两个共享变量进行赋值操作:

a = 1;

b = 2;

 

假设当线程A对a进行复制操作的时候发现这个变量在主内存已经被其它的线程加了访问锁,那么此时线程A怎么办?等待释放锁?不,等待太浪费时间了,它会去尝试进行b的赋值操作,b这时候没被人占用,因此就会先为b赋值,再去为a赋值,那么执行的顺序就变成了:

b = 2;

a = 1;

例子2:A线程指令重排导致B线程出错

对于在同一个线程内,这样的改变是不会对逻辑产生影响的,但是在多线程的情况下指令重排序会带来问题。看下面这个情景:

在线程A中:

context = loadContext();

inited = true;

 

在线程B中:

while(!inited ){ //根据线程A中对inited变量的修改决定是否使用context变量

   sleep(100);

}

doSomethingwithconfig(context);

 

假设线程A中发生了指令重排序:

inited = true;

context = loadContext();

 

那么B中很可能就会拿到一个尚未初始化或尚未初始化完成的context,从而引发程序错误。

 

例子3:指令重排导致单例模式失效

我们都知道一个经典的懒加载方式的单例模式:

public class Singleton {

  private static Singleton instance = null;

  private Singleton() { }

  public static Singleton getInstance() {

      if(instance == null) {

         synchronzied(Singleton.class) {

           if(instance == null) {

               instance = new Singleton();

           }

        }

     }

     return instance;

   }

}

(更多关于单例模式的实现方式,参考另一篇 博文) 


看似简单的一段赋值语句:instance = new Singleton();,其实JVM内部已经转换为多条指令:

memory = allocate();   //1:分配对象的内存空间 

ctorInstance(memory);  //2:初始化对象 

instance = memory;     //3:设置instance指向刚分配的内存地址 

 

但是经过重排序后如下:

memory = allocate();   //1:分配对象的内存空间 

instance = memory;    //3:设置instance指向刚分配的内存地址,此时对象还没被初始化

ctorInstance(memory);  //2:初始化对象

 

可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面,在线程A初始化完成这段内存之前,线程B虽然进不去同步代码块,但是在同步代码块之前的判断就会发现instance不为空,此时线程B获得instance对象进行使用就可能发生错误。


2.3 volatile关键字

除了前面内存可见性中讲到的volatile关键字可以保证变量修改的可见性之外,还有另一个重要的作用: 在JDK1.5之后,可以使用volatile变量禁止指令重排序。

 

例子2和例子3中的变量以关键字volatile修饰之后,就会组织JVM对其相关代码进行指令重排,这样就能够按照既定的顺序指执行。

 

总结

相对于synchronized块的代码锁,volatile应该是提供了一个轻量级的针对共享变量的锁,当我们在多个线程间使用共享变量进行通信的时候需要考虑将共享变量用volatile来修饰。

 

附录:参考文章

http://my.oschina.net/chihz/blog/58035?fromerr=H6M9Pl4V

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

 

作者:jiq408694711 发表于2016/1/24 0:11:12 原文链接
阅读:284 评论:0 查看评论

相关 [java 基础 并发] 推荐:

Java并发编程基础

- - 并发编程网 - ifeve.com
并发是一种能并行运行多个程序或并行运行一个程序中多个部分的能力. 如果程序中一个耗时的任务能以异步或并行的方式运行,那么整个程序的吞吐量和可交互性将大大改善. 现代的PC都有多个CPU或一个CPU中有多个核. 是否能合理运用多核的能力将成为一个大规模应用程序的关键. 进程是以独立于其他进程的方式运行的,进程间是互相隔离的.

JAVA并发总结-基础篇

- - CSDN博客编程语言推荐文章
java中有几种方法可以实现一个线程. 继承Thread类,实现Runnable接口创建一个线程的唯一方法是实例化java.lang.Thread类(或其子类),并调用其start()方法. 调用ThreadInstanceA.inerrupt()方法,这样当A线程在Thread的sleep,join方法,或者Object的wait方法的时候会直接抛出InerruptedException,捕捉后便可退出.

JAVA多线程和并发基础面试问答

- - 企业架构 - ITeye博客
原文链接    译文连接  作者:Pankaj   译者: 郑旭东   校对:方腾飞. 多线程和并发问题是Java技术面试中面试官比较喜欢问的问题之一. 在这里,从面试的角度列出了大部分重要的问题,但是你仍然应该牢固的掌握Java多线程基础知识来对应日后碰到的问题. ( 校对注:非常赞同这个观点).

Java基础:并发环境下指令重排带来的问题

- - CSDN博客推荐文章
JVM内存模型 - 主内存和线程独立的工作内存. Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存,线程只能访问自己的工作内存,不可以访问其它线程的工作内存. 工作内存中保存了主内存共享变量的. 副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中.

java基础知识

- - CSDN博客互联网推荐文章
JAVA相关基础知识. 1、面向对象的特征有哪些方面. 抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面. 抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节. 抽象包括两个方面,一是过程抽象,二是数据抽象. 继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法.

JAVA面试精选【Java基础】

- - CSDN博客编程语言推荐文章
  这个系列面试题主要目的是帮助你拿轻松到offer,同时还能开个好价钱. 只要能够搞明白这个系列的绝大多数题目,在面试过程中,你就能轻轻松松的把面试官给忽悠了. 对于那些正打算找工作JAVA软件开发工作的童鞋们来说,当你看到这份题目的时候,你应该感动很幸运,因为,只要你把题目中的内容都搞懂了,在笔试的时候就可以游刃有余,通过面试只有半步之遥了,笔试只能反映你的JAVA技能.

Java基础—ClassLoader的理解

- - ZJD'S NOTES
} ``` 其余两个ClassLoader都是继承自`ClassLoader`这个类. Java的类加载采用了一种叫做“双亲委托”的方式(稍后解释),所以除了`Bootstarp ClassLoader`其余的ClassLoader都有一个“父”类加载器, 不是通过集成,而是一种包含的关系. ``` ##“双亲委托” 所谓“双亲委托”就是当加载一个类的时候会先委托给父类加载器去加载,当父类加载器无法加载的时候再尝试自己去加载,所以整个类的加载是“自上而下”的,如果都没有加载到则抛出`ClassNotFoundException`异常.

JAVA基础之理解JNI原理

- shuangxi - 博客园-首页原创精华区
JNI是JAVA标准平台中的一个重要功能,它弥补了JAVA的与平台无关这一重大优点的不足,在JAVA实现跨平台的同时,也能与其它语言(如C、C++)的动态库进行交互,给其它语言发挥优势的机会. 有了JAVA标准平台的支持,使JNI模式更加易于实现和使用. 环境说明:ubuntu 10.4.2 LTS系统.