zoukankan      html  css  js  c++  java
  • Volatile 关键字的原理和实现

    1. 前言

    Volatile 是一个经常用于多线程并发下的关键字,作用是标记某个变量,让其多个线程并发读写时必须取最新的值。理解volatile关键字,先要理解内存交互操作。

    2. 内存间交互操作

    JVM 规定了以下8种操作是原子性的(因为long和double类型的非原子性协定,以下只针对32位的基础类型)。作为使用者一般只要用先行发生(Happens-Before)原则,思考下面加粗的内容即可。即如果A发生的操作能被B观察到,且指令顺序不在JVM规定的先行发生规则时,就有可能发生指令重排而线程不安全。

    • lock(锁定)
    • unlock(解锁)
    • read(读取)
    • load(载入)
    • use(使用)
    • assign(赋值)
    • store(存储)
    • write(写入)

    3. Volatitle特性

    一般来说有两个特性:1. 对所有线程的可见性;2. 禁止指令重排优化。

    3.1 对所有线程的可见性

    一个线程修改了 Volatitle变量后,能将影响立即同步到其他线程。其做法是比较容易理解的,线程的私有变量是放在栈帧里不让访问的(子内存区),共享变量则是栈帧里保留了共享变量的引用,读写时再去主内存区(堆或者直接内存)里读写。那么在读Volatitle变量时,必须先从主内存区load载入然后立即read读取。写Volatitle变量时,在write写值后立即store存回主内存区。
    这里的关键就是立即,两个原子性的操作组成了一个新的原子操作(load-read、write-store),期间不允许干其他能影响该值的事情,以此保证读时总是读到最新值,写时立即能影响到其他线程。
    这里需要注意,Volatitle在仅有单纯读和单纯写时是线程安全的,在做读写计算操作时并不是线程安全的。这是因为java的运算操作符不是原子操作。

    public class Test{
    	public static volatile int sum = 0;
    	public static void increase() {
    		sum++;
    	}
    	public static void main(String[] args) {
    		Thread[] threads = new Thread[10];
    		for(int i = 0; i < 10; i++) {
    			threads[i] = new Thread(new Runnable() {
    				@Override
    				public void run(){
    					for(int i = 0; i < 10000; i++) {
    						increase();
    					}
    					System.out.println("Thread now = " + sum);
    				}
    			});
    			threads[i].start();
    		}
    		while(Thread.activeCount() > 1) {
    			Thread.yield();
    		}
    		System.out.println(sum);
    	}
    }
    

    预计应该是10^6,但一般都是小于该值,《深入了解JAVA虚拟机》里解释了这个问题,从字节码上看,++指令会有一个将变量取至操作栈顶再加一赋值的操作,取栈顶时数据无误,但进行此时已经是子内存了,接下来加一和赋值时,主内存值可能已经修改,子内存和主内存不同步,故写回主内存时数据子内存已经是过期数据。

    3.2 禁止指令重排优化

    因为read是原子的,write是原子的,单一线程内指令串行的,故保证有序。指令重排时会对所有线程的指令进行重新排序以优化执行效率,这个是机器级别的优化,故线程看自己时有序的,看其他线程就是乱序的,做为线程自身没法保证所有线程对volatile的变量的操作有序。
    于是只能有JVM这个老大哥出面进行协调了,具体的做法是在读写的赋值前,JVM会插入一条lock addl $0x0,(%esp)之类的指令,含义为对esp寄存器的值加0并写入缓存。因为lock不允许和专门的nop空指令配合使用,故用这种无意义的操作来替代空操作,同时用lock将缓存值写入内存中(stroe-write),构成了一个内存屏障(Memory Barrier 或 Memory Fence)。内存屏障告诉操作系统,在重排序时不允许将后面的指令排到内存屏障的前面,也就是只能在两个内存屏障间进行指令重排优化,这样就保证了屏障前的volatile变量修改值能立刻影响到所有处理器和线程。

    参考资料:《深入理解java虚拟机》

  • 相关阅读:
    第一篇正式文章 随便聊聊吧
    CSS 28 块之间的空格
    CSS 27 贴在下方
    CSS 26 左右固定
    CSS 25 垂直居中
    CSS 24 左侧固定
    CSS 23 水平居中
    CSS 22 显示方式
    CSS 21 浮动
    CSS 20 相对定位
  • 原文地址:https://www.cnblogs.com/hyry/p/15232099.html
Copyright © 2011-2022 走看看