Java并发编程、内存模型与Volatile

 

http://www.importnew.com/24082.html  volatile关键字html

http://www.importnew.com/16142.html  ConcurrentHashMap原理分析java

http://www.importnew.com/19612.html  Java内存模型c++

 

Java内存模型:编程

关键字:主存、工做内存;堆区、栈区(http://www.importnew.com/19612.html  )缓存

在Java Memory Model中,Memory分为两类,main memory和working memory,main memory为全部线程共享,working memory中存放的是线程所须要的变量的拷贝(线程要对main memory中的内容进行操做的话,首先须要拷贝到本身的working memory,通常为了速度,working memory通常是在cpu的cache中的)。volatile的变量在被操做的时候不会产生working memory的拷贝,而是直接操做main memory,固然volatile虽然解决了变量的可见性问题,但没有解决变量操做的原子性的问题,这个还须要synchronized或者CAS相关操做配合进行。多线程

Java内存模型规定了全部的变量都存储在主内存中。每条线程中还有本身的工做内存,线程的工做内存中保存了被该线程所使用到的变量(这些变量是从主内存中拷贝而来)。线程对变量的全部操做(读取,赋值)都必须在工做内存中进行。不一样线程之间也没法直接访问对方工做内存中的变量,线程间变量值的传递均须要经过主内存来完成。并发

 

并发编程的三大概念:原子性,有序性,可见性。app

可见性

也就说假设一个对象中有一个变量i,那么i是保存在main memory中的,当某一个线程要操做i的时候,首先须要从main memory中将i 加载到这个线程的working memory中,这个时候working memory中就有了一个i的拷贝,这个时候此线程对i的修改都在其working memory中,直到其将i从working memory写回到main memory中,新的i的值才能被其余线程所读取。从某个意义上说,可见性保证了各个线程的working memory的数据的一致性。 可见性遵循下面一些规则:函数

  • 当一个线程运行结束的时候,全部写的变量都会被flush回main memory中。
  • 当一个线程第一次读取某个变量的时候,会从main memory中读取最新的。
  • volatile的变量会被马上写到main memory中的,在jsr133中,对volatile的语义进行加强,后面会提到
  • 当一个线程释放锁后,全部的变量的变化都会flush到main memory中,而后一个使用了这个相同的同步锁的进程,将会从新加载全部的使用到的变量,这样就保证了可见性。

原子性

还拿上面的例子来讲,原子性就是当某一个线程修改i的值的时候,从取出i到将新的i的值写给i之间不能有其余线程对i进行任何操做。也就是说保证某个线程对i的操做是原子性的,这样就能够避免数据脏读。 经过锁机制或者CAS(Compare And Set 须要硬件CPU的支持)操做能够保证操做的原子性。性能

有序性

假设在main memory中存在两个变量i和j,初始值都为0,在某个线程A的代码中依次对i和j进行自增操做(i,j的操做不相互依赖)

1
2
i++;
j++;

因为,因此i,j修改操做的顺序可能会被从新排序。那么修改后的ij写到main memory中的时候,顺序可能就不是按照i,j的顺序了,这就是所谓的reordering,在单线程的状况下,当线程A运行结束的后i,j的值都加1了,在线程本身看来就好像是线程按照代码的顺序进行了运行(这些操做都是基于as-if-serial语义的),即便在实际运行过程当中,i,j的自增可能被从新排序了,固然计算机也不能帮你乱排序,存在上下逻辑关联的运行顺序确定仍是不会变的。可是在多线程环境下,问题就不同了,好比另外一个线程B的代码以下

1
2
3
if (j== 1 ) {
     System.out.println(i);
}

按照咱们的思惟方式,当j为1的时候那么i确定也是1,由于代码中i在j以前就自增了,但实际的状况有可能当j为1的时候i仍是为0。这就是reordering产生的很差的后果,因此咱们在某些时候为了不这样的问题须要一些必要的策略,以保证多个线程一块儿工做的时候也存在必定的次序。JMM提供了happens-before 的排序策略。这样咱们能够获得多线程环境下的as-if-serial语义。 这里不对happens-before进行详细解释了,详细的请看这里http://www.ibm.com/developerworks/cn/java/j-jtp03304/,这里主要讲一下volatile在新的java内存模型下的变化,在jsr133以前,下面的代码可能会出现问题

1
2
3
4
5
6
7
8
9
10
11
12
Map configOptions;
char [] configText;
volatile boolean initialized = false ;
// In Thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true ;
// In Thread B
while (!initialized)
   sleep();
// use configOptions

jsr133以前,虽然对 volatile 变量的读和写不能与对其余 volatile 变量的读和写一块儿从新排序,可是它们仍然能够与对 nonvolatile 变量的读写一块儿从新排序,因此上面的Thread A的操做,就可能initialized变成true的时候,而configOptions尚未被初始化,因此initialized先于configOptions被线程B看到,就产生问题了。

JSR 133 Expert Group 决定让 volatile 读写不能与其余内存操做一块儿从新排序,新的内存模型下,若是当线程 A 写入 volatile 变量 V 而线程 B 读取 V 时,那么在写入 V 时,A 可见的全部变量值如今均可以保证对 B 是可见的。

结果就是做用更大的 volatile 语义,代价是访问 volatile 字段时会对性能产生更大的影响。这一点在ConcurrentHashMap中的统计某个segment元素个数的count变量中使用到了。

 

 

 

深刻理解volatile关键字

1.volatile保证可见性

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰以后,那么就具有了两层语义:

1)保证了不一样线程对这个变量进行操做时的可见性,即一个线程修改了某个变量的值,这新值对其余线程来讲是当即可见的。

2)禁止进行指令重排序。

先看一段代码,假如线程1先执行,线程2后执行:

1
2
3
4
5
6
7
8
//线程1
boolean stop = false ;
while (!stop){
     doSomething();
}
 
//线程2
stop = true ;

这段代码是很典型的一段代码,不少人在中断线程时可能都会采用这种标记办法。可是事实上,这段代码会彻底运行正确么?即必定会将线程中断么?不必定,也许在大多数时候,这个代码可以把线程中断,可是也有可能会致使没法中断线程(虽然这个可能性很小,可是只要一旦发生这种状况就会形成死循环了)。

下面解释一下这段代码为什么有可能致使没法中断线程。在前面已经解释过,每一个线程在运行过程当中都有本身的工做内存,那么线程1在运行的时候,会将stop变量的值拷贝一份放在本身的工做内存当中。

那么当线程2更改了stop变量的值以后,可是还没来得及写入主存当中,线程2转去作其余事情了,那么线程1因为不知道线程2对stop变量的更改,所以还会一直循环下去。

可是用volatile修饰以后就变得不同了:

第一:使用volatile关键字会强制将修改的值当即写入主存;

第二:使用volatile关键字的话,当线程2进行修改时,会致使线程1的工做内存中缓存变量stop的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);

第三:因为线程1的工做内存中缓存变量stop的缓存行无效,因此线程1再次读取变量stop的值时会去主存读取。

那么在线程2修改stop值时(固然这里包括2个操做,修改线程2工做内存中的值,而后将修改后的值写入内存),会使得线程1的工做内存中缓存变量stop的缓存行无效,而后线程1读取时,发现本身的缓存行无效,它会等待缓存行对应的主存地址被更新以后,而后去对应的主存读取最新的值。

那么线程1读取到的就是最新的正确的值。

2.volatile不能确保原子性

下面看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Test {
     public volatile int inc = 0 ;
 
     public void increase() {
         inc++;
     }
 
     public static void main(String[] args) {
         final Test test = new Test();
         for ( int i= 0 ;i< 10 ;i++){
             new Thread(){
                 public void run() {
                     for ( int j= 0 ;j< 1000 ;j++)
                         test.increase();
                 };
             }.start();
         }
 
         while (Thread.activeCount()> 1 //保证前面的线程都执行完
             Thread.yield();
         System.out.println(test.inc);
     }
}

你们想一下这段程序的输出结果是多少?也许有些朋友认为是10000。可是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字。

可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操做,因为volatile保证了可见性,那么在每一个线程中对inc自增完以后,在其余线程中都能看到修改后的值啊,因此有10个线程分别进行了1000次操做,那么最终inc的值应该是1000*10=10000。

这里面就有一个误区了,volatile关键字能保证可见性没有错,可是上面的程序错在没能保证原子性。可见性只能保证每次读取的是最新的值,可是volatile没办法保证对变量的操做的原子性。

在前面已经提到过,自增操做是不具有原子性的,它包括读取变量的原始值、进行加1操做、写入工做内存。那么就是说自增操做的三个子操做可能会分割开执行,就有可能致使下面这种状况出现:

假如某个时刻变量inc的值为10,

线程1对变量进行自增操做,线程1先读取了变量inc的原始值,而后线程1被阻塞了;

而后线程2对变量进行自增操做,线程2也去读取变量inc的原始值,因为线程1只是对变量inc进行读取操做,而没有对变量进行修改操做,因此不会致使线程2的工做内存中缓存变量inc的缓存行无效,也不会致使主存中的值刷新,因此线程2会直接去主存读取inc的值,发现inc的值时10,而后进行加1操做,并把11写入工做内存,最后写入主存。

而后线程1接着进行加1操做,因为已经读取了inc的值,注意此时在线程1的工做内存中inc的值仍然为10,因此线程1对inc进行加1操做后inc的值为11,而后将11写入工做内存,最后写入主存。

那么两个线程分别进行了一次自增操做后,inc只增长了1。

根源就在这里,自增操做不是原子性操做,并且volatile也没法保证对变量的任何操做都是原子性的。

解决方案:能够经过synchronized或lock,进行加锁,来保证操做的原子性。也能够经过AtomicInteger。

在java 1.5的java.util.concurrent.atomic包下提供了一些原子操做类,即对基本数据类型的 自增(加1操做),自减(减1操做)、以及加法操做(加一个数),减法操做(减一个数)进行了封装,保证这些操做是原子性操做。atomic是利用CAS来实现原子性操做的(Compare And Swap),CAS其实是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操做。

3.volatile保证有序性

在前面提到volatile关键字能禁止指令重排序,因此volatile能在必定程度上保证有序性。

volatile关键字禁止指令重排序有两层意思:

1)当程序执行到volatile变量的读操做或者写操做时,在其前面的操做的更改确定所有已经进行,且结果已经对后面的操做可见;在其后面的操做确定尚未进行;

2)在进行指令优化时,不能将在对volatile变量的读操做或者写操做的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

可能上面说的比较绕,举个简单的例子:

1
2
3
4
5
6
7
8
//x、y为非volatile变量
//flag为volatile变量
 
x = 2 ;        //语句1
y = 0 ;        //语句2
flag = true //语句3
x = 4 ;         //语句4
y = - 1 ;       //语句5

因为flag变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句3放到语句一、语句2前面,也不会讲语句3放到语句四、语句5后面。可是要注意语句1和语句2的顺序、语句4和语句5的顺序是不做任何保证的。

而且volatile关键字能保证,执行到语句3时,语句1和语句2一定是执行完毕了的,且语句1和语句2的执行结果对语句三、语句四、语句5是可见的。

那么咱们回到前面举的一个例子:

1
2
3
4
5
6
7
8
9
//线程1:
context = loadContext();   //语句1
inited = true ;             //语句2
 
//线程2:
while (!inited ){
   sleep()
}
doSomethingwithconfig(context);

前面举这个例子的时候,提到有可能语句2会在语句1以前执行,那么久可能致使context还没被初始化,而线程2中就使用未初始化的context去进行操做,致使程序出错。

这里若是用volatile关键字对inited变量进行修饰,就不会出现这种问题了,由于当执行到语句2时,一定能保证context已经初始化完毕。

 

volatile的实现原理

1.可见性

处理器为了提升处理速度,不直接和内存进行通信,而是将系统内存的数据独到内部缓存后再进行操做,但操做完后不知何时会写到内存。

若是对声明了volatile变量进行写操做时,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写会到系统内存。 这一步确保了若是有其余线程对声明了volatile变量进行修改,则当即更新主内存中数据。

但这时候其余处理器的缓存仍是旧的,因此在多处理器环境下,为了保证各个处理器缓存一致,每一个处理会经过嗅探在总线上传播的数据来检查 本身的缓存是否过时,当处理器发现本身缓存行对应的内存地址被修改了,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操做时,会强制从新从系统内存把数据读处处理器缓存里。 这一步确保了其余线程得到的声明了volatile变量都是从主内存中获取最新的。

2.有序性

Lock前缀指令实际上至关于一个内存屏障(也成内存栅栏),它确保指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操做已经所有完成。

 

volatile的应用场景

synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,而volatile关键字在某些状况下性能要优于synchronized,可是要注意volatile关键字是没法替代synchronized关键字的,由于volatile关键字没法保证操做的原子性。一般来讲,使用volatile必须具有如下2个条件:

1)对变量的写操做不依赖于当前值

2)该变量没有包含在具备其余变量的不变式中

下面列举几个Java中使用volatile的几个场景。

①.状态标记量

1
2
3
4
5
6
7
8
9
volatile boolean flag = false ;
  //线程1
while (!flag){
     doSomething();
}
   //线程2
public void setFlag() {
     flag = true ;
}

根据状态标记,终止线程。

②.单例模式中的double check

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Singleton{
     private volatile static Singleton instance = null ;
 
     private Singleton() {
 
     }
 
     public static Singleton getInstance() {
         if (instance== null ) {
             synchronized (Singleton. class ) {
                 if (instance== null )
                     instance = new Singleton();
             }
         }
         return instance;
     }
}
为何要使用volatile 修饰instance?

主要在于instance = new Singleton()这句,这并不是是一个原子操做,事实上在 JVM 中这句话大概作了下面 3 件事情:

1.给 instance 分配内存

2.调用 Singleton 的构造函数来初始化成员变量

3.将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)。

可是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序多是 1-2-3 也多是 1-3-2。若是是后者,则在 3 执行完毕、2 未执行以前,被线程二抢占了,这时 instance 已是非 null 了(但却没有初始化),因此线程二会直接返回 instance,而后使用,而后瓜熟蒂落地报错。

相关文章
相关标签/搜索