深刻理解java并发编程基础篇(三)-------volatile

1、前言

  在上一篇,咱们研究了Java内存模型,而且知道Java内存模型的概念以及做用,围绕着原子性、可见性、有序性进行了简单的概述,那么在这一篇咱们首先会介绍volatile关键字的基础认知,而后深刻的去解析volatile在这三个特性中究竟有什么样的做用?volatile是如何实现的?java

2、volatile的用法

  volatile一般被比喻成”轻量级的Synchronized“,也是Java并发编程中比较重要的一个关键字。和Synchronized不一样,volatile是一个变量修饰符,只能用来修饰变量。没法修饰方法及代码块等。c++

  volatile的用法比较简单,只须要在声明一个可能被多线程同时访问的变量时,使用volatile修饰就能够了。算法

  举一个单例实现的简单例子,代码以下:编程

package com.MyMineBug.demoRun.test;

public class Singleton {

	private volatile static Singleton singleton;

	private Singleton() {
		
	};

	public static Singleton getInstance() {
		if(singleton == null) {
			synchronized (Singleton.class) {
				if (singleton == null) {
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}

}
复制代码

这段代码是比较典型的使用双重锁校验实现单例的一种形式,其中使用volatile关键字修饰能够被多个线程同时访问。缓存

3、volatile的特性

  首先看一个代码例子:多线程

package com.MyMineBug.demoRun.test;

class VolatileFeaturesExample {

    volatile long vl = 0L;  // 使用 volatile 声明 64 位的 long 型变量 

    public void set(long l) {
        vl = l;   // 单个 volatile 变量的写 
    }

    public void getAndIncrement () {
        vl++;    // 复合(多个)volatile 变量的读 / 写 
    }

    public long get() {
        return vl;   // 单个 volatile 变量的读 
    }
}
复制代码

假设有多个线程分别调用上面程序的三个方法,这个程序在语意上和下面程序等价:并发

package com.MyMineBug.demoRun.test;

class VolatileFeaturesExample {
    long vl = 0L;               // 64 位的 long 型普通变量 

    public synchronized void set(long l) {     // 对单个的普通 变量的写用同一个监视器同步 
        vl = l;
    }

    public void getAndIncrement () { // 普通方法调用 
        long temp = get();           // 调用已同步的读方法 
        temp += 1L;                  // 普通写操做 
        set(temp);                   // 调用已同步的写方法 
    }
    
    public synchronized long get() { 
    // 对单个的普通变量的读用同一个监视器同步 
        return vl;
    }
}
复制代码

如上面示例程序所示,对一个 volatile 变量的单个读 / 写操做,与对一个普通变量的读 / 写操做使用同一个监视器锁来同步,它们之间的执行效果相同。 经过对比,咱们能够知道:post

1.可见性。对一个 volatile 变量的读,老是能看到(任意线程)对这个 volatile 变量最后的写入。优化

2.原子性:对任意单个 volatile 变量的读 / 写具备原子性,但相似于 volatile++ 这种复合操做不具备原子性。spa

3.1 volatile与有序性

  volatile一个强大的功能,那就是他能够禁止指令重排优化。经过禁止指令重排优化,就能够保证代码程序会严格按照代码的前后顺序执行。那么volatile又是如何禁止指令重排的呢?

  先看一个概念内存屏障(Memory Barrier):是一类同步屏障指令,是CPU或编译器在对内存随机访问的操做中的一个同步点,使得此点以前的全部读写操做都执行后才能够开始执行此点以后的操做。而volatile就是是经过内存屏障来禁止指令重排的。下表描述了和volatile有关的指令重排禁止行为:

从上表咱们能够看出:

当第二个操做是volatile写时,无论第一个操做是什么,都不能重排序。这个规则确保volatile写以前的操做不会被编译器重排序到volatile写以后。

当第一个操做是volatile读时,无论第二个操做是什么,都不能重排序。这个规则确保volatile读以后的操做不会被编译器重排序到volatile读以前。

当第一个操做是volatile写,第二个操做是volatile读时,不能重排序。

为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来讲,发现一个最优布置来最小化插入屏障的总数几乎不可能,为此,JMM 采起保守策略。下面是基于保守策略的 JMM 内存屏障插入策略:

在每一个 volatile 写操做的前面插入一个 StoreStore 屏障。

在每一个 volatile 写操做的后面插入一个 StoreLoad 屏障。

在每一个 volatile 读操做的后面插入一个 LoadLoad 屏障。

在每一个 volatile 读操做的后面插入一个 LoadStore 屏障。

这种保守策略总结以下:

接下来,咱们经过具体的代码来讲明:

package com.MyMineBug.demoRun.test;

class VolatileBarrierExample {
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite() {
        int i = v1;           // 第一个 volatile 读 
        int j = v2;           // 第二个 volatile 读 
        a = i + j;            // 普通写 
        v1 = i + 1;          // 第一个 volatile 写 
        v2 = j * 2;          // 第二个 volatile 写 
    }

    …                    // 其余方法 
}
复制代码

针对 readAndWrite() 方法,编译器在生成字节码时能够作以下的优化:

因此,volatile经过在volatile变量的操做先后插入内存屏障的方式,来禁止指令重排,进而保证多线程状况下对共享变量的有序性。

3.2 volatile与可见性

  可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其余线程可以当即看获得修改的值。

  在上一篇文章深刻理解java并发编程基础篇(二)-------线程、进程、Java内存模型中,咱们知道:Java内存模型规定了全部的变量都存储在主内存中,每条线程还有本身的工做内存,线程的工做内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的全部操做都必须在工做内存中进行,而不能直接读写主内存。不一样的线程之间也没法直接访问对方工做内存中的变量,线程间变量的传递均须要本身的工做内存和主存之间进行数据同步进行。因此,就可能出现线程1改了某个变量的值,可是线程2不可见的状况。

  在Java中,咱们知道被volatile修饰的变量在被修改后能够当即同步到主内存,被其修饰的变量在每次是用以前都从主内存刷新。所以,可使用volatile来保证多线程操做时变量的可见性。那么被volatile修饰的变量程序是如何让具体保证其可见性呢?这就与*内存屏障有关。
  volatile对于可见性的实现,内存屏障也起着相当重要的做用。由于内存屏障至关于一个数据同步点,他要保证在这个同步点以后的读写操做必须在这个点以前的读写操做都执行完以后才能够执行。而且在遇到内存屏障的时候,缓存数据会和主存进行同步,或者把缓存数据写入主存、或者从主存把数据读取到缓存。

  因此,内存屏障也是保证可见性的重要手段,操做系统经过内存屏障保证缓存间的可见性,JVM经过给volatile变量加入内存屏障保证线程之间的可见性。

3.3 volatile与原子性

  原子性是指一个操做是不可中断的,要么所有执行完成,要么就都不执行。

  在咱们的实际应用场景中,咱们应该知道的是:volatile是不能保证原子性的。 那么为神马volatile是不能保证原子性?

  下一篇介绍synchronized的时候,咱们会知道为了保证原子性,须要经过字节码指令monitorenter和monitorexit,可是volatile和这两个指令之间是没有任何关系的。

  根据本身的理解是:线程是CPU调度的基本单位。CPU有时间片的概念,会根据不一样的调度算法进行线程调度。当一个线程得到时间片以后开始执行,在时间片耗尽以后,就会失去CPU使用权。因此在多线程场景下,因为时间片在线程间轮换,就会发生原子性问题。

下面来看一段volatile与原子性的代码:

package com.MyMineBug.demoRun.test;

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);
    }
}
复制代码

以上代码比较简单,就是建立10个线程,而后分别执行1000次 i++ 操做。正常状况下,程序的输出结果应该是10000,可是,屡次执行的结果都小于10000。这其实就是volatile没法知足原子性的缘由。
为何会出现这种状况呢,那就是由于虽然volatile能够保证inc在多个线程之间的可见性。可是没法inc++ 的原子性。

4、总结

  volatile有序性和可见性是经过内存屏障实现的。而volatile是没法保证原子性的。 在下一下篇,咱们将深刻解析关键字synchronized

  若是以为还不错,请点个赞!!!

  Share Technology And Love Life

相关文章
相关标签/搜索