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

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

 

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

由于数据从主存中读取数据和写入数据要比CPU执行指令的速度慢的多,如果任何时候对数据的操作都需要通过和主存进行交互,会大大降低指令的执行速度。

因此在CPU处理器里面有了高速缓存。

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

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

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

 

 

2.缓存不一致问题解决

举个例子说明:

    例如

           int i= 0;

           i=i+1;

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

    当线程执行到这个语句的时候,会从主存中读取数据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中对应的缓存行无效。

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

 

4.锁和volatile关键字的对比

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

     volatile可以看成一种"程度较轻的synchronized",与synchronized 块相比,volatile变量的使用所需的编码较少,并且运行开销比较小。

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

   

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

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

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

 变化的操作,使用 volatile 进行只读操作。其中,锁一次只允许一个线程访问值,volatile 允许多个线程执行读操作,因此当使用 volatile 保证读代

 码路径时,要比使用锁执行全部代码路径获得更高的共享度 —— 就像读-写操作一样。然而,要随时牢记这种模式的弱点:如果超越了该模式的最基本应用,

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

 

6.下面看一组例子:

多线程对共享变量++操作,单使用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);
    }

}

 

(1)使用原子类,原子自增操作,其底层实现,通过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);
    }

}

 

参考文章: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 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值