java 深入理解volatile关键字

一、首先要说明Java内存模型:参考资料

1、Java为了保证其平台性,使Java应用程序与操作系统内存模型隔离开,需要定义自己的内存模型。在Java内存模型中,内存分为主内存工作内存两个部分,其中主内存是所有线程所共享的,而工作内存则是每个线程分配一份,各线程的工作内存间彼此独立、互不可见,在线程启动的时候,虚拟机为每个线程分配一块工作内存,不仅包含了线程内部定义的局部变量,也包含了线程所需要使用的共享变量(非线程内构造的对象)的副本,即为了提高执行效率,读取副本比直接读取主内存更快(这里可以简单地将主内存理解为虚拟机中的堆,而工作内存理解为栈(或称为虚拟机栈),栈是连续的小空间、顺序入栈出栈,而堆是不连续的大空间,所以在栈中寻址的速度比堆要快很多)。工作内存与主内存之间的数据交换通过主内存来进行

2、工作内存可以说是主内存的一份缓存,为了避免缓存的不一致性,所以volatile需要废弃此缓存。但除了内存缓存之外,在CPU硬件级别也是有缓存的,即寄存器。假如线程A将变量X由0修改为1的时候,CPU是在其缓存内操作,没有及时回写到内存,那么JVM是无法X=1是能及时被之后执行的线程B看到的,所以JVM在处理volatile变量的时候,也同样用了硬件级别的缓存一致性原则。

二、关于volatile关键字

1、对于一个变量的修改操作,在内存中是这样的,先从变量的地址复制一份出来,修改复制的值,然后把修改后的值写回到该对象原地址。

2、volatile指出 变量是随时可能发生变化的,每次使用它的时候必须从它的地址中读取,编译器生成的汇编代码会重新从变量的地址读取数据。

3、编译器优化做法是,由于编译器发现两次从变量读数据的代码之间的代码没有对变量进行过操作,它会自动把上次读取的数据拿来用。而不是重新从变量的地址读。

4、单例的双重检测

public class ReportController {
    private static final String TAG = "ReportController";
    private volatile static ReportController mInstance;
    private static ActionLogEvent mEvent = new ActionLogEvent();

    private ReportController() {
    }

    public static ReportController init() {
        if (mInstance == null) {
            synchronized (ReportController.class) {
                if (mInstance == null) {
                    mInstance = new ReportController();
                }
            }
        }
        return mInstance;
    }
}

双重检测是为了提高效率,如果不同步可能产生多个对象,如果将整个函数同步了那么每次都要同步,其实读是不用同步的,这样写只有在第一需要同步,而在以前这样写也会不对,因为可能重排序是使instance先获得地址,而实例却还没写入地址。后来volatile不准重排序就解决了这个问题。

注:关于重排序,使instance先获得地址,而实例却还没写入地址【 instance要加volatile的原因
创建对象过程,1、给要创建的对象分配内存   2、调用对象构造函数实例化成员变量   3、把instance指向对象内存地址

由于重排序,会导致2、3调用顺序不一定相同,如果顺序为1-3-2,线程A执行完3未执行2时,线程B切入读取instance,之后A线程执行了2,此时线程A与线程B的工作内存中instance副本包含的内容就不同了。

三、总结

对volatile变量的写,会立即从线程的工作内存写入到主存中,并且会使其他线程工作内存中的变量失效,迫使它重读。而普通变量不会这样。声明变量是volatile的,JVM保证了每次读变量都从内存中读,跳过CPU cache这一步。

1、volatile修饰变量的可见性理解:

volatile的第一条语义是保证线程间变量的可见性,简单地说就是当线程A对变量X进行了修改后,在线程A之后的其他线程能看到变量X的变动,本质原因:

线程对变量进行修改之后,要立刻回写到主内存。
线程对变量读取的时候,要从主内存中读,而不是缓存。


2、volatile的第二条语义:禁止指令重排序

1)指令重排序(为了避免CPU内存操作速度远慢于CPU运行速度,导致CPU空置,虚拟机/CPU按一定规则将程序编写顺序打乱)

volatilesynchronized可以禁用重排序

虚拟机层面,为了尽可能减少内存操作速度远慢于CPU运行速度所带来的CPU空置的影响,虚拟机会按照自己的一些规则(这规则后面再叙述)将程序编写顺序打乱——即写在后面的代码在时间顺序上可能会先执行,而写在前面的代码会后执行——以尽可能充分地利用CPU。

硬件层面,CPU会将接收到的一批指令按照其规则重排序,同样是基于CPU速度比缓存速度快的原因,和上一点的目的类似,只是硬件处理的话,每次只能在接收到的有限指令范围内重排序,而虚拟机可以在更大层面、更多指令范围内重排序。

2)重排序的部分规则

a.程序次序规则(Program Order Rule):在一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是代码顺序,因为要考虑分支、循环等结构。【 一个线程内,代码块之间是有序的
b.监视器锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个对象锁的lock操作。这里强调的是同一个锁,而“后面”指的是时间上的先后顺序,如发生在其他线程中的lock操作。【 同一个锁,在不同线程都有使用时,在一个线程中已经lock那么要等该线程中锁unlock之后才会发生在另一个线程对该锁的lock操作
c.volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作发生于后面对这个变量的读操作,这里的“后面”也指的是时间上的先后顺序。【 一个线程读了volatile变量后,要等它写回主内存后,后面线程才能读
d.线程启动规则(Thread Start Rule):Thread独享的start()方法先行于此线程的每一个动作。【 thread内的操作一定在它的start方法之后
e.线程终止规则(Thread Termination Rule):线程中的每个操作都先行发生于对此线程的终止检测,我们可以通过 Thread.join()方法结束、 Thread.isAlive()的返回值检测到线程已经终止执行。【 线程中每个操作优先先行发生在线程终止检测(join()/isAlive())之前
f.线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用优先于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否已中断。【 interrupt()优先于interrupted()之前发生
g.对象终结原则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。【 对象的构造函数先行发生于它的finalize()
h.传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。【 A先于B,B先于C,那么A一定先于C


3、volatile不能保证原子性的理解

volatile不能保证复合操作的原子性

Demo:线程A在做了i+1,但未赋值的时候,线程B就开始读取i,那么当线程A赋值i=1,并回写到主内存,而此时线程B已经不再需要i的值了,而是直接交给处理器去做+1的操作,于是当线程B执行完并回写到主内存,i的值仍然是1,而不是预期的2。也就是说,volatile缩短了普通变量在不同线程之间执行的时间差,但仍然存有漏洞,依然不能保证原子性

注意:只有在对变量读取频率很高的情况下,虚拟机才不会及时回写主内存,而当频率没有达到虚拟机认为的高频率时,普通变量和volatile是同样的处理逻辑。如在每个循环中执行System.out.println(1)加大了读取变量的时间间隔,使虚拟机认为读取频率并不那么高,所以实现了和volatile的效果。volatile的效果在jdk1.2及之前很容易重现,但随着虚拟机的不断优化,如今的普通变量的可见性已经不是那么严重的问题了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值