JUC回顾之-volatile的原理和使用

 1.计算机内存模型的相关概念原理 

     计算机在执行程序时,每条指令都是在CPU中执行的,在指令的执行过程当中,涉及到数据的读取和写入。因为程序在运行的过程当中数据是放在"主存"中的,html

因为数据从主存中读取数据和写入数据要比CPU执行指令的速度慢的多,若是任什么时候候对数据的操做都须要经过和主存进行交互,会大大下降指令的执行速度。java

所以在CPU处理器里面有了高速缓存。c++

      也就是,当程序的运行过程当中,会将运算的须要的数据从主存复制一份到CPU的高速缓存中,那么当CPU进行计算时就能够直接从他的高速缓存读取数据编程

和向高速缓存写入数据,当运算以后将高速缓存中的数据刷新到主存中。api

下面是计算机中,数据缓存经过总线、缓存一致性协议在处理器CPU和内存之间的传递过程:缓存

 

 

2.缓存不一致问题解决

举个例子说明:安全

    例如多线程

           int i= 0;并发

           i=i+1;oracle

        这段代码在计算机中是如何计算的。

    当线程执行到这个语句的时候,会从主存中读取数据i的值,而后复制一份到高速缓存中,而后CPU指令对i进行+1操做,而后将数据写入到高速缓存中,最后将高速缓存中的i最新的值刷新到主存中。

        这个代码在单线程中运行是没有问题的,可是在多线程中运行就有问题了。在多核的CPU中,每条线程可能运行于不一样的CPU中,所以每一个线程运行时有本身的高速缓存。

        假若有两个线程A,B;初始的时候分别从主存中读取i的值,而后放在各自所在的CPU高速缓存中,而后线程A进行+1操做,而后把i最新的值写入到主存。此时线程B的高速缓存中i的值仍是0,进行+1操做,i的值为1.而后线程B把i的值写入到内存。最终i的值是1,而不是2.

       这就是缓存一致性问题。

也就是说,若是一个变量在多个CPU中都存在缓存(通常在多线程编程时才会出现),那么就可能存在缓存不一致的问题。

  为了解决缓存不一致性问题,一般来讲有如下2种解决方法:

  1)经过在总线加LOCK#锁的方式

  2)经过缓存一致性协议

  这2种方式都是硬件层面上提供的方式。

      在早期的CPU当中,是经过在总线上加LOCK#锁的形式来解决缓存不一致的问题。由于CPU和其余部件进行通讯都是经过总线来进行的,若是对总线加LOCK#锁的话,也就是说阻塞了其余CPU对其余部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。好比上面例子中 若是一个线程在执行 i = i +1,若是在执行这段代码的过程当中,在总线上发出了LOCK#锁的信号,那么只有等待这段代码彻底执行完毕以后,其余CPU才能从变量i所在的内存读取变量,而后进行相应的操做。这样就解决了缓存不一致的问题。

     可是上面的方式会有一个问题,因为在锁住总线期间,其余CPU没法访问内存,致使效率低下。

     因此就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每一个缓存中使用的共享变量的副本是一致的。

     它核心的思想是:当CPU写数据时,若是发现操做的变量是共享变量,即在其余CPU中也存在该变量的副本,会发出信号通知其余CPU将该变量的缓存行置为无效状态,所以当其余CPU须要读取这个变量时,发现本身缓存中缓存该变量的缓存行是无效的,那么它就会从内存从新读取。 

3.深刻剖析volatile关键字原理

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

 volatile关键字的做用:

 * 可见性:能够保证不一样线程对这个变量的可见性,一旦某个线程修改了volatile变量的值,这个值对其余线程是可见的。

 * 原子性:对单个volatile的读和写具备原子性,可是对volatile++这种复合的计数操做不具备原子性。不能用于线程安全计数器。 由于volatile++这种操做,实质上是由一个读取-修改-写入操做序列组成的组合操做。

看下面的代码:

package concurrentMy.Volatiles;

public class VolatileFeaturesExample {
    
    int a = 0;
    volatile boolean  flag = true;
    
    public void writer(){
        a = 1; //1
        flag = true; //2
    }
    
    public void reader(){
        if(flag){  //3
            int i= a; //4
            System.out.println(i);
        }
    }
    

}

假设线程A执行writer方法后,线程B执行reader方法。

(1)从happens-before原则上来说,对volatile的写操做必定happen-before对volatile的读。也就是说上述代码2 happens-before与3,根据程序的执行顺序1 happens-before 2,3 happens-before 4。根据happens-before的传递性,1 happens-before 4.也就保证了线程A,写入volatile flag 变量,当即对B线程可见。

(2)从JMM内存语义上来说,当写一个volatile变量的时候,JMM会把该线程的对应的本地缓存中的共享变量值当即刷新到主内存中。当读一个volatile共享变量时候,JMM会把该线程对应的本地缓存置为无效,也就是上面缓存一致性说的会把该CPU线程对应的缓存行至为无效。直接从主存中读取。

   下图为线程A执行volatile flag写后,共享变量的状态示意图:

   

从写-读的内存语义上来说,一个线程把共享的volatile写入线程本地内存,而后在刷新到主内存,而后其余线程从主内存中

读取这个共享变量。这样其实就实现了线程之间的通讯,经过主内存。

(1)线程A写一个volatile变量,实质上是线程A向将要读这个volatile变量的某个线程发出了消息。

(2)线程A写一个volatile变量,而后线程B读这个volatile变量,这个过程实质上是线程A经过主内存向线程B发送消息。

3.底层实现

 “观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

  lock前缀指令实际上至关于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  1)它确保指令重排序时不会把其后面的指令排到内存屏障以前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操做已经所有完成;

  2)它会强制将对缓存的修改操做当即写入主存;

  3)若是是写操做,它会致使其余CPU中对应的缓存行无效。

  编译器不会对volatile变量的读和读后面的任意内存操做重排序;编译器不会对volatile变量写和写前面任意内存操做作重排序。

 

4.锁和volatile关键字的对比

     功能上锁比volatile更强大,能够保证操做的原子;而可伸缩性和执行的性能上volatile比锁更有优点。

     volatile能够当作一种"程度较轻的synchronized",与synchronized 块相比,volatile变量的使用所需的编码较少,而且运行开销比较小。

     可是不能保证原子性,须要结合一些技术来保证,好比CAS。并发包下面的原子类,可重入锁的实现就是经过volatile结合CAS来实现的。

5.开销较低的读-写锁策略:

 之因此将这种技术称之为 “开销较低的读-写锁” 是由于您使用了不一样的同步机制进行读写操做。由于本例中的写操做违反了使用 volatile 的第一个条件,

 所以不能使用 volatile 安全地实现计数器 —— 您必须使用锁。然而,您能够在读操做中使用 volatile 确保当前值的可见性,所以可使用锁进行全部

 变化的操做,使用 volatile 进行只读操做。其中,锁一次只容许一个线程访问值,volatile 容许多个线程执行读操做,所以当使用 volatile 保证读代

 码路径时,要比使用锁执行所有代码路径得到更高的共享度 —— 就像读-写操做同样。然而,要随时牢记这种模式的弱点:若是超越了该模式的最基本应用,

 结合这两个竞争的同步机制将变得很是困难。

 

6.volatitle的使用场景:

经过关键字sychronize能够防止多个线程进入同一段代码,在某些特定的场景下中,volatitle至关于一个轻量级的sychronize,由于不会引发线程的上下文切换。可是volatitle的使用必须知足两个条件:

1. 对变量的写操做不依赖当前值,如多线程对共享变量执行i++操做,是没法经过volatile保证结果的正确性的;

2.该变量没有包含在具备其余变量的不变式中,经过下面的例子来了解;参考例子(3)

下面看一组例子:多线程对共享变量++操做,单使用volatile变量的话,会出现线程安全的问题,会致使计数不对,下面经过几种方法实现计数功能:

(1)加ReentrantLock互斥锁保证原子性:

package concurrentMy.Volatiles;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 
 * (类型功能说明描述)
 *
 * <p>
 * 修改历史:                                            <br>  
 * 修改日期            修改人员       版本             修改内容<br>  
 * -------------------------------------------------<br>  
 * 2016年4月8日 下午6:07:36   user     1.0        初始化建立<br>
 * </p> 
 *
 * @author        Peng.Li 
 * @version        1.0  
 * @since        JDK1.7
 */
public class VolatiteLock implements Runnable{
    // 不能保证原子性,若是不加synchronized的话
    private volatile int inc = 0;
    Lock lock = new ReentrantLock();
    

    /**
     * 
     * 理解:高速缓存 - 主存
     * 经过ReentrantLock保证原子性:读主存,在高速缓存中计算获得+1后的值,写回主存
     * (方法说明描述) 
     *
     */
    public  void increase() {
        lock.lock();
        try {
            inc++;
        } catch (Exception e) {
            e.printStackTrace();
        }finally{
            lock.unlock();
        }

    }
    

    public void run() {
        for (int i = 0; i < 10000; i++) {
            increase();
        }

    }

    public static void main(String[] args) throws InterruptedException {

        VolatiteLock v = new VolatiteLock();
        // 线程1
        Thread t1 = new Thread(v);
        // 线程2
        Thread t2 = new Thread(v);
        t1.start();
        t2.start();

        // for(int i=0;i<100;i++){
        // System.out.println(i);
        // }

        System.out.println(Thread.activeCount() + Thread.currentThread().getId() + Thread.currentThread().getName());

        while (Thread.activeCount() > 1)
            // 保证前面的线程都执行完
            Thread.yield();
        
        //20000
        System.out.println(v.inc);
    }

}

 (2)使用原子类,原子自增操做,其底层实现,经过volatile+Cas保证原子性操做,保证读-改-写操做顺序执行,不会发生线程安全的问题。

package concurrentMy.Volatiles;

import java.util.concurrent.atomic.AtomicInteger;

/**
 *     
 *     
 *     
 *  
 * <p>
 * 修改历史:                                            <br>  
 * 修改日期            修改人员       版本             修改内容<br>  
 * -------------------------------------------------<br>  
 * 2015年7月14日 下午3:58:30   user     1.0        初始化建立<br>
 * </p> 
 *
 * @author        Peng.Li 
 * @version        1.0  
 * @since        JDK1.7
 */
public class VolatileAtomic implements Runnable {
    private AtomicInteger ai = new AtomicInteger(0);

    /**
     * atomic是利用CAS来实现原子性操做的(Compare And Swap),CAS其实是利用处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操做。
     * (方法说明描述) 
     *
     */
    public void increaseAtomic() {
        ai.incrementAndGet();
    }

    public void run() {
        for (int i = 0; i < 10000; i++) {
            increaseAtomic();
        }
    }

    public static void main(String[] args) throws InterruptedException {

        VolatileAtomic v = new VolatileAtomic();
        // 线程1
        Thread t1 = new Thread(v);
        // 线程2
        Thread t2 = new Thread(v);
        t1.start();
        t2.start();

        // for(int i=0;i<100;i++){
        // System.out.println(i);
        // }

        System.out.println(Thread.activeCount() + Thread.currentThread().getId() + Thread.currentThread().getName());
        while (Thread.activeCount() > 1)
            // 保证前面的线程都执行完
            Thread.yield();

        System.out.println(v.ai);
    }

}

(3):对于“volatitle的使用场景2”的解释以下:

 

public class NumberRange {
    private volatile int lower = 0; private volatile int upper = 10; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } }

上述代码中,上下界初始化分别为0和10,假设线程A和B在某一个时刻同时执行了setLower(8)和setUpper(5),且经过了检查,那么就设置了一个无效的范围(8,5)

因此在这种场景下,须要经过lock保证setLower和setUpper在每个时刻只有一个线程执行;

下面是项目中常常用到的volatile关键字的两个场景:

1.状态标记量

在高并发的场景中,经过一个boolean类型的变量控制开关按钮,控制代码的逻辑开关(当即生效的开关),好比是否走促销的逻辑,该如何实现?

public class SwitchControl {
    private volatile int isOpen; public void run() { if (isOpen) { //促销逻辑 } else { //正常逻辑  } } public void setIsopen(boolean isopen) { this.isopen = isopen } }

这里举个例子说明了volatile的使用方法:用户的请求线程执行到run方法的时候,若是开启促销活动,能够经过mcc配置的开关开启为true,因为isOpen是volatile修饰的,因此一经修改,其余线程均可以拿到isOpen的最新值,在高并发下用户的请求线程能够执行到促销的逻辑了;

 

2.单例的应用中double check检查防止进行重排序

  单例模式不少人忽略写volatile关键字,由于大部分状况下没有这个关键字,程序也很好的的运行,可是代码稳定性不是100%,说不定在某个时刻,隐藏的bug就出来了,可能在高并发的状况下,出现指令重排序,致使线程拿到的单例对象没有初始化;

public class Singleton {
   private volatile static Singleton instance; private Singleton(){} public static Singleton getInstatance(){ if(instance == null){ // 0 若是不加这个if思考问题? synchronized(Singleton.class){ if(instance = null){ //1 instance = new Singleton(); //2 初始化单例类 } } } return instance; // 3 } }

 若是不加0处的if判断,会致使多个线程频繁调用 getInstance方法的时候,将会致使锁竞争,致使性能开销;因而想出了双重检查断定来下降同步的开销;

若是第一次检查instance不为null,那么就不须要进行下面的加锁和初始化操做了,所以能够下降synchronize带来的性能开销;

1.多个线程试图在同一个时间点建立对象,会经过加锁来保证只有一个线程能建立对象。

2.在对象建立好以后,执行getInstance()方法将不须要获取锁,直接返回已经建立好的的对象。

思考:若是再2处的代码不加volatile关键字,这个单例程序会不会有问题?

首先在理解下volatile内存的可见性,volatile的可见性是基于内存屏障实现的,什么是内存屏障?内存屏障,是一个CPU的指令,在程序运行时,为了提升执行的性能,编译器和处理器会对指令进行重排序,JMM为了保证不一样不一样编译器和CPU上有相同的结果,经过插入特定类型的内存屏障来禁止特定类型的编译器的重排序和处理器的重排序,插入一条内存屏障告诉编译器和CPU;无论什么指令都不能对这条内存屏障进行指令的重排序。若是再2处不加volatile关键字,因为1出的代码内部其实相似这样的实现,对象的初始化过程实际上是这样的:

instance = new Singleton(); //2 初始化单例类

分为3个步骤:

*  1. memory = allocate(); 分配对象的内存空间,在堆上
* 2.ctorInstance(memory); 初始化对象
* 3.instance = memory; 设置instance指向刚分配的内存地址

 不加volatile可能致使上面3调语句的执行过程随意进行重排序,即执行的过程多是123,或者是132;若是是132执行过程,假如A执行完3了,B线程也调用getInstance方法,根据0处的代码if(instance == null)由于A线程给instance分配了内存地址,因此致使B线程认为这个对象不为null,直接返回了这个对象;可是这个对象尚未执行2,因此对象其实仍是未初始化。那么程序就出现了问题。

若是加了volatile,会插入内存屏障,会禁止1,2,3步骤的重排序,不容许2,3进行重排序,那么不会发生B访问到的是一个未初始化的对象;

经过观察volatile变量和普通变量的汇编代码能够发现,操做volatile变量多出了一个lock前缀指令:

Java代码:
instance = new Singleton(); 汇编代码: 0x01a3de1d: movb $0x0,0x1104800(%esi); 0x01a3de24: **lock** addl $0x0,(%esp);

这个lock前缀指令至关于内存屏障,提供了如下保证:

一、将当前CPU缓存行的数据写会到主内存;

二、这个写回内存的操做致使在其余CPU里面的缓存了该内存地址的数据无效;

CPU为了提升性能,并不直接和内存进行通讯,而是将内存的数据读取到内部缓存(L1,L2)再进行操做,可是操做完并不能肯定什么时候写回到内存,若是对volatile变量进行写操做,当CPU执行到Lock前缀指令时候,会将这个变量的缓存行的数据写回到主内存,不过仍是存在数据一致性的问题,就算内存是最新的,其余CPU缓存仍是旧值,因此为了保证各个CPU缓存的一致性,每一个CPU经过嗅探在总线上传播的数据来检查本身缓存数据的有效性,当发现本身的缓存行对应的内存地址的数据被修改,就会将该缓存行设置为无效状态,当CPU读取变量时,发现缓存行被设置为无效,就会从新到主内存读取数据到缓存中。

 

补充第二种线程安全的延迟初始化方案(这个方案被称为 Initialization On Demand Holder idiom IODH):这种方案是经过JVM类的初始化期间获取这个初始化锁,而且每一个线程至少获取一次锁开确保这个类已经被初始化过了;这就保证在同一个时刻,A线程在调用getInstance方法初始化下面InstanceHolder类的时候,B线程是须要等待A初始化完后,拿到这个初始化锁才能初始化话这个类,保证了B线程没法看到Instance = new Instance(); 内部的3部初始化重排序过程,也就不会拿到一个未初始化的实例;下面给出第二种单例的写法:

public class InstanceFactory{
    private static class InstanceHolder{
        public static Instance instance = new Instance();
   }
      
   public static Instance getInstance(){
       return InstanceHolder.instance;
  }
}

两种线程安全延迟初始化方案的对比:

延迟初始化下降了初始化类或者建立实例的开销,若是确实须要对实例字段采用线程安全的延迟初始化,基于volatile的延迟化方案;若是确实须要对静态字段的使用线程安全的延迟初始化,那么建议采用类初始化方案;

 

参考文章:1.http://www.ibm.com/developerworks/cn/java/j-jtp06197.html

              2.http://www.cnblogs.com/dolphin0520/p/3920373.html  

              3.http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/package-frame.html

              4 深刻浅出 Java Concurrency (5): 原子操做 part 4:http://www.blogjava.net/xylz/archive/2010/07/04/325206.html 

              5.狼哥:https://www.jianshu.com/p/195ae7c77afe

相关文章
相关标签/搜索