学习笔记-volatile

volatile-与java内存有关

1.内存模型相关概念

若一个变量在多个CPU都存在缓存(多线程编程才会出现),那么可能就会出现缓存不一致的情况,为了解决这一问题,有以下两种方法:

    <<1在总线加LOCK#锁方式

 CPU与其他部件的通信是通过总线来进行的,若对总线加LOCK#锁,阻塞其他CPU对其他部件(如内存)的访问(?????),从而只有一个CPU能使用这个变量的内存。

若一个线程执行 i=i+1,在执行这段代码的过程中,总线发出了LOCK#锁的信号,那么只有等待这段代码完全执行完毕之后,其他CPU才能在变量i所在的内存读取变量,然后在进行相应的操作。

但是这种方法有问题,在锁总线的期间,其他CPU无法访问内存,导致效率低下

 

(1)数据总线DB(Data Bus):用于CPU与主存储器、CPU与I/O接口之间传送数据。数据总线的宽度(根数)等于计算机的字长。
(2)地址总线AB(Address Bus):用于CPU访问主存储器或外部设备时,传送相关的地址。此地址总线的宽度决定CPU的寻址能力。
(3)控制总线CB(Control Bus):用于传送CPU对主存储器和外部设备的控制信号。

    <<2通过缓存一致性协议

最出名的是Intel的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的,它的核心思想是:当CPU写数据时,若发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存置为无效状态,因此其他CPU需要读取这个变量时,会发现自己缓存中的该变量缓存是无效的,那么会在内存中重新读取。

注:上述这两方法都是在硬件层面提供的方法

2.并发编程中的三个概念

<<1原子性

即一个操作或多个操作,要么全部执行并且执行过程中不会被任何因素打断,要么都不执行。

如:A向B转1000元,有两个操作:从A账户上减1000,B账户上加1000

<<2可见性

当一个线程修改了共享变量的值,其他线程立即看得到

<<3有序性

即程序执行的顺序按照代码的先后顺序执行

int i=0;
boolean flag = false;
i = 1;    //语句1
flag = true;  //语句2

 

在执行上述代码时,语句1一定会在语句2前面么?为什么?

答:不一定,会发生指令的重排序。处理器会为了提高程序的运行效率,要对输入代码进行优化,它不能保证程序执行的顺序与代码书写的顺序一致,但是结果会一致,因为JVM、编译器、处理器都会遵从as-if-serial语义。

3.java内存模型(JMM)

在JVM中试图定义一种java内存模型(java memory model)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现java程序在各种平台上达到一致的内存访问效果。

JMM中定义了程序执行的次序,也存在缓存一致性和指令重排序问题

JMM中规定所有变量都存在主存中(类似于物理内存)

每个线程都有自己的工作内存(类似于高速缓存),线程对变量的所有操作都必须在工作内存中执行,不能直接对主存进行操作且每个线程都不能访问其他线程的工作内存。

<<1原子性:JMM只保证了基本的读取和赋值是原子性操作

                     由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,从而保证原子性

<<2可见性:java提供了volatile关键字来保证可见性

                    当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主存中,当其他线程读取时读取到的是最新值

<<3有序性:在JMM中,允许编译器和处理器对指令的重排序,重排序不会影响单线程程序的执行,但会影响多线程并发执行的正确性

                     在java中,通过volatile保证原子性,也可通过synchronized和Lock保证有序性。

JMM具有先天的“有序性”,不需要任何手段可保证有序性,通常称为happens-before原则

4.深入剖析volatile关键字

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰后,有2层含义,一是保证了不同线程对这个变量的操作的可见性,二是禁止指令重排序

volatile不能保证对变量的操作都是原子性,可以保证有序性,禁止指令重排序

volatile的原理和实现机制

观察加入volatile和没有加入volatile所生成的汇编代码发现,加入volatile时,会多出一个Lock前缀指令

Lock前缀指令相当于一个内存屏障,会提供3个功能:

(1)确保指令重排序时不会把后面的指令重排到内存屏障之前,也不会把前面的指令排到内存屏障之后,即在执行到内存屏障这句指令时,在它前面的操作全部完成

(2)强制对缓存的修改立即写入主存

(3)若是写操作,它会导致其他CPU中对应的缓存失效

 

对于volatile变量执行写操作时,会在操作后加一条store屏障指令,强制将写的值更新到主内存中去;

对于volatile变量执行读操作时,会在操作前加一条load屏障指令,会强制使缓冲区的缓存失效,要读取volatile时,会从主内存中读取到最新值。

通俗点来讲就是,volatile变量每次被线程访问时,都会强迫从主内存中重新读取该变量的值,当变量发生变化时,又会强迫将最新值更新到主内存中。

这样的话,各个线程看到的都是该值的最新值。

 

int volatile number=0;

number++;    //此操作是非原子性操作

比如:number=5;

线程A和线程B都要访问number,A首先访问到了number,为5,但是现在B竞争到了cpu,且cpu分配给它的时间足够线程B执行完number++,所以线程B的工作内存中number为6,,也更新到了主内存中,主内存的number为6;这时线程A竞争到了cpu,开始执行刚才没执行完的代码,此时线程A的工作内存中number为6,也更新到了主内存中。虽然进行了两次操作,但是最后只加了一次1.

所以说,volatile不能保证变量的原子性。

那么怎么样才能保证number自加的原子性呢??

答:1>使用synchronized/Lock

       2> 使用AtomicInteger

          java.util.concurrent.atomic包下提供了一些原子性的类,自加、自减、加法、减法进行了封装,保证这些操作都是原子性

       3>使用ReentrantLock

          在java.util.concurrent.locks包中

 


 

 

    

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值