volatile 关键字详解

volatile 关键字详解

volatile 关键字详解

01

概念

1 volatile变量,用来确保将变量的更新操做通知到其余线程。
2 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,所以不会将该变量上的操做与其余内存操做一块儿重排序。
3 volatile变量不会被缓存在寄存器或者对其余处理器不可见的地方,所以在读取volatile类型的变量时总会返回最新写入的值。java

02

特性

假如一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰以后,具有如下特性:
一、保证多线程下的可见性   
二、对于单个的共享变量的读/写具备原子性,没法保证相似num++的原子性。
三、禁止进行指令重排序(即保证有序性)。即volatile前面的代码先于后面的代码先执行面试

在Java内存模型中,容许编译器和处理器对指令进行重排序,可是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。在Java里面,能够经过volatile关键字来保证必定的“有序性”。
Java内存模型具有一些先天的“有序性”,即不须要经过任何手段就可以获得保证的有序性,也就是happens-before 原则。redis

happens-before 原则
程序次序规则:一个线程内,按照代码顺序,书写在前面的操做先行发生于书写在后面的操做
锁定规则:一个unLock操做先行发生于后面对同一个锁额lock操做
volatile变量规则:对一个变量的写操做先行发生于后面对这个变量的读操做
传递规则:若是操做A先行发生于操做B,而操做B又先行发生于操做C,则能够得出操做A先行发生于操做C
线程启动规则:Thread对象的start()方法先行发生于此线程的每一个一个动做
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中全部的操做都先行发生于线程的终止检测,咱们能够经过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始算法

Java内存模型 (Java Memory Model,JMM)
Java内存模型由Java虚拟机规范定义,用来屏蔽各个平台的硬件差别。简单来讲:
1 全部变量储存在主内存。2 每条线程拥有本身的工做内存,其中保存了主内存中线程使用到的变量的副本。3 线程不能直接读写主内存中的变量,全部操做均在工做内存中完成。设计模式

线程,主内存,工做内存的交互关系如图:
volatile 关键字详解缓存

和volatile有关的操做为微信

read(读取):做用于主内存变量,把一个变量值从主内存传输到线程的工做内存中,以便随后的load动做使用markdown

load(载入):做用于工做内存的变量,它把read操做从主内存中获得的变量值放入工做内存的变量副本中。
use(使用):做用于工做内存的变量,把工做内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个须要使用变量的值的字节码指令时将会执行这个操做。
assign(赋值):做用于工做内存的变量,它把一个从执行引擎接收到的值赋值给工做内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操做。
store(存储):做用于工做内存的变量,把工做内存中的一个变量的值传送到主内存中,以便随后的write的操做。
write(写入):做用于主内存的变量,它把store操做从工做内存中一个变量的值传送到主内存的变量中。

03

为何要使用Volatile

Volatile变量修饰符若是使用恰当的话,它比synchronized的使用和执行成本会更低,由于它不会引发线程上下文的切换和调度。多线程

03

volatile的原子性问题

volatile仅仅保障对其修饰的变量的写操做( 以及读操做 )自己的原子性 ,而这并不表示对 volatile 变量的赋值操做必定具备原子性。例如,以下对volatile 变量 count1的赋值操做并非原子操做:
count1 = count2 + 1;
若是变量count2也是一个共享变量,那么该赋值操做其实是一个read-modify-write 操做。其执行过程当中其余线程可能已经更新了 count2 的值,所以该操做不具有不可分割性,也就不是原子操做。若是变量count2 是一个局部变量,那么该赋值操做就是一个原子操做。
对volatile变量的赋值操做,其右边表达式中只要涉及共享变量 ( 包括被赋值的 volatile 变量自己 ),那么这个赋值操做就不是原子操做。要保障这样操做的原子性, 仍然须要借助锁。并发

04

解决num++操做的原子性问题

针对num++这类复合类的操做,可使用java并发包中的原子操做类原子操做类是经过循环CAS的方式来保证其原子性的。

public class Counter {  //使用原子操做类
public static AtomicInteger num = new AtomicInteger(0);
//使用CountDownLatch来等待计算线程执行完
static CountDownLatch countDownLatch = new CountDownLatch(30);
public static void main(String []args) throws InterruptedException {
   //开启30个线程进行累加操做
   for(int i=0;i<30;i++){
       new Thread(){
           public void run(){
               for(int j=0;j<10000;j++){
                   num.incrementAndGet();//原子性的num++,经过循环CAS方式
               }
               countDownLatch.countDown();
           }
       }.start();
   }
   //等待计算线程执行完
   countDownLatch.await();
   System.out.println(num);
}
}

05

实现原理

可见性实现原理
将一个共享变量声明为volatile后,会有如下效应
1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的变量强制刷新到主内存中去;
2.这个写会操做会致使其余线程中的缓存无效。
volatile可以保证可见性,那么它是如何实现可见性的呢?以X86处理器为例,在对volatile修饰的变量进行写操做时,经过编译器生成反汇编指令后,会发现会多一条Lock前缀,就是因为这条Lock前缀所实现的可见性。Lock前缀在多核处理器中会引起下面这两件事情:
1Lock指令会将当前处理器缓存行的数据写回到主内存。(ps:每一个处理器都有本身的cache缓存,每次缓存中操做的变量都是主内存中变量的拷贝) 2 一个处理器写回主内存的操做会形成其余处理的缓存无效。

禁止指令重排原理
经过内存屏障来实现禁止指令重排。
如图
volatile 关键字详解

06

volatile的使用优化

在JDK7的并发包里新增了一个队列集合类LinkedTransferQueue,它在使用volatile变量的时候,会采用一种将字节追加到64字节的方法来提升性能。
追加到64字节可以优化性能缘由
在不少处理器中它们的L一、L二、L3缓存的高速缓存行都是64字节宽,不支持填充缓存行,例如,如今有两个不足64字节的变量AB,那么在AB变量写入缓存行时会将AB变量的部分数据一块儿写入一个缓存行中,那么在CPU1和CPU2想同时访问AB变量时是没法实现的,也就是想同时访问一个缓存行的时候会引发冲突,若是能够填充到64字节,AB两个变量会分别写入到两个缓存行中,这样就能够并发,同时进行变量访问,从而提升效率。

07

总结

volatile是一种轻量级的同步机制,它主要有三个特性:
一是保证共享变量对全部线程的可见性
二是禁止指令重排序优化
三是volatile对于单个的共享变量的读/写具备原子性,没法保证相似num++的原子性,须要经过循环CAS的方式来保证num++操做的原子性。

推荐阅读:

  • 深刻解析HashMap和ConcurrentHashMap源码以及底层原理

  • 设计模式(二):几种工厂模式详解

  • 进程同步的五种机制以及优缺点(翻译)

  • redis五种数据类型的实现方式,经常使用命令,应用场景

  • redis和memcahed的共同点,区别以及应用场景

  • 详解TCP的三次握手与四次挥手及面试题(很全面)

  • Arrays 工具类详解(超详细)

  • 算法必须掌握几种方法

  • QPS、TPS、并发用户数、吞吐量

  • 设计模式之单例模式

  • Collections 工具类详解(超详细)

END
volatile 关键字详解
扫描二维码 | 关注咱们
微信公众号 : jiagoudiantang
CSDN : https://fking.blog.csdn.net

相关文章
相关标签/搜索