volatile的特性

volatile是Java中用于修饰变量的关键字,其主要是保证了该变量的可见性以及顺序性,但是没有保证原子性;其是Java中最为轻量级的同步关键字;

接下来我将会一步步来分析volatile关键字是如何在Java代码层面、字节码层面、JVM源码层次、汇编层面、操作系统层面、CPU层面来保证可见性和顺序性的;

Java代码层面

当一个变量被定义为volatile之后,具备两项特性:

  1. 保证此变量对所有线程的可见性
  2. 禁止指令重排序优化

volatile所保证的可见性

volatile所修饰的变量在一条线程修改一个变量的值的时候,新值对于其他线程来说是可以立即知道的;

普通变量的值在线程间传递的时候都是通过主内存去完成;

根据JMM我们可以知道,每一个线程其实都有它单独的栈空间,而实际的对象其实都是存放在主内存中的,所以如果是普通对象的话,便会有一个栈空间的对象主内存中的对象存在差异的时间;而volatile所修饰的变量其保持了可见性,其会强制让栈空间所存在的对应变量失效,然后从主内存强制刷新到栈空间,如此便每次看到的都是最新的数据;

volatile所保证的禁止指令重排

Java的每一行语句其实都对应了一行或者多行字节码语句,而每一行字节码语句又对应了一行或者多行汇编语句,而每一行汇编语句又对应了一行或者多行机器指令;但是CPU的指令优化器可能会对其指令顺序进行重排,优化其运行效率,但是这样也可能会导致并发问题;而volatile便可以强制禁止优化指令重排;

volatile在字节码层面的运用

我们先看到以下代码

点击查看代码
  1. public class Main {
  2. static int a ;
  3. static volatile int b ;
  4. public static synchronized void change(int num) {
  5. num = 0;
  6. }
  7. public static void main(String[] args) {
  8. a = 10;
  9. b = 20;
  10. change(a);
  11. }
  12. }

我们先试用javac来将java文件编译为class文件,然后通过javap -v来反编译;

点击查看代码
  1. Classfile /opt/software/java-study/Main.class
  2. Last modified Mar 1, 2022; size 400 bytes
  3. MD5 checksum c7691713c9365588495a60da768c32a6
  4. Compiled from "Main.java"
  5. public class Main
  6. SourceFile: "Main.java"
  7. minor version: 0
  8. major version: 51
  9. flags: ACC_PUBLIC, ACC_SUPER
  10. Constant pool:
  11. #1 = Methodref #6.#20 // java/lang/Object."<init>":()V
  12. #2 = Fieldref #5.#21 // Main.a:I
  13. #3 = Fieldref #5.#22 // Main.b:I
  14. #4 = Methodref #5.#23 // Main.change:(I)V
  15. #5 = Class #24 // Main
  16. #6 = Class #25 // java/lang/Object
  17. #7 = Utf8 a
  18. #8 = Utf8 I
  19. #9 = Utf8 b
  20. #10 = Utf8 <init>
  21. #11 = Utf8 ()V
  22. #12 = Utf8 Code
  23. #13 = Utf8 LineNumberTable
  24. #14 = Utf8 change
  25. #15 = Utf8 (I)V
  26. #16 = Utf8 main
  27. #17 = Utf8 ([Ljava/lang/String;)V
  28. #18 = Utf8 SourceFile
  29. #19 = Utf8 Main.java
  30. #20 = NameAndType #10:#11 // "<init>":()V
  31. #21 = NameAndType #7:#8 // a:I
  32. #22 = NameAndType #9:#8 // b:I
  33. #23 = NameAndType #14:#15 // change:(I)V
  34. #24 = Utf8 Main
  35. #25 = Utf8 java/lang/Object
  36. {
  37. static int a;
  38. flags: ACC_STATIC
  39. static volatile int b;
  40. flags: ACC_STATIC, ACC_VOLATILE
  41. public Main();
  42. flags: ACC_PUBLIC
  43. Code:
  44. stack=1, locals=1, args_size=1
  45. 0: aload_0
  46. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  47. 4: return
  48. LineNumberTable:
  49. line 1: 0
  50. public static synchronized void change(int);
  51. flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
  52. Code:
  53. stack=1, locals=1, args_size=1
  54. 0: iconst_0
  55. 1: istore_0
  56. 2: return
  57. LineNumberTable:
  58. line 5: 0
  59. line 6: 2
  60. public static void main(java.lang.String[]);
  61. flags: ACC_PUBLIC, ACC_STATIC
  62. Code:
  63. stack=1, locals=1, args_size=1
  64. 0: bipush 10
  65. 2: putstatic #2 // Field a:I
  66. 5: bipush 20
  67. 7: putstatic #3 // Field b:I
  68. 10: getstatic #2 // Field a:I
  69. 13: invokestatic #4 // Method change:(I)V
  70. 16: return
  71. LineNumberTable:
  72. line 9: 0
  73. line 10: 5
  74. line 11: 10
  75. line 12: 16
  76. }

我们仔细观察加了volatile修饰的变量与其他变量的区别便可以看出,其主要是在flags中添加了一个**ACC_VOLATILE**;同时先进行**putstatic**指令;

volatile在JVM源码方面的运用

在JVM源码方面,我编译了OpenJDK7然后利用find与grep进行全局查找,然后进行方法追踪,由于涉及到大量C++的知识,我便跳过其C++代码追踪,而直接看最后追踪到的函数;

先来做一个总结,其实volatile的JVM源码的原理对应的是被称为内存屏障来实现的;

点击查看代码
  1. static void loadload();
  2. static void storestore();
  3. static void loadstore();
  4. static void storeload();

这四个分别对应了经常在书中看到的JSR规范中的读写屏障

  • LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
  • LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
  • StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
  • StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

对于volatile操作而言,其操作步骤如下:

  • 每个volatile写入之前,插入一个StoreStore,写入以后插入一个StoreLoad
  • 每个volatile读取之前,插入一个LoadLoad,读取之后插入一个LoadStore

在JVM源码层次而言,内存屏障直接起到了禁止指令重排的作用,且之后与总线锁或者MESI协议配合实现了可见性;

汇编层次

在汇编层次而言,我是使用JITWatch配合hsdis进行的转汇编,可以发现在含有volatile的变量的时候,汇编指令会有一个lock前缀,而lock前缀在CPU层次中自己实现了内存屏障的功能;

CPU层次

在x86的架构中,含有lock前缀的指令拥有两种方法实现;

一种是开销很大的总线锁,它会把对应的总线直接全部锁住,如此明显是不合理的;

所以后期intel引入了缓存锁以及mesi协议,如此便可以轻量化的实现内存屏障;

Java并发杂谈(一):volatile的底层原理,从字节码到CPU的更多相关文章

  1. 【死磕Java并发】-----深入分析volatile的实现原理

      通过前面一章我们了解了synchronized是一个重量级的锁,虽然JVM对它做了很多优化,而下面介绍的volatile则是轻量级的synchronized.如果一个变量使用volatile,则它 ...

  2. 【死磕Java并发】—–深入分析volatile的实现原理

    通过前面一章我们了解了synchronized是一个重量级的锁,虽然JVM对它做了很多优化,而下面介绍的volatile则是轻量级的synchronized.如果一个变量使用volatile,则它比使 ...

  3. Java 并发编程:volatile的使用及其原理

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  4. Java并发编程:Synchronized及其实现原理

    Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...

  5. Java并发编程:volatile关键字解析

    Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...

  6. (转)Java并发编程:volatile关键字解析

    转:http://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或 ...

  7. Java并发编程:volatile关键字解析(转载)

    转自https://www.cnblogs.com/dolphin0520/p/3920373.html Java并发编程:volatile关键字解析   Java并发编程:volatile关键字解析 ...

  8. Java并发编程:volatile关键字解析-转

    Java并发编程:volatile关键字解析 转自海子:https://www.cnblogs.com/dayanjing/p/9954562.html volatile这个关键字可能很多朋友都听说过 ...

  9. 6、Java并发编程:volatile关键字解析

    Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在 ...

  10. 转:Java并发编程:volatile关键字解析

    Java并发编程:volatile关键字解析 Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字, ...

随机推荐

  1. netty系列之:好马配好鞍,为channel选择配套的selector

    目录 简介 netty服务的基本构建方式 EventLoopGroup channel 多种构建方式 其他的channel 总结 简介 我们知道netty的基础是channel和在channel之上的 ...

  2. 【记录一个问题】opencl enqueueWriteBuffer()中,cl_bool blocking参数设置无效

    err = queue.enqueueWriteBuffer(in_buf, true, 0, bmp_size, bmp_data, NULL, &event); 以上代码中,第二个参数设置 ...

  3. unity3d之sokect通信

    using System.Collections; using System.Collections.Generic; using UnityEngine; using System; using S ...

  4. 一段关于java NIO server端接受客户端socket连接;演示了关于channel,selector等组件的整合使用

    public class ReactorDemo { public static void main(String[] args) throws IOException { ServerSocketC ...

  5. for each ……in

    使用一个变量迭代一个对象的所有属性值,对于每一个属性值,有一个指定的语句块被执行. 作为ECMA-357(E4X)标准的一部分,for each...in语句已被废弃,E4X中的大部分特性已被删除,但 ...

  6. java抽象类案例

    1 package face_09; 2 /* 3 * 雇员示例: 4 * 需求:公司中程序员有姓名,工号,薪水,工作内容. 5 * 项目经理除了有姓名,工号,薪水,还有奖金,工作内容. 6 * 对给 ...

  7. chapter2 线性回归实现

    1 导入包 import numpy as np 2 初始化模型参数 ### 初始化模型参数 def initialize_params(dims): w = np.zeros((dims, 1)) ...

  8. Maven的安装、配置与使用

    5.Maven 我为什么要学习这个技术? 在JavaWeb开发中,需要使用大量的jar包,我们手动去导入: 如何能够让一个东西自动帮我们导入和配置这个jar包. 由此,Maven诞生! 5.1.Mav ...

  9. pageX的兼容性处理1

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  10. smartimageview 的原理

    自定义的控件在布局文件中的引用都需要指定类的完整路径 1.自定义了一个MyImageview类继承了Imageview,添加三个构造方法     2.添加一个setImageUrl方法接受一个图片ur ...