volatile的使用与原子性问题

  • 2020 年 3 月 10 日
  • 筆記

1. volatile的使用

java源代码

public class VolatileUsedClass {        private static int sharedVar = 10;        public static void main(String[] args) throws Exception {            Thread thread1 = new Thread(new Runnable() {              @Override              public void run() {                  try {                      // modify the sharedVar,write first                      TimeUnit.MICROSECONDS.sleep(500L);                      sharedVar = 20;                      System.out.printf("%s modify the shared var to %s  ...n", "thread-1", sharedVar);                  } catch (Exception e) {                      System.out.println(e);                  }              }          });              Thread thread2 = new Thread(new Runnable() {              @Override              public void run() {                  try {                      // read the value,read for the last                      TimeUnit.MICROSECONDS.sleep(505L);                      System.out.printf("%s read the shared var %s n", "thread-2", sharedVar);                  } catch (Exception e) {                      System.out.println(e);                  }              }          });            thread2.start();          thread1.start();            thread1.join();          thread2.join();            System.out.println("finish the thread task...");      }  }

客户端模式-client

  • 不加volatile的执行结果(多次执行)
  • 加volatile的执行结果(多次)
  • 结果分析
    • 在源代码中是添加时间休眠主要是保证先写后读的逻辑
    • 从运行结果可以看出,虽然时间片很短,读线程的数据仍然是本地缓存的数据,并没有从主内存中读取值
    • 添加volatile关键字之后,可以看到读线程的数据正是写线程之后的数据,也就是写读数据是一致的

服务端模式,-server

  • 无volatile修饰的执行结果(多次)
  • 有volatile修饰的执行结果(多次)
  • 结果分析
    • 不带volatile修饰与客户端执行效果一致
    • 但是使用server模式带有volatile的方式却出现了数据不一致的情况,为什么?
    • 原因在于-server模式会在编译成字节码的时候进行代码重排序导致的,主要用于优化程序提升执行效率

小结

  • clinet模式是jdk执行的默认配置,可用于测试环境或者本地开发
  • server模式一般用于生产环境,目的是开启server模式的时候编译器会针对OS系统情况做一些优化操作
  • 服务端与客户端运行模式参考:
参考JVM Server vs Client Mode:  https://javapapers.com/core-java/jvm-server-vs-client-mode/
2. 原子性问题

说明: 以下运行环境是使用-client模式进行,排除重排序的干扰

Java中的原子性

  • jdk文档对于原子性的说明如下:
    • 除了long和double类型之外,引用变量与大多数的原始数据类型都具备读写操作的原子性
    • 所有使用volatile修饰的变量都具备读写操作的原子性
  • 分析
    • 针对64bit的数据类型,主要与处理器(32bit/64bit)有关,在32bit处理器上,JVM会将64bit的long/double划分为两个32bit的写操作,并不具备原子性(数据的读写主要是通过处理器总线与主内存进行传递)
    • 基于Happen-Before原则,对于volatile的变量读取总是可以“看到”任何一个线程对该volatile变量的最后写入,因此在临界区代码的执行是具备原子性,即使是long或是double类型

volatile修饰单个变量的自增减问题

  • 代码
// 部分代码,在上述的写线程进行修改, 前提: volatile修饰变量sharedVar  Thread t1 = new Thread(){              @Override              public void run() {                  try {                      // modify the sharedVar                      TimeUnit.MICROSECONDS.sleep(500L);                      sharedVar ++;                      System.out.printf("%s modify the shared var with atomic %s  ...n", "thread-1", sharedVar);                  } catch (Exception e) {                      System.out.println(e);                  }              }          };
  • 运行结果
  • sharedVar = 20L显示的字节码如下:
 L3   LINENUMBER 102 L3   BIPUSH 20		// 压入线程的操作数栈   INVOKESTATIC com/xiaokunliu/blogs/thread/volatile2code/VolatileUsedClass.access$002 (I)I    //实例化并加载sharedVar并压入操作数栈,说明完成赋值操作   POP   // 弹出数据   L4
  • sharedVar ++ 显示的字节码如下:
 L3   LINENUMBER 27 L3   INVOKESTATIC com/xiaokunliu/blogs/thread/volatile2code/VolatileUsedClass.access$000 ()I  // 实例化并加载sharedVar并压入操作数栈   ICONST_1    // 将常量 1 压入线程操作数栈总   IADD            // 执行sharedVar+1   INVOKESTATIC com/xiaokunliu/blogs/thread/volatile2code/VolatileUsedClass.access$002 (I)I  // 重新实力化加载sharedVar,说明完成赋值操作   POP   L4  
  • 运行分析:
    • 通过字节码可知,sharedVar ++;相比单纯赋值操作增加了一个添加的动作
    • 也就整体代码块存在在并发多线程下交替执行的两个操作,不具备原子性,volatile在这里是保证代码刷新到主内存,对于sharedVar = const 是具备原子性的

使用volatile小结

  • 对变量进行写操作的时候可以通过volatile来实现对其他线程的可见,同样在单步指令操作中是具备原子性,针对long或者double也起到具备原子性的作用
  • 对于需要用volatile修饰的变量来完成一系列的非单步操作运算是无法保证原子性,必须借助lock的方式来实现代码块的原子性
  • JDK关于原子性问题说明参考:
参考JDK关于原子性文档:  https://docs.oracle.com/javase/tutorial/essential/concurrency/atomic.html

感谢花时间阅读,如有用请转发或者点个好看,谢谢!