volatile与java内存模型的那点关系

有一半(1/3+1/6)的关系!

因为java内存模型的三个特性,volatile满足了两个!

1、可见性(满足)

2、有序性(满足了一半)

3、原子性(不满足)

下面解释下:

可见性:多个线程使用到同一个变量时,一旦有一个线程修改了变量,其他线程再读取时一定是读取到这个修改后的值。

有序性:程序执行的顺序按照代码的先后顺序执行。不会发生指令重排序,或者指令重排序在多线程环境下显现出有序性!

原子性:一个不可以再分割的操作即为原子操作。如:a = 0(a为int类型);

 

关于有序性(满足了一半),我下面举两个例子说明一下!

演示下指令重排序的例子1:(a加了volatile修饰,依然会发生指令重排序的!!)

public class Test {
    private volatile int a;
    private int b;


    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            final Test test = new Test();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    test.a = 1;
                    test.b = 1;//发生指令重排序的话,b会被先赋值,a还是默认值0
                }
            }).start();
            new Thread(new Runnable() {
                @Override
                public void run() {
                    if (0 == test.a && test.b == 1) {
                        System.out.println("发生指令重排序!");
                    }
                }
            }).start();
        }
    }
}

运行了几次,控制台打印出了:

发生指令重排序!

 

下面再举例一个volatile“禁止”了指令重排序的例子2:(指令重排序会有内存屏障)

参考自:https://blog.csdn.net/weixin_37817685/article/details/80261549

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;
        }
}

注意:singleton = new Singleton(),这不是一个原子操作,一共有3个步骤:

1. 给 singleton 分配内存
2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null了)

假如没有volatile修饰,有两个线程同时调用了getInstance()方法,其中一个线程A执行到singleton = new Singleton();这一句,发生了指令重排序【1-3-2】,当2还没有执行的时候,线程B发现instance不为null,直接返回instance(此时instance还没有成员变量的信息,因为2还没有执行),线程B使用instance就可能会出错了。

volatile关键字的一个作用是“禁止”指令重排,把instance声明为volatile之后,对它的写操作就会有一个内存屏障,这样,在它的赋值完成之前,就不用会调用读操作。

注意:volatile阻止的不是singleton = new Singleton()这句话内部【1-2-3】的指令重排,而是保证了在一个写操作【1-2-3】或者【1-3-2】完成之前,不会调用读操作(if (instance == null))。

从例子2可以看出,我必须等到你写完了才能读(volatile的写屏障),表现出来的指令重排序在多线程环境下显现出有序性。

 

下面说一下volatile满足了可见性却不满足原子性的问题!代码如下:

    private volatile static int count = 0;

    public static void main(String[] args){
        for (int i = 0; i < 10000; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    count++;
                    System.out.println("count=" + count);
                }
            }).start();
        }
    }

 有时打印到9999,有时打印到9998。是不是发现不对劲了,看看上面的可见性:

可见性:多个线程使用到同一个变量时,一旦有一个线程修改了变量,其他线程再读取时一定是读取到这个修改后的值。

其实是因为count++(count = count + 1)并非是一个原子操作,看下图:

图片来源:https://blog.csdn.net/strivenoend/article/details/80440884

 

例如当主存中count变量为99,现在线程A去Read主存中的count,并存到线程内部工作栈99,然后执行count+1了,这时,另一个线程已经将主存中的count更新为100了,这会使得线程内部工作栈count为99的副本失效(使缓存行无效)。但是线程A并不需要去读取主存中的count(100),而是将自己运算的count+1(100)写到线程的缓存行,然后再写回主存,还是100!!!两个线程都count++了,结果count才加了1。

出现上述问题的原因是,count++不是一个原子操作,volatile不满足原子性。

 

如果对您有用的话赞一下呗!谢谢!

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值