【多线程与并发】JMM与volatile

一、java内存模型

每个线程访问共享变量时(如静态变量、单例中的成员变量等),会将该变量从主内存中存入一个副本到自己的工作内存。线程对这些共享变量的读写只会在工作内存中进行,且线程间不能访问对方的工作内存,共享变量的值传递需要通过主内存完成。这种内存模型的主要来源是计算机硬件模型,线程是cpu的基本调度单位,线程的工作内存就是cpu对应的高速缓存cache。那么为什么要用这种模型呢?原因还是效率问题:CPU速率>Cache速率>主内存速率>io外设速率。两者关系见下图
深入理解java虚拟机12-1
深入了解java虚拟机12-2

二、jvm内存操作原语

  • lock:主内存变量加锁,标识为线程独占状态
  • unlock:主内存变量解锁,解锁后其他线程可以锁定该变量
  • read:将主内存变量传输到线程工作内存
  • load:将read过来的变量值加载到工作内存的副本中
  • use:将工作内存变量传给执行引擎
  • assign:将执行引擎计算的值赋给工作内存变量
  • store:将工作内存变量传输到主内存
  • write:将store过去的变量值写入主内存

需要注意的是一条字节码指令并不意味着只有一条原语,一条字节码指令在执行时可能需要运行多个原语才能实现它的语义

三、并发三要素

  1. 原子性:一组指令同一时刻只能有一个线程执行,不会被其他线程干扰
  2. 可见性:当一个线程修改了共享变量的值后,其他线程能够立即感知到。
  3. 有序性:程序执行的顺序按照代码的先后顺序执行

四、volatile

volatile作为java内置的关键字,实现轻量级的同步,实现了三要素中的可见性和有序性

  1. 可见性:可见性是如何实现的呢?JMM对volatile语义有要求:线程在读取volatile修饰的共享变量时需要每次从主内存获取;线程在修改volatile修饰的变量时需要立刻写入主内存(happens-before原则的Volatile Variable Rule)。即保证了read-load-use 和 assign-store-write成为了两个不可分割的原子操作。下面看一个经典的测试程序(共享变量作为while条件),有个很有意思的情况可以看下即使不加volatile,只要在循环中加入一点耗时的操作即可实现普通变量的可见性。网上查了一些问答给出的是这个原因:CPU空闲后会遵循jvm优化基准,尽可能快的保证数据的可见性,从而将变量b从主内存同步到工作内存中,最终导致程序的结束

    import java.util.concurrent.TimeUnit;
    
    /**
     * 不加volatile的话,也可以做到变量b的可见性,只需要放开下面while循环中注释的任意一个即可
     * 测试jdk版本:1.8
     * 测试OS:MacOS 10.13.6
     */
    public class VolatileTest {
        static volatile boolean b = true;
    
        public static void main(String[] args) throws InterruptedException {
            new Thread(() -> {
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                change();
            }, "t").start();
    
            while (b) {
                // TimeUnit.NANOSECONDS.sleep(1);
                // Thread.yield();
                // System.out.print("\r");
            }
            System.out.println("finish");
        }
    
        public static void change() {
            b = false;
        }
    }
    
  2. 有序性:主要体现在禁止指令重排序,内存屏障
    hotspot源码:

    //hotspot/src/os_cpu/linux_x86/vm/orderAccess_linux_x86.inline.hpp
    inline void OrderAccess::fence() {
      if (os::is_MP()) {
        // always use locked addl since mfence is sometimes expensive
    #ifdef AMD64
        __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
    #else
        __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
    #endif
      }
    }
    

    还是上面那个程序,我们来执行命令java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=compileonly,*VolatileTest.change VolatileTest > asm.out反汇编,结果如下:

      0x00000001169063ac: movabs $0x76ab72688,%rsi  ;   {oop(a 'java/lang/Class' = 'VolatileTest')}
      0x00000001169063b6: mov    $0x0,%edi
      0x00000001169063bb: mov    %dil,0x68(%rsi)
      0x00000001169063bf: lock addl $0x0,(%rsp)     ;*putstatic b
                                                    ; - VolatileTest::change@1 (line 30)
    

    可以看到最后一行的汇编指令lock addl $0x0,(%rsp)和hostspot中定义的是一样的。这个指令的含义是把ESP寄存器的值加0,是一个空操作,关键是lock指令,它将本处理器的缓存写入了内存实现了内存屏障。

  3. 由于volatile没有原子性,因此即使多线程修改被volatile修饰的共享变量也不是线程安全的。例如a++这类自增操作,需要先进行read-load-use 然后再 assign-store-write。由于volatile只能分别保证这两个操作的原子性而不是连起来的原子性,所以很有可能当你assign-store-write的时候工作内存的变量a已经是过时的数据了。

五、参考

深入理解java虚拟机

猜你喜欢

转载自blog.csdn.net/hch814/article/details/106298963
今日推荐