volatile关键字深度讲解

目录

cpu缓存

Java内存模型(Java Memory Model,JMM)

并发编程核心问题

1.原子性

2.可见性

3.有序性

剖析volatile关键字

1.volatile关键字的语义

2.volatile的原理和实现机制

3.volatile不适用的场景

4.应用

总结


cpu缓存

1.CPU里面就有了高速缓存:数据执行是在cpu,数据存储在内存,内存给cpu传数据速度慢,cpu速度快,所以在cpu里面有一个缓存,把需要的数据先放在cpu的缓存中,执行结束,一起高速缓存中的结果冲刷到主存中

现在面临问题:缓存不一致,解决方法:

1.通过在总线加LOCK#锁的方式:

在锁住总线程期间,其他CPU无法访问内存,导致效率低下。

2.通过缓存一致性协议

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

Java内存模型(Java Memory Model,JMM)

1.目的:屏蔽各个硬件平台和操作系统的内存访问差异以实现让Java程序在各种平台下都能达到一致的内存访问效果。

2.内容:它定义了程序中变量的访问规则,定义了程序执行的次序。

3.Java内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行速度,也没有限制编译器对指令进行重排序

优势:可以获得较好的执行性能,

缺点:存在缓存一致性问题和指令重排序的问题。

4.Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

并发编程核心问题

想要并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

原子性

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

可见性

当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

有序性

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

指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

1.原子性

a. 基本类型的读取和赋值操作,且赋值必须是数字赋值给变量(x=6)

    变量之间的相互赋值不是原子性操作。(x++和 x = x+1包括3个操作:读取x的值,进行加1操作,写入新的值。不是原子操作)

b.所有引用reference的赋值操作

c.java.concurrent.Atomic.* 包中所有类的一切操作

Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证了原子性。

2.可见性

对于可见性,Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值

普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。synchronized和Lock也能够保证可见性

3.有序性

在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

在Java里面,可以通过volatile关键字来保证一定的“有序性”,也可以通过synchronized和Lock来保证有序性。

Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

happens-before原则(先行发生原则):

  1.     程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  2.     锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  3.     volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  4.     传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  5.     线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  6.     线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  7.     线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  8.     对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

剖析volatile关键字

1.volatile关键字的语义

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1. 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
  2. 禁止进行指令重排序。
  • 当程序执行到volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;
  • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。
  • 但是volatile没办法保证对变量的操作的原子性。

2.volatile的原理和实现机制

下面这段话摘自《深入理解Java虚拟机》:“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”

lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

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

2. 它会强制将对缓存的修改操作立即写入主存;

3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。

3.volatile不适用的场景

(1)volatile不适合复合操作:inc++不是一个原子性操作,可以由读取、加、赋值3步组成,

(2)解决方法

   1.方法返回值加synchronized

   2.采用Lock     加锁:lock.lock   解锁:lock.unlock

   3.采用java并发包中的原子操作类,原子操作类是通过CAS循环的方式来保证其原子性的

4.应用

 1.单例模式的双重锁为什么要加volatile

需要volatile关键字的原因是,在并发情况下,如果没有volatile关键字,在第5行会出现问题。instance = new TestInstance();可以分解为3行伪代码

   1.memory = allocate()   //分配内存

   2. ctorInstanc(memory)   //初始化对象

   3. instance = memory  //设置instance指向刚分配的地址

上面的代码在编译运行时,可能会出现重排序从1-2-3排序为1-3-2。在多线程的情况下会出现以下问题。线程A在执行第5行代码时,B线程进来,而此时A执行了1和3,没有执行2,此时B线程判断instance不为null,直接返回一个未初始化的对象。

2.ConcurrentHashMap的源代码中,volatile有一处经典的应用

Segment的get方法在整个过程中不加锁,除非读到的值为null或者空,才会加锁重新读

Hashtable的get方法是加锁的(synchronized )。

ConcurrentHashMap的get方法中使用的共享变量,都定义成了volatile类型

例如用于统计当前Segment大小的count字段和用于存储值的HashEntry的value。定义成volatile类型的变量,

能够在多线程之间保持可见性,多线程可以同时读,并且读到的都是最新的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖原值),在get操作里只需要读,不需要写count变量。之所以能够保证多线程读到的都是最新的值,是因为根据Java内存模型的happen before原则,对于volatile字段的写入操作先于读操作,也就是说,2个线程同时修改和读取volatile变量的值,写操作先执行,读操作后执行。这就是volatile的经典应用之一。

Segment中(或者说ConcurrentHashMap)count全局变量的定义如下:

/**

* The number of elements in this segment's region.

*/

transient volatile int count;

Hashtable中count全局变量的定义如下:

/**

* The total number of entries in the hash table.

*/

private transient int count;

总结

volatile每次读取前必须先从主内存刷新最新的值。

每次写入后必须立即同步回主内存当中。

比如i++分为:①i取值;②i+1;③结果赋值给i ---volatile操作指保证②③ 没有办法保证①,因为已经读取

volatile的使用过程中很容易出现的一个问题是:错把volatile变量当做原子变量。主要是因为volatile关键字使变量的读、写具有了“原子性”。然而这种原子性仅限于变量(包括引用)的读和写,无法涵盖变量上的任何操作即:

  • 基本类型的自增(如count++)等操作不是原子的。
  • 对象的任何非原子成员调用(包括成员变量和成员方法)不是原子的。

如果想要具有原子性,那么只能采取锁、原子变量更多的措施。

综上,其实volatile保持内存可见性和防止指令重排序的原理,本质上是同一个问题,也都依靠内存屏障得到解决。用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最新值。volatile很容易被误用,用来进行原子性操作。

被写入 volatile 变量的这些有效值独立于任何程序的状态,包括变量的当前状态。事实上,我的理解就是上面的2个条件需要保证操作是原子性操作,才能保证使用volatile关键字的程序在并发时能够正确执行。

到此结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值