一分钟读懂Java中的volatile关键字

一、volatile简介
volatile意为,不稳定的,反复无常的。
下边我们具体阐述volatile的三个特性:

  • volatile能保证内存可见性
  • volatile不能保证原子性
  • volatile禁止指令重排序(有序性)

它作为Java中的一个关键字,用来声明变量的值可能随时会受到其它线程的修改,使用volatile修饰的变量被修改后会立即强制将修改后的值写入主存,主存中的值的更新会使缓存中的值失效 。也就是当一条线程修改了共享变量的值,新值对于其他线程来说是可以立即得知的(volatile能保证内存可见性)。

为什么出现这种情况呢 ,我们需要先了解一下JMM
实际上当程序运行时,JVM会为每一个执行任务的线程独立的分配一个缓存,用于提高程序的执行效率。java虚拟机有自己的内存模型(Java Memory Model,JMM),JMM可以屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。JMM决定一个线程对共享变量的写入何时对另一个线程可见,JMM定义了线程和主内存之间的抽象关系:共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存保存了被该线程使用到的主内存的副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。这三者之间的交互关系如下:
在这里插入图片描述
了解了JMM的简单定义后,问题就很容易理解了,对于普通的共享变量来讲,当有两个线程T1和线程T2同时操作主线程中的数据的时候,线程T1先从主存中将数据读取到自己缓存空间,对数据进行修改,然后将来把修改的数据刷新回主存中去。但是在写入主存之前线程T2来了,线程T2比线程T1先读取到主存中的数据,线程T2读到的是线程T1还没有刷新回主存之前的数据。产生这种现象的原因是多个线程在操作共享数据时,对于共享数据的操作彼此是不可见的,所以就导致了上述的问题。

那么这种共享变量在多线程模型中的不可见性如何解决呢?
比较粗暴的方式自然就是加锁,但是此处使用synchronized或者Lock这些方式太重量级了,使用同步锁机制sychronized可以保证每次都刷新缓存,但是sychronized枷锁、释放锁、效率低,多个线程操作时,只有一个线程使用,其他线程只能处于等待状态。这时候可以使用volatile关键字,用volatile修饰变量时不会执行加锁操作,因此也就不会使执行线程阻塞,当多个线程进行操作共享数据时,可以保证内存中的数据可见,相较于synchronized是一种较为轻量级的同步策略,我们可以这样理解它的操作就是在主存中进行操作的。使用volatile修饰能保证,一个线程修改的值,对另外一个线程是立刻知道的,但是volatile只能保证内存可见性,不能保证原子性。volatile解决的是变量在多个线程之间的可见性,像i++这种操作volatile无法保证其原子性。

volatile不能保证原子性
原子是世界上的最小单位,具有不可分割性。比如 i=0 这个操作是不可分割的,那么我们说这个操作是原子操作。再比如:i++这个操作实际是i = i +1是可分割的,所以它不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作,一个操作是原子操作,那么我们称它具有原子性。在Java中synchronized 和lock、unlock 中操作保证原子性。

volatile禁止指令重排序(有序性)
先看下jvm中指令重排的定义:
指令重排序是JVM为了优化指令,提高程序运行效率,在不影响单线程程序执行结果的前提下,尽可能地提高并行度。编译器、处理器也遵循这样一个目标。注意是单线程。多线程的情况下指令重排序就会给程序员带来问题。
关于指令重排,可以通过单例模式的经典模式双重加锁来详细了解:
在这里插入图片描述
双重加锁单例模式是一种懒汉式的加载模式,为了减少锁的消耗会再函数入口处提前判空,再在锁的代码段落内初始化实例。但实际上初始化的代码并非原子操作:
在这里插入图片描述
这个步骤,其实在jvm里面的执行分为三步:
1.在堆内存开辟内存空间。
2.在堆内存中实例化SingleTon里面的各个参数。
3.把对象指向堆内存空间。

由于jvm存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,singleton已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。
解决办法:
由于 volatile 禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。

volatile实现原理:
如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,该Lock指令会使这个变量所在缓存行的数据回写到系统内存,根据缓存一致性协议,每个处理器都会通过嗅探在总线上传输的数据来检查自己缓存的值是否已过期,当处理器发现自己的缓存行对应的地址被修改,就会将当前处理器的缓存行设置成无效状态,在下次访问相同内存地址时,强制执行缓存行填充。

内存屏障
上面总结到volatile关键字可以实现变量在各线程中的一致性,并且具有禁止指令重排的功能。其实这两个特性是通过内存屏障来实现的.
内存屏障是jvm上的指令,jvm上还有其它指令例如:

(1) lock:将主内存中的变量锁定,为一个线程所独占

(2) unclock:将lock加的锁定解除,此时其它的线程可以有机会访问此变量

(3) read:将主内存中的变量值读到工作内存当中

(4) load:将read读取的值保存到工作内存中的变量副本中。

(5) use:将值传递给线程的代码执行引擎

(6) assign:将执行引擎处理返回的值重新赋值给变量副本

(7) store:将变量副本的值存储到主内存中。

(8) write:将store存储的值写入到主内存的共享变量当中。

发布了30 篇原创文章 · 获赞 12 · 访问量 3460

猜你喜欢

转载自blog.csdn.net/zx1293406/article/details/103548776