java多线程-volatile(六)

java多线程-volatile(六)

在jvm的内存模型中也有讲到过,这里希望单独拿出来再讲一遍,加强理解

volatile变量规则

volatile赋予了变量可见——禁止编译器对成员变量进行优化,它修饰的成员变量在每次被线程访问时,都强迫从内存中重读该成员变量的值;而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存,这样在任何时刻两个不同线程总是看到某一成员变量的同一个值,这就是保证了可见性.
如图所示:

不加volatile并不代表其他线程的修改就真的不可见了,只是可能有延迟;volatile的可见性只是把回写方式保存缓存一致性的架构变成直写的效果——缓存修改了立即能回映到内存;而不加volatile的则是CUP在执行该线程任务一段时间后(也许是几个时钟周期),或者切换任务后,将就修改过的缓存映射回内存。所以其他CPU对这块内存修改后的读取会有较大的延迟~~而不是完全读不到。

关键字volatile是JVM中最轻量的同步机制。volatile变量具有2种特性:

  • 保证变量的可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入,这个新值对于其他线程来说是立即可见的。
  • 屏蔽指令重排序:指令重排序是编译器和处理器为了高效对程序进行优化的手段。

volatile语义并不能保证变量的原子性。对任意单个volatile变量的读/写具有原子性,但类似于i++、i–这种复合操作不具有原子性,因为自增运算包括读取i的值、i值增加1、重新赋值3步操作,并不具备原子性。

非原子性操作
类似”a += b”这样的操作不具有原子性,在某些JVM中”a += b”可能要经过这样三个步骤:
(1)取出a和b
(2)计算a+b
(3)将计算结果写入内存
如果有两个线程t1,t2在进行这样的操作。t1在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是t2开始执行,t2执行完毕后t1又把没有完成的第三步做完。这个时候就出现了错误,相当于t2的计算结果被无视掉了。所以上面的例子在同步add方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。类似的,像”a++”这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作

由于volatile只能保证变量的可见性和屏蔽指令重排序,只有满足下面2条规则时,才能使用volatile来保证并发安全,否则就需要加锁(使用synchronized、lock或者java.util.concurrent中的Atomic原子类)来保证并发中的原子性。

  • 运算结果不存在数据依赖(重排序的数据依赖性),或者只有单一的线程修改变量的值(重排序的as-if-serial语义)
    变量不需要与其他的状态变量共同参与不变约束
  • 因为需要在本地代码中插入许多内存屏蔽指令在屏蔽特定条件下的重排序,volatile变量的写操作与读操作相比慢一些,但是其性能开销比锁低很多。

代码段说明:

package cn.thread.first.volatile_dome;

class VolatileService2 {

    private volatile int a = 0;

    public void printABC() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        a = a + 1;

        System.out.println(Thread.currentThread().getId() + ":a=" + a);
    }
}

public class VolatileDome2 {

    public static void main(String args[]) {

        final VolatileService2 service = new VolatileService2();


        new Thread(new Runnable() {
            public void run() {
                service.printABC();
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                service.printABC();
            }
        }).start();
        new Thread(new Runnable() {
            public void run() {
                service.printABC();
            }
        }).start();
    }
}

输出结果
10:a=2
11:a=2
12:a=3
有时候为:
10:a=1
11:a=2
12:a=3
解说:第一步获取a的值
第二步计算a的值
第三步赋值给啊。
三个线程同时到达,
第一步的时候获取到的a都是0,第二步计算的时候,同时对a进行+1操作。这个时候volatile把值直接写回主内存。得到的都是1,或者2,或者3

在多线程环境下,use和assign是多次出现的,但是这一操作不是原子性的,也就会有read和load之后,如果主内存count变量发生修改之后,线程工作内存中的值由于已经加载了,不会产生对应的变化,也就是私有内存和公共内存中的变量不同步的情况,所以计算出来的结果会和预期的不一样了,也就出现了非线程安全的问题。

从Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。

volatile在write时插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。

什么是内存屏障(Memory Barrier)?

内存屏障(memory barrier)是一个CPU指令。基本上,它是这样一条指令: a) 确保一些特定操作执行的顺序; b) 影响一些数据的可见性(可能是某些指令执行后的结果)。编译器和CPU可以在保证输出结果一样的情况下对指令重排序,使性能得到优化。插入一个内存屏障,相当于告诉CPU和编译器先于这个命令的必须先执行,后于这个命令的必须后执行。内存屏障另一个作用是强制更新一次不同CPU的缓存。例如,一个写屏障会把这个屏障前写入的数据刷新到缓存,这样任何试图读取该数据的线程将得到最新值,而不用考虑到底是被哪个cpu核心或者哪颗CPU执行的。

内存屏障(memory barrier)和volatile什么关系?上面的虚拟机指令里面有提到,如果你的字段是volatile,Java内存模型将在写操作后插入一个写屏障指令,在读操作前插入一个读屏障指令。这意味着如果你对一个volatile字段进行写操作,你必须知道:1、一旦你完成写入,任何访问这个字段的线程将会得到最新的值。2、在你写入前,会保证所有之前发生的事已经发生,并且任何更新过的数据值也是可见的,因为内存屏障会把之前的写入值都刷新到缓存。

总结

private volatile bealean b=true;
volatile的前面已经讲的很清楚了,可见行。用途:比如一个线程一值在for循环,另外一个线程设置一个变量b=false,跳出for循环。
在直接一点我的理解就是,volatile只有可见行,两个线程如果同时对b操作中有赋值都会导致线程不安全;如果是A线程操作操作b,而B线程使用b,这样在多线程中使用volatile就会有很好的保障。

package cn.thread.first.volatile_dome;

class VolatileService2 {

    private volatile int a = 0;
    public volatile boolean b = true;

    public void printABC() {

        while (b) {
            a++;
            System.out.println(Thread.currentThread().getId() + ":a=" + a);
        }
        System.out.println("end");


    }
}

public class VolatileDome2 {

    public static void main(String args[]) {

        final VolatileService2 service = new VolatileService2();


        new Thread(new Runnable() {
            public void run() {
                service.printABC();
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                service.b=false;
                service.printABC();
            }
        }).start();
    }
}

参看文献:

http://www.cnblogs.com/Mainz/p/3556430.html

猜你喜欢

转载自blog.csdn.net/piaoslowly/article/details/81476019