三大性质总结:原子性、可见性以及有序性

1. 三大性质简介

在并发编程中分析线程安全的问题时每每须要切入点,那就是两大核心:JMM抽象内存模型以及happens-before规则(在这篇文章中已经通过了),三条性质:原子性,有序性和可见性。关于synchronizedvolatile已经讨论过了,就想着将并发编程中这两大神器在 原子性,有序性和可见性上作一个比较,固然这也是面试中的高频考点,值得注意。java

2. 原子性

原子性是指一个操做是不可中断的,要么所有执行成功要么所有执行失败,有着“同生共死”的感受。及时在多个线程一块儿执行的时候,一个操做一旦开始,就不会被其余线程所干扰。咱们先来看看哪些是原子操做,哪些不是原子操做,有一个直观的印象:面试

int a = 10; //1编程

a++; //2安全

int b=a; //3性能优化

a = a+1; //4并发

上面这四个语句中只有第1个语句是原子操做,将10赋值给线程工做内存的变量a,而语句2(a++),实际上包含了三个操做:1. 读取变量a的值;2:对a进行加一的操做;3.将计算后的值再赋值给变量a,而这三个操做没法构成原子操做。对语句3,4的分析同理可得这两条语句不具有原子性。固然,java内存模型中定义了8中操做都是原子的,不可再分的。app

  1. lock(锁定):做用于主内存中的变量,它把一个变量标识为一个线程独占的状态;
  2. unlock(解锁):做用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定
  3. read(读取):做用于主内存的变量,它把一个变量的值从主内存传输到线程的工做内存中,以便后面的load动做使用;
  4. load(载入):做用于工做内存中的变量,它把read操做从主内存中获得的变量值放入工做内存中的变量副本
  5. use(使用):做用于工做内存中的变量,它把工做内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个须要使用到变量的值的字节码指令时将会执行这个操做;
  6. assign(赋值):做用于工做内存中的变量,它把一个从执行引擎接收到的值赋给工做内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操做;
  7. store(存储):做用于工做内存的变量,它把工做内存中一个变量的值传送给主内存中以便随后的write操做使用;
  8. write(操做):做用于主内存的变量,它把store操做从工做内存中获得的变量的值放入主内存的变量中。

上面的这些指令操做是至关底层的,能够做为扩展知识面掌握下。那么如何理解这些指令了?好比,把一个变量从主内存中复制到工做内存中就须要执行read,load操做,将工做内存同步到主内存中就须要执行store,write操做。注意的是:java内存模型只是要求上述两个操做是顺序执行的并非连续执行的。也就是说read和load之间能够插入其余指令,store和writer能够插入其余指令。好比对主内存中的a,b进行访问就能够出现这样的操做顺序:read a,read b, load b,load ajvm

由原子性变量操做read,load,use,assign,store,write,能够大体认为基本数据类型的访问读写具有原子性(例外就是long和double的非原子性协定)ide

synchronizedpost

上面一共有八条原子操做,其中六条能够知足基本数据类型的访问读写具有原子性,还剩下lock和unlock两条原子操做。若是咱们须要更大范围的原子性操做就可使用lock和unlock原子操做。尽管jvm没有把lock和unlock开放给咱们使用,但jvm以更高层次的指令monitorenter和monitorexit指令开放给咱们使用,反应到java代码中就是---synchronized关键字,也就是说synchronized知足原子性

volatile 咱们先来看这样一个例子:

public class VolatileExample {
    private static volatile int counter = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++)
                        counter++;
                }
            });
            thread.start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}
复制代码

开启10个线程,每一个线程都自加10000次,若是不出现线程安全的问题最终的结果应该就是:10*10000 = 100000;但是运行屡次都是小于100000的结果,问题在于 volatile并不能保证原子性,在前面说过counter++这并非一个原子操做,包含了三个步骤:1.读取变量counter的值;2.对counter加一;3.将新值赋值给变量counter。若是线程A读取counter到工做内存后,其余线程对这个值已经作了自增操做后,那么线程A的这个值天然而然就是一个过时的值,所以,总结果必然会是小于100000的。

若是让volatile保证原子性,必须符合如下两条规则:

  1. 运算结果并不依赖于变量的当前值,或者可以确保只有一个线程修改变量的值;
  2. 变量不须要与其余的状态变量共同参与不变约束

3. 有序性

synchronized

synchronized语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其余线程只能等待。所以,synchronized语义就要求线程在访问读写共享变量时只能“串行”执行,所以synchronized具备有序性

volatile

在java内存模型中说过,为了性能优化,编译器和处理器会进行指令重排序;也就是说java程序自然的有序性能够总结为:若是在本线程内观察,全部的操做都是有序的;若是在一个线程观察另外一个线程,全部的操做都是无序的。在单例模式的实现上有一种双重检验锁定的方式(Double-checked Locking)。代码以下:

public class Singleton {
    private Singleton() { }
    private volatile static Singleton instance;
    public Singleton getInstance(){
        if(instance==null){
            synchronized (Singleton.class){
                if(instance==null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
复制代码

这里为何要加volatile了?咱们先来分析一下不加volatile的状况,有问题的语句是这条:

instance = new Singleton();

这条语句实际上包含了三个操做:1.分配对象的内存空间;2.初始化对象;3.设置instance指向刚分配的内存地址。但因为存在重排序的问题,可能有如下的执行顺序:

不加volatile可能的执行时序

若是2和3进行了重排序的话,线程B进行判断if(instance==null)时就会为true,而实际上这个instance并无初始化成功,显而易见对线程B来讲以后的操做就会是错得。而用volatile修饰的话就能够禁止2和3操做重排序,从而避免这种状况。volatile包含禁止指令重排序的语义,其具备有序性

4. 可见性

可见性是指当一个线程修改了共享变量后,其余线程可以当即得知这个修改。经过以前对synchronzed内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,synchronized具备可见性。一样的在volatile分析中,会经过在指令中添加lock指令,以实现内存可见性。所以, volatile具备可见性

5. 总结

经过这篇文章,主要是比较了synchronized和volatile在三条性质:原子性,可见性,以及有序性的状况,概括以下:

synchronized: 具备原子性,有序性和可见性volatile:具备有序性和可见性

参考文献

《java并发编程的艺术》

《深刻理解java虚拟机》

相关文章
相关标签/搜索