volatile可以保证原子性吗

可见性之volatile关键字

什么是可见性

cpu在执行代码的时候,为了减少变量访问的时间消耗可能会将代码中访问的变量值缓存到该CPU的缓存区,所以在相应代码再次访问某个变量时,读到的值可能来自于缓存区而不是主存,同样,代码对这些缓存过的变量的值的修改也可能只是被写入到缓存区,而没有回写到主存,且每个CPU都有自己的缓存区,因此一个CPU缓存区中的内容对于其他CPU是不可见的

可见性就是一个线程对于共享变量的修改能够及时的被其他线程看到

共享变量是指如果一个变量在多个线程的工作内存中都存在副本,那么这个变量就是就几个线程的共享变量

在java规范中指出:为了获得更快的速度,允许线程保存共享变量的私有拷贝,而且只有在线程进入或者离开同步代码块时才会将私有拷贝与共享内存中的原始值进行比较

可见性的实现方式

  • synchronized(保证原子性和可见性) synchronized关键字
  • volatile(只保证可见性),如果变量已在synchronized代码块中,或者为常量时,没有必要使用volatile修饰,比synchronized更快,不会引起线程上下文的切换和调度

先举一个例子,来看一下不使用volatile的时候

java 复制代码
public class TestVolatile {
    
    public static void main(String[] args) {
        ThreadDemo runnable = new ThreadDemo();
        new Thread(runnable).start();

        while (true){
            // 不会停止  一直在while循环
            if(runnable.isFlag()){
                System.out.println("----------");
                break;
            }
        }
    }
    
}

class ThreadDemo implements Runnable{

    private boolean flag;
    @Override
    public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        flag = true;

        System.out.println(Thread.currentThread().getName()+"---flag:"+flag);
    }

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }
}

结果

console 复制代码
Thread-0---flag:true

在这里主线程一直获取不到子线程中flag的状态修改为true了,一直在while循环中不出来,这是为什么呢

现象解读

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1或L2)后在进行操作,但是缓存不知道何时会写到内存

对于共享数据,在每个线程中都会缓存一份数据,

对于上面的例子来说就是 main线程在线程内部缓存了一份flag=false Thread-0线程也在线程内部缓存了一份flag=false,然后Thread-0线程将flag改为true之后同步到主存中,但是main线程的while循环使用的底层的循环机制,效率特别高,根本就没有从主存中重新去获取一下新的数据,线程内的缓存数据没有更新,导致一直在while循环中。

而这种问题就是内存不可见导致的,可以使用synchronized来解决,

synchronized在获取锁前后也要保证数据的一致性,获取锁 读内存屏障 释放锁 写内存屏障

所以可以在while循环中加上synchronized关键字来解决

但是加锁效率太低,所以一般情况下使用volatile关键字解决该问题,对于flag关键字加上volatile来修饰

java 复制代码
private volatile boolean flag;

volatile具有内存可见性的作用

volatile的作用

  • 内存可见性,volatile修饰的变量不会被缓存在寄存器中,每次都是从主存中读取,对于其他线程全部可见

  • 禁止指令重排序(CPU的缓存一致性协议),保证了对于volatile修饰的变量会按照代码顺序执行

在单例懒汉式中双重检测时就使用了volatile禁止指令重排序

在实例化对象时,JVM分为三步

1、申请分配对象内存空间 memory=allocate()

2、对象初始化,成员变量初始化 instance(memory)

3、设置instance指向刚分配的内存地址,此时instance != null instance = memory ,堆地址给栈

如果指令重排的话就会导致不是按照先后顺序进行执行,如1->3->2,其实内部使用的属性还未初始化

为什么要有指令重排序

指令重排序可以提高性能,每一个指令包含多个步骤,每个步骤可能使用不同的硬件,在不影响结果的情况下,可以进行指令执行顺序的调整,减少了停顿,提高了CPU的处理能力,但是会造成乱序问题

指令重排分为三种

  • 编译器优化重排:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
  • 指令并行重排:使用指令集并行技术将多条指令重叠执行,在不存在数据依赖性的前提下,可以改变语句对应的机器指令的执行顺序
  • 内存系统重排:由于处理器使用缓存和读写缓冲区,使得加载和存储操作看上去可能是在乱序执行

volatile的执行过程

在写一个volatile变量时,JMM会把线程对应的本地内存中的共享变量值刷新到主内存。

当读一个volatile变量时,JMM会把线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量

何时使用

  • 写入变量值不依赖变量的当前值时,因为volatile不保证原子性
  • 读写变量时没有加锁,因为加锁已经保证了可见性,就不需要把变量声明为volatile

内存屏障

内存屏障(Memory Barrier)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。java编译器也会根据内存屏障的规则禁止重排序

分为几个类型

  • LoadLoad屏障:对于Load1;LoadLoad;Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕

  • StoreStore屏障:对于Store1;StoreStore;Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其他处理器可见

  • LoadStore屏障:对于Load1;LoadStore;Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕

  • StoreLoad屏障:对于Store1;StoreLoad;Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。开销最大

volatile如何实现可见的

volatile是通过加入内存屏障和禁止重排序来实现的

  • volatile关键字会将变量写操作前加一个Release屏障 ,以保证写操作不会进行指令重排,在写操作后加一个Store屏障,以保证写完数据之后立刻刷新到主内存;

  • 在读操作前加一个Load屏障 ,以保证读到的数据是最新的,在读操作之后加一个Acquire屏障,禁止读操作后的任何读写操作会跟读操作指令重排

Acquire屏障 =LoadLoad屏障 +LoadStore屏障

Release屏障 =StoreLoad屏障 +StoreStore屏障

volatile和synchronize的区别

  • volatile是告诉JVM当前变量在寄存器中的值是不准确的,需要从主存读取;synchronize则是使得只有当前线程可以访问该变量,其他线程阻塞住,保证了同一时刻只有一个线程在操作变量
  • volatile仅能进行修饰变量;synchronize可以修饰方法和代码块
  • volatile只能实现可见性,不能保证原子性;synchronize能保证可见性和原子性
  • volatile不会造成线程阻塞;synchronize会造成线程阻塞
  • volatile标记的变量禁止了指令重排;synchronize没有禁止指令重排

zhhll.icu/2020/多线程/基础...

本文由mdnice多平台发布

相关推荐
七星静香22 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
Jacob程序员23 分钟前
java导出word文件(手绘)
java·开发语言·word
ZHOUPUYU23 分钟前
IntelliJ IDEA超详细下载安装教程(附安装包)
java·ide·intellij-idea
stewie626 分钟前
在IDEA中使用Git
java·git
Elaine20239142 分钟前
06 网络编程基础
java·网络
G丶AEOM43 分钟前
分布式——BASE理论
java·分布式·八股
落落鱼201344 分钟前
tp接口 入口文件 500 错误原因
java·开发语言
想要打 Acm 的小周同学呀1 小时前
LRU缓存算法
java·算法·缓存
镰刀出海1 小时前
Recyclerview缓存原理
java·开发语言·缓存·recyclerview·android面试
阿伟*rui3 小时前
配置管理,雪崩问题分析,sentinel的使用
java·spring boot·sentinel