热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Java之深入浅出解读volatile

Java之深入浅出解读volatile目录 一、基本概念回顾二、CPU与内存

Java 之深入浅出解读 volatile

目录

 

一、基本概念回顾

二、CPU与内存

三、Lock前缀指令

四、Volatile保证可见性的实现原理

五、Volatile禁止指令重排序


一、基本概念回顾



在进入正文之前,首先回顾一下Java 内存模型中的一些基本概念:可见性、原子性和有序性。

可见性:

  可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉。通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制。

  可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果。另一个线程马上就能看到。比如:用volatile修饰的变量,就会具有可见性。volatile修饰的变量不允许线程内部缓存和重排序,即直接修改内存。所以对其他线程是可见的。但是这里需要注意一个问题,volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。比如 volatile int a = 0;之后有一个操作 a++;这个变量a具有可见性,但是a++ 依然是一个非原子操作,也就是这个操作同样存在线程安全问题。

  在 Java 中 volatile、synchronized 和 final 实现可见性。

原子性:

  原子是世界上的最小单位,具有不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

  在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。

有序性:

  Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行执行。

 


二、CPU与内存

如下图所示,一个简化的CPU与内存的关系图。CPU在对主内存中的共享变量进行操作的时候,并不是直接操作主内存,那样速度太慢了,而是将主内存中的变量 “拷贝” 到缓存中(严格的说,计算机的缓存分为1、2、3级缓存,这里简化为缓存),在高速缓存中执行操作完毕后,再回写到主内存中。

不难想见,多CPU、多线程的环境下,如果共享变量没有采用锁机制,那么,多个线程并发操作主内存的共享变量(如 int value=0),由于变量在线程间是不可见的,可能出现以下情形(举例):

  1. A、B线程分别将变量value从主内存读到各自的缓存中,此时,对于两个线程来说,value=0,缓存与主内存一致;

  2. 线程A对变量value进行写操作,如value=10,完成后回写到主内存,线程B中value仍然为0,主内存与缓存不一致;

  3. 线程B完成对value的写操作,回写主内存,覆盖掉线程A写入的值;

Java 之深入浅出解读 volatile - 文章图片


三、Lock前缀指令

关于LOCK指令,Intel手册的解释如下:

Causes the processor’s LOCK# signal to be asserted during execution of the accompanying instruction (turns the instruction into an atomic instruction). In a multiprocessor environment, the LOCK# signal insures that the processor has exclusive use of any shared memory while the signal is asserted.

其意为:LOCK指令会使紧跟在其后面的指令变成 原子操作(atomic instruction)。暂时的锁一下总线,指令执行完了,总线就解锁了!!!

  • LOCK前缀指令

LOCK 指令是一个汇编层面的指令,在一些特殊的场景下,作为前缀加在以下汇编指令之前,保证操作的原子性,这种指令被称为 “LOCK前缀指令”。

ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG,DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG.

LOCK前缀导致处理器在执行指令时会置上LOCK#信号,于是该指令就被作为一个原子指令(atomic instruction)执行。在多处理器环境下,置上LOCK#信号可以确保任何一个处理器能独占使用任何共享内存。

  • 锁总线

LOCK总线封锁信号,三态输出,低电平有效。LOCK有效时表示CPU不允许其它总线主控者占用总线(CPU与内存等硬件之前的通信需要经过总线)。这个信号由软件设置,当前指令前加上LOCK前缀时,则在执行这条指令期间LOCK保持有效,阻止其它主控者使用总线。说白了就是LOCK前缀只保证对当前指令要访问的内存互斥。

换言之,在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存(通过锁住总线,避免其它处理器访问共享内存),不过成本较高。

  • 锁缓存

在Pentium4、Inter Xeon和P6系列以及之后的处理器中,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销的比较大。

在所有的 X86 CPU 上都具有锁定一个特定内存地址的能力,当这个特定内存地址被锁定后,它就可以阻止其它的系统总线读取或修改这个内存地址。这种能力是通过 LOCK 指令前缀再加上具体操作(如ADD)的汇编指令来实现的。当使用 LOCK 指令前缀时,它会使 CPU 宣告一个 LOCK# 信号,这样就能确保在多处理器系统或多线程竞争的环境下互斥地使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。

处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如CPU A嗅探到CPU B打算写内存地址,且这个地址处于共享状态,那么正在嗅探的处理器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。


四、Volatile保证可见性的实现原理

前已述及,Java语言提供了一种弱同步机制,即 volatile变量,用来确保将变量的更新操作在不同的线程间是可见的。如何做到呢?

  • volatile如何保证可见性?

多处理器、多线程环境下,若某个线程对声明了volatile的变量进行写操作,JVM会向处理器发送一条LOCK前缀的指令,将这个变量所在缓存行的数据写回主内存,LOCK前缀指令通过 “锁缓存” 可以确保回写主内存的操作是原子性的。但是,其它处理器的缓存中存储的仍然是 “旧值” ,并不能保证可见性,因此,还要借助缓存一致性协议:每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是否过期,当处理器发现自己缓存行对应的内存地址被修改时,就会设置当前缓存行为无效,需要对数据进行修改的时候会重新从主内存中加载。如此,便保证了可见性。

  • volatile不能保证原子性!

为什么不能保证原子性呢?对于volatile修饰的变量,LOCK前缀指令保证的是其写操作和回写主内存的操作是原子性的。什么是写操作?如:value=10,对变量value进行写,这是实实在在的写,是一个原子操作。

但是,“value++;”是单纯的写操作吗?不是!value++的时候会先将value赋值给另外一个临时变量(设为tmp),tmp属于工作内存的局部变量表,再将tmp返回到缓存,缓存再返回到主存,这里需要一些寄存器运算的知识。形象一些,可以把value++拆分成以下伪代码:

int tmp = value; //1
tmp = tmp + 1; //2
value = tmp; //3

很明显,对于变量value而言,最后一步:value=temp才是真正的写操作,LOCK前缀指令可以保证写操作和回写主内存的操作是原子性的。而前面两步并没有对value进行任何写操作,JVM不会做出反应,这就是为什么volatile不能保证原子性的根本原因。

有Java多线程编程经验的读者应该清楚,以下代码执行的结果是不稳定的,并且结果都是<=10000的,其根因就是value++并非原子性,volatile无能为力。

public class App
{
private static volatile int value = 0;
public static void main(String[] args)
{
for (int i = 0; i <10; i++)
{
new MyThread().start();
}
System.out.println("value = " + value);
}
static class MyThread extends Thread
{
@Override
public void run()
{
for (int i = 0; i <1000; i++)
{value++;
}
}
}
}


五、Volatile禁止指令重排序

volatile关键字禁止指令重排序有两层意思(不完全禁止):

  • 程序执行到volatile变量的读或写时,在其前面的操作肯定全部已经执行完毕,且结果已经对后面的操作可见;在其后面的操作肯定还没有执行

  • 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

lock前缀指令相当于一个内存屏障(也称内存栅栏),内存屏障主要提供3个功能:

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

  2. 强制将对缓存的修改操作立即写入主存,利用缓存一致性机制,并且缓存一致性机制会阻止同时修改由两个以上CPU缓存的内存区域数据;

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

参考文章:

1.https://www.jianshu.com/p/9abb4a23ab05


推荐阅读
  • 本文详细介绍了在PHP中如何获取和处理HTTP头部信息,包括通过cURL获取请求头信息、使用header函数发送响应头以及获取客户端HTTP头部的方法。同时,还探讨了PHP中$_SERVER变量的使用,以获取客户端和服务器的相关信息。 ... [详细]
  • 汇总了2023年7月7日最新的网络安全新闻和技术更新,包括最新的漏洞披露、工具发布及安全事件。 ... [详细]
  • 2023年1月28日网络安全热点
    涵盖最新的网络安全动态,包括OpenSSH和WordPress的安全更新、VirtualBox提权漏洞、以及谷歌推出的新证书验证机制等内容。 ... [详细]
  • 函子(Functor)是函数式编程中的一个重要概念,它不仅是一个特殊的容器,还提供了一种优雅的方式来处理值和函数。本文将详细介绍函子的基本概念及其在函数式编程中的应用,包括如何通过函子控制副作用、处理异常以及进行异步操作。 ... [详细]
  • Docker安全策略与管理
    本文探讨了Docker的安全挑战、核心安全特性及其管理策略,旨在帮助读者深入理解Docker安全机制,并提供实用的安全管理建议。 ... [详细]
  • Hadoop MapReduce 实战案例:手机流量使用统计分析
    本文通过一个具体的Hadoop MapReduce案例,详细介绍了如何利用MapReduce框架来统计和分析手机用户的流量使用情况,包括上行和下行流量的计算以及总流量的汇总。 ... [详细]
  • 如何高效学习鸿蒙操作系统:开发者指南
    本文探讨了开发者如何更有效地学习鸿蒙操作系统,提供了来自行业专家的建议,包括系统化学习方法、职业规划建议以及具体的开发技巧。 ... [详细]
  • Java虚拟机及其发展历程
    Java虚拟机(JVM)是每个Java开发者日常工作中不可或缺的一部分,但其背后的运作机制却往往显得神秘莫测。本文将探讨Java及其虚拟机的发展历程,帮助读者深入了解这一关键技术。 ... [详细]
  • 本文详细介绍了如何在 Ubuntu 14.04 系统上搭建仅使用 CPU 的 Caffe 深度学习框架,包括环境准备、依赖安装及编译过程。 ... [详细]
  • 本文详细介绍了在Luat OS中如何实现C与Lua的混合编程,包括在C环境中运行Lua脚本、封装可被Lua调用的C语言库,以及C与Lua之间的数据交互方法。 ... [详细]
  • 吴石访谈:腾讯安全科恩实验室如何引领物联网安全研究
    腾讯安全科恩实验室曾两次成功破解特斯拉自动驾驶系统,并远程控制汽车,展示了其在汽车安全领域的强大实力。近日,该实验室负责人吴石接受了InfoQ的专访,详细介绍了团队未来的重点方向——物联网安全。 ... [详细]
  • 在测试软件或进行系统维护时,有时会遇到电脑蓝屏的情况,即便使用了沙盒环境也无法完全避免。本文将详细介绍常见的蓝屏错误代码及其解决方案,帮助用户快速定位并解决问题。 ... [详细]
  • 七大策略降低云上MySQL成本
    在全球经济放缓和通胀压力下,降低云环境中MySQL数据库的运行成本成为企业关注的重点。本文提供了一系列实用技巧,旨在帮助企业有效控制成本,同时保持高效运作。 ... [详细]
  • 本文详细介绍了JQuery Mobile框架中特有的事件和方法,帮助开发者更好地理解和应用这些特性,提升移动Web开发的效率。 ... [详细]
  • CRZ.im:一款极简的网址缩短服务及其安装指南
    本文介绍了一款名为CRZ.im的极简网址缩短服务,该服务采用PHP和SQLite开发,体积小巧,约10KB。本文还提供了详细的安装步骤,包括环境配置、域名解析及Nginx伪静态设置。 ... [详细]
author-avatar
怪话greenup
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有