Java并发编程知识点总结(七)——原子性、有序性、可见性

这一节来对比下synchronized和volatile关键字在三大性质中的不同。

1. 原子性

原子性是指一个操作是不可中断的,要么全部执行成功,要么全部执行失败。即使在多线程情况下,也能保证不被其它线程干扰。

我们来看下面几个例子

int a = 10;  // 1 
++a;  // 2
int b = a;  // 3
a = a+1;  // 4

在上面的三个操作中,只有第一个操作时具有原子性的。第二个自增操作实际上时分为三步的:取a的值、对a的值+1、赋值给a,所以是无法保证原子性的。3、4同理也不具有原子性。

在JMM内存模型中,只有如下8个操作是具有原子性的

  1. lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态。
  2. unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来。
  3. read(读取):作用于主内存中的变量,它把从主内存中读取的变量传送到工作内存,以便后面的load。
  4. load(载入):作用于工作内存的变量,将read操作读取的变量放入工作内存中的变量副本。
  5. use(使用):作用域工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎。
  6. assign(赋值):作用于工作内存中的变量,它把从执行引擎计算的变量赋值给工作内存的变量。
  7. store(存储):作用于工作内存的变量,它把工作内存中的变量值送给主内存中以便随后的write操作。
  8. write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存中。

值得注意的是:Java内存模型只是要求上述两个操作时顺序执行的而不是连续执行的。也就是说read和load之间可以插入其它指令,store和write可以插入其它指令。例如可以出现这样的顺序:read a,read b,load b,load a。

synchronized

synchronized关键字的实现原理是基于monitorenter和monitorexit,其实就相当于lock和unlok的原子性操作。
因此,synchronized是满足原子性的

volatile

我们来看下面的代码

public class Demo1 {
    private static volatile int num = 0;
    public static void add(){
        for(int i=0;i<1000;i++){
            num++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for(int i=0;i<10;i++){
            Thread thread = new Thread(){
                @Override
                public void run() {
                    Demo1.add();
                }
            };
            thread.start();
        }
        Thread.sleep(1000);
        System.out.println(Demo1.num);
    }
}

实际的执行结果可能是1000、也可能是9991,9983…
我们可以设想这样一个场景:首先线程A从主内存中拷贝了一份变量副本到工作内存中,但是还没来得及修改,就被阻塞了。这时,线程B也从主内存中读取了变量到工作内存中,因为这时num还没有被修改,所以是有效的。然后线程B对num进行了自增操作,并写回到了主内存中。虽然volatile关键字是具有可见性的,但是由于线程A已经完成了读取的这个原子性操作了,所以就不会再检查。这时线程A会直接将100加1,然后写回到主内存中。这就造成了数据不一致的问题。因此volatile不能使得本身不具有原子性的操作变成具有原子性的

因此,volatile操作是不满足原子性的

2. 有序性

Java程序的天然的有序性可以解释为:如果在本线程内观察,所有的操作都是有序的;如果在一个线程观察另外一个线程,所有操作都是无序的。

synchronized

synchronized关键字使得在同一个时刻,只能有一个线程获得监视器,进入临界区。其实也就相当于,要求读写共享变量是串行执行的

因此synchronized操作是满足有序性的

volatile

我们利用懒汉式的双重检验单例模式(有篇文章介绍双重检验单例模式讲的很好,值得一看)来分析volatile是否具有有序性。代码如下:

public class Demo {
    public volatile static Demo demo;
    private Demo(){

    }

    public Demo getDemo(){
        if(demo==null){
            synchronized (Demo.class){
                if(demo==null){
                    demo = new Demo();
                }
            }
        }
        return demo;
    }
}

创建一个对象实际分为三步:

  1. 分配对象的内存空间
  2. 初始化对象
  3. 设置demo指向刚分配的内存地址

第一步和第二步是存在依赖关系的,不能被重排序。而第三步和第二步没有依赖关系,实际上是可以重排序的。这样就有可能出现这种情况:
在这里插入图片描述

如果2和3进行了重排序,就有可能造成线程B拿到了没有被初始化的对象。而volatile关键字能够禁止操作2和3的重排序,从而避免这种情况。

因此,volatile关键字是满足有序性的

3. 可见性

可见性是指当一个线程修改了共享变量后,其它线程能够立即得知这个修改

synchronized

synchronized保证,当线程获取到锁的时候,会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。

因此synchronized关键字是满足可见性的。

volatile

由于lock前缀指令,volatile写之后会使得其它缓存中的共享变量也失效,强制从主内存中重新读取,从而实现可见性。

因此volatile关键字是满足可见性的。

4. 总结

synchronized:原子性、有序性、可见性
volatile:有序性、可见性。

参考了这篇文章:三大性质总结:原子性,有序性,可见性

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值