并发编程——Volatile关键字

Volatile关键字

Volatile关键字

概述

volatile关键字只能修饰变量和实例变量

硬件CPU

在计算机中所有的运算操作都是由CPU的寄存器来完成的,CPU指令的执行过程需要涉及数据的读取和写入操作,CPU所能访问的所有数据只能是计算机主存(通常是指RAM),但是CPU的发展频率不断的提升,但是内存没有太多的进展,因此CPU的处理速度和内存访问速度之间的差距是比较大的。

CPU Cache模型

由于两边的速度严重的不对等,通过传统的FSB直连内存的访问方式明显的会导致CPU资源受到大量的限制,降低CPU整体的吞吐量,于是就有了多级缓存的设计。

现在缓存的数量增加到3级,最靠近CPU缓存成为L1,然后依次是L2,L3和主内存。

由于程序执行和程序数据的行为和热点分布差异很大,因此L1 Cache又被划分为L1i(i是instruction的缩写)和L1d(d是data的缩写)这两种各自专门用途的缓存.

CPU Cache又是由很多个Cache Line构成的,Cache Line可以认为是CPU Cache中最小缓存单位,目前主流CPU Cache的Cache Line大小都是64字节。

Cache的出现是为了解决CPU直接访问内存效率低下问题的,程序运行过程中,会将运算所需要的数据从主存中复制一份到CPU Cache中,这样CPU进行计算时可以直接对CPU Cache中的数据进行读取和写入,当运行结果结束后,将CPU Cache中的最新数据刷新到主内存中,CPU直接访问Cache的方式替代直接访问主存的方式极大的提高了CPU的吞吐能力。

但是同时也带来了缓存一致性的问题

CPU缓存一致性问题

由于缓存的出现,极大地提高了CPU的吞吐能力,但是同时也引入了缓存不一致的问题,

为了解决缓存不一致的问题,通常主流的解决办法有:

  1. 通过总线加锁的方式
    通过总线加锁的方式,是早期CPU当中比较常见的方法,而且是一种悲观的实现方式,CPU和其他组件的通信都是通过总线(数据总线、控制总线、地址总线)来进行的,如果采用总线加锁的方式,则会阻塞其他CPU对其他组件的访问,从而使得只有一个CPU(抢到总线锁)能够访问这个变量的内存。这种方式可以说非常的低效率。
  2. 通过缓存一致性协议
    在缓存一致性协议中最为出名的是Intel的MESI协议,MESI协议保证了每一个缓存中使用的共享变量副本都是一致的,当CPU在操作Cache中的数据时,如果发现该变量是一个共享变量,也就是在其他的CPU Cache中也存在一个副本,那么,
    1. 读取操作,不做任何处理,只是将Cache中的数据读取到寄存器。
    2. 写入操作,发出信号通知其他CPU将该变量的Cache line置为无效转台,其他CPU在进行该变量的读取的时候不得不到主内存中再次获取。

java内存模型

java内存模型(Java Memory Mode,JMM)指定了Java虚拟机如何与计算机主存(RAM)进行工作,理解java内存模型对于编写行为正确的并发程序是非常重要的。

java内存模型决定了一个线程对共享变量的写入何时对其他线程可见,Java内存模型定义了线程和主内存之间的抽象关系:

  1. 共享变量存储于主内存之中,每个线程都可以访问。
  2. 每个线程都有私有的工作内存或者成为本地内存。
  3. 工作内存只存储该线程对共享变量的副本。
  4. 线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存。
  5. 工作内存和Java内存模型一样也是一个抽象的概念,他其实并不存在,它涵盖了缓存、寄存器、编译器优化以及硬件等。

深入理解Volatile关键字

并发编程的三个至关重要的特性

1.原子性

所谓原子性指的是一次的操作或者多次操作中,要么所有的操作全部都得到执行并且不会受到其他任何因素的干扰而中断,要么所有的操作都不执行。

注意:
两个原子性的操作结合在一起未必还是原子性的。
比如i++,i++运算中一共分为三个步骤:

  1. get i
  2. i+1
  3. set i
    三个操作均是原子性操作,但是i++并不是原子操作
    当然,volatile关键字是不能够保证原子性操作的,synchronized是能够保证的,JDK1.5之后提供了原型类型变量也可以保证原子性。
2.可见性

可见性指的是当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值。

3.有序性

有序性指的是程序代码在执行过程中的先后顺序,由于Java编译器以及运行期的优化,导致了代码的执行顺序未必就是开发者编写代码的顺序。

这里就涉及到了指令重排序(Instruction Recorder):
代码:

int a = 1;
int b = 2;
a++;
b = 5;   

在JVM编译之后,它的顺序有可能会发生改变,变成

int a = 1;
int b = 2;
b = 5;  
a++;

一般来说,处理器为了提高程序的运行效率,可能对输入的代码指令做一定的优化,它不会百分之百的保证代码的执行顺序严格按照编写的代码顺序来进行,但是它会保证程序的最终结果是编码所期望的。

当然这个是在单线程的前提下,如果多线程执行的时候,可能就会造成错误的发生。

JVM如何保证三大特性

JVM与原子性

在java语言中,对基本数据类型的变量读取赋值操作都是原子性的,对引用类型的变量读取和赋值的操作也是原子性。

举例:

  1. x=10;赋值操作
    这个毫无疑问是原子性的
  2. y = x; 赋值操作
    这个显然不是原子操作,这个赋值操作包括了几个步骤,
    首先是读取x,其次将x的值赋值给y,这明显是两个步骤,而不是一步操作,虽然每一步都是原子性的,但是整体并不是;
  3. i++;自增操作
    这个刚上面也解释过了,i++包括三个步骤,每一步都是原子性,但是合道一起并不是原子操作;
  4. a = a + 1;加1操作;
    这个操作和i++操作时类似的,都是三个步骤,所以也不是原子操作的;

结论:

  1. 多个原子性操作在一起不一定是原子操作
  2. 简单的读取和赋值操作都是原子性的,将一个变量赋值给另一个变量的操作不是原子性的操作
  3. Java内存模型(JMM)只保证基本读取和赋值的原子性操作,其他的均不保证,如果想要使得某个代码片段具有原子性,需要使用synchronized关键字,或者JUC中的lock,如果想要使用基本类型的自增具有原子性,可以使用JUC包下的atomic,java.util.concurrent.atomic.*

volatile 关键字不具备保证原子性的语义;

JVM与可见性

java中提供了三种方式保证可见性:

  1. 使用关键字volatile
  2. 通过synchronized关键字
  3. 通过JUC提供的显式锁Lock也能抱枕数据的可见性
JVM与有序性

java中提供了三种方式保证有序性:

  1. 使用关键字volatile
  2. 通过synchronized关键字
  3. 通过JUC提供的显式锁Lock也能抱枕数据的有序性

后两种属于同步机制,同步代码与单线程情况下一样所以能够保证有序性。

java的内存模型具备一些天生的有序性规则,不需要任何同步手段就能够保证有序性,这个规则被称为Happens-before原则,

Happens-before原则:

  1. 程序次序规则
    在一个线程内,代码按照编写的次序执行,编写在后面的操作发生于编写在前面的操作之后。
  2. 锁定规则
    一个unlock操作要线性发生于对同一个锁的lock操作
  3. volatile变量规则
    对一个变量的写操作要早于对这个变量之后的读操作
  4. 传递规则
    A早于B,B早于C,则A早于C
  5. 线程启动规则
    Tread对象的start()方法先行发生于该线程的任何动作
  6. 线程中断规则
    对线程执行interrupt()方法可定要优先于捕获到中断信号
  7. 线程的终结规则
    线程中的所有操作都要先行发生于线程的终止检测
  8. 对象的终结规则
    一个对象初始化的完成先行发生于finalize()方法之前。

volatile的原理和实现机制

volatile关键字的源码:包含"lock;"的前缀;

"lock;"前缀实际上相当于一个内存屏障,该内存屏障会为指令的执行提供几个保障:

  1. 确保指令重排序时不会将其后面的代码排到内存屏障之前;
  2. 确保指令重排序时不会将其前面的代码排到内存屏障之后;
  3. 确保在执行到内存屏障修饰的指令时前面的代码全部执行完成;
  4. 强制将线程工作内存中值的修改刷新至主内存中;
  5. 如果是写操作,则会导致其他线程工作内存(CPU Cache)中的缓存数据失效;

volatile应用场景

  1. 开关控制利用可见性的特点
  2. 状态标记利用有序性特点
  3. Singleton(单例模式)设计模式的double-check也是利用有序性的特点

volatile和synchronized

区别:

  1. 使用区别
    1. volatile关键字修饰实例变量或者类变量,不能用于修饰方法以及方法参数和局部变量、常量等
    2. synchronized关键字不能用于对变量的修饰,只能用于修饰方法或者语句块
    3. volatile修饰的变量可以为null, synchronized关键字同步语句块的monitor对象不能为空
  2. 对原子性的保证
    1. volatile不能保证原子性
    2. 由于synchronized关键字的排他性,其修饰的同步代码无法被中途打断,因此可以保证代码的原子性
  3. 对可见性的保证
    1. 均能够保证共享资源在多线程之间的可见性,但是实现机制完全不同。
    2. synchronized是利用同步的机制,将代码进行串行化,所有资源都会被刷新到主内存中
    3. volatile关键字则是使用机器指令,偏硬件的方式迫使其他线程工作内存中的数据失效,不得到主内存中进行再次加载。
  4. 对有序性的保证
    1. volatile关键字禁止JVM编译器以及处理器对其进行重排序,它能够保证有序性。
    2. synchronized关键字也能够保证有序性,但是这种有序性是以程序的串行化执行换来的,当然,在synchronized关键字修饰的代码块中还是有可能会出现执行重排序的情况。
  5. 线程阻塞
    1. volatile关键字不会使线程陷入阻塞
    2. synchronized关键字会是线程陷入阻塞

学习自:
《Java高并发编程详解——多线程与架构设计》 汪文君

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值