「Java并发编程」初识Volatile(概念与特性)


一、什么是Volatile

概念

volatile是Java中的关键字,用来修饰会被不同线程访问和修改的变量。volatile可以说是java虚拟机提供的最轻量级同步机制

二、特性

1、保证可见性

概述

保证不同线程对这个变量进行操作时的可见性,即变量一旦改变所有线程立即可见。

代码验证

public class VolatileSeeDemo {
    static boolean flag=true;
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"	 come in");
            while (flag) {

            }
            System.out.println(Thread.currentThread().getName()+"	 come out");
        }, "t1").start();
        //线程睡眠2s
        try{TimeUnit.MILLISECONDS.sleep(2000);}catch(Exception e){e.printStackTrace();}
        flag=false;
        System.out.println(Thread.currentThread().getName() + "	 修改完成");
    }
}

输出结果:程序无法正常结束

线程t1中为何看不到被主线程main修改为false的flag的值?

问题可能:

  1. 主线程修改了flag之后没有将其刷新到主内存,所以t1线程看不到。
  2. 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中更新获取flag最新的值。

解析

由于普通变量没有可见性,无法感知结果的变化,主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中更新获取flag最新的值。 故while里面的flag始终是ture,因此导致程序一直在while循环中。

下面用volatile修饰flag

public class VolatileSeeDemo {
    static volatile boolean flag=true;
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"	 come in");
            while (flag) {
​
            }
            System.out.println(Thread.currentThread().getName()+"	 come out");
        }, "t1").start();
        //线程睡眠2s
        try{TimeUnit.MILLISECONDS.sleep(2000);}catch(Exception e){e.printStackTrace();}
        flag=false;
        System.out.println(Thread.currentThread().getName() + "	 修改完成");
    }
}

运行结果:程序正常结束

解释

由于flag被volatile修饰,故其具有可见性,flag的每一次修改都会被发现,flag由true变为false,循环结束。

volatile修改的变量特点

  1. 线程中读取的时候,每次读取都会去主内存中读取共享变量最新的值,然后将其复制到工作内存
  2. 线程中修改了工作内存中变量的副本,修改之后会立即刷新到主内存 。

volatile变量的读写过程

Java内存模型中定义的8种工作内存与主内存之间的原子操作

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

read: 作用于主内存 ,将变量的值从主内存传输到工作内存,主内存到工作内存

load: 作用于工作内存,将read从主内存传输的变量值放入工作 内存变量副本 中,即数据加载

use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作

assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作

store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存

write: 作用于主内存 ,将store传输过来的变量值赋值给主内存中的变量

由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁 ,所以,JVM提供了另外两个原子指令 :

lock: 作用于主内存 ,将一个变量标记为一个线程独占的状态,只是写时候加锁,就只是锁了写变量的过程。

unlock: 作用于主内存 ,把一个处于锁定状态的变量释放,然后才能被其他线程占用

2、没有原子性

概述

原子性指的是一个操作是 不可中断 的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。

代码验证

class MyNumber{
    static int number=0;
    public static void add(){
        number++;
    }
}
public class VolatileNoAtomicDemo {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int i1 = 0; i1 < 1000; i1++) {
                    MyNumber.add();
                }
            }, String.valueOf(i)).start();
        }
        //暂停3s线程
       try{TimeUnit.MILLISECONDS.sleep(3000);}catch(Exception e){e.printStackTrace();}
        System.out.println(Thread.currentThread().getName() + "	 +myNumber:" + MyNumber.number);
    }
}

输出结果

理论上是输出10*1000=1w的,为什么这里只有4312呢?

字节码角度分析

原子性指的是一个操作是 不可中断 的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。

public void add () {
    i++; // 不具备原子性 ,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分3步完成 
}

如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值 ,那么第二个线程就会与第一个线程一起看到同一个值,

并执行相同值的加1操作,这也就造成了线程安全失败(即一个线程更新数据完成后去刷新主内存,导致正在修改数据的线程直接结束,因此造成部分i++失效,而循环次数一直在累加),因此对于add方法必须使用synchronized修饰,以便保证线程安全.

不保证原子性原因

多线程环境下, "数据计算"和"数据赋值" 操作可能多次出现,即操作非原子 。若数据在加载之后,若主内存count变量发生修改之后,由于线程工作内存中的值在此前已经加载,从而不会对变更操作做出相应变化,即私有内存和公共内存中变量不同步,进而导致数据不一致 。

对于volatile变量,JVM只是保证从主内存加载到线程工作内存的值是最新的,也就是数据加载时是最新的。

由此可见volatile解决的是变量读时的可见性问题,但无法保证原子性,对于多线程修改共享变量的场景必须使用加锁同步

既然一修改就是可见,为什么还不能保证原子性?

volatile主要是对其中部分指令做了处理

要use(使用)一个变量的时候必需load(载入),要载入的时候必需从主内存read(读取)这样就解决了读的可见性。

写操作是把assign和store做了关联(在assign(赋值)后必需store(存储) )。store(存储)后write(写入)。

也就是做到了给一个变量赋值的时候一串关联指令直接把变量值写到主内存

就这样通过用的时候直接从主内存取,在赋值到直接写回主内存做到了内存可见性。 注意蓝色框框的间隙。。。。。。o(╥﹏╥)o

总结

读取赋值一个volatile变量的情况

read-load-use和 assign-store-write 成为了两个不可分割的原子操作, 但是在use和assign之间依然有极小的一段真空期 ,有可能变量会被其他线程读取,导致 写丢失一次 ...o(╥﹏╥)o

但是无论在哪一个时间点主内存的变量和任一工作内存的变量的值都是相等的。这个特性就导致了volatile变量不适合参与到依赖当前值的运算,如i = i + 1; i++;之类的那么依靠可见性的特点volatile可以用在哪些地方呢? 通常volatile用做保存某个状态的boolean值or int值。

《深入理解Java虚拟机》提到:

3、禁止指令重排

概述

指令重排序是指编译器或CPU为了优化程序的执行性能而对指令进行重新排序的一种手段,重排序会带来可见性问题,所以在多线程开发中必须要关注并规避重排序。

从源代码到最终运行的指令,会经过如下两个阶段的重排序。

第一阶段,编译器重排序,就是在编译过程中,编译器根据上下文分析对指令进行重排序,目的是减少CPU和内存的交互,重排序之后尽可能保证CPU从寄存器或缓存行中读取数据。从CPU层面来说,避免了处理器每次都去内存中加载stop,减少了处理器和内存的交互开销。

第二阶段,处理器重排序,处理器重排序分为两个部分。

并行指令集重排序,这是处理器优化的一种,处理器可以改变指令的执行顺序。

内存系统重排序,这是处理器引入Store Buffer缓冲区延时写入产生的指令执行顺序不一致的问题。

volatile有关的禁止指令重排的行为

四大屏障的插入情况

代码模拟

模拟一个单线程,什么顺序读?什么顺序写?

public class  VolatileTest {
     int  i  =  0 ;
     volatile boolean  flag  =  false ;
     public void  write(){
         i  =  2 ;
         flag  =  true ;
    }
     public void  read(){
         if ( flag ){
            System. out .println( "---i = "  +  i );
        }
    }
} 

在每一个volatile写操作前面插入一个StoreStore屏障 在每一个volatile写操作后面插入一个StoreLoad屏障 在每一个volatile读操作后面插入一个LoadLoad屏障 在每一个volatile读操作后面插入一个LoadStore屏障

展开阅读全文

页面更新:2024-05-16

标签:赋值   屏障   线程   原子   变量   指令   特性   内存   概念   操作   数据   工作

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top