深入理解java中volatile的特性

一、 对volatile的理解

1. volatile是java虚拟机提供的轻量级的同步机制。

  • 保证可见性
  • 不保证原子性
  • 禁止指令重排

保证可见性

什么是可见性?
JMM(java内存模型)
JMM是一个抽象的概念本身不存在,它描述的是一组规范,通过这组规范定义了程序中各个变量的访问方式。

  • 可见性
  • 原子性
  • 有序性

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

JMM可见性图解:
在这里插入图片描述

volatile保证可见性的代码证明:

没有添加volatile关键字修饰

package com.jess.juc;

import java.util.concurrent.TimeUnit;

class Data {
    
    
    int number = 0;

    public void add(){
    
    
        this.number = 60;
    }
}

public class VolatileDemo{
    
    
    public static void main(String[] args) {
    
    
        // 假如int number = 0; number变量之前没有添加volatile关键字修饰
        Data data = new Data();
        new Thread(()->{
    
    
            System.out.println(Thread.currentThread().getName()+" 线程启动");
            //暂停一下线程 3s
            try {
    
    
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            data.add();
            System.out.println(Thread.currentThread().getName()+" 线程更新:"+data.number);
        },"A").start();

        //main线程
        while (data.number == 0){
    
    
            //等待,知道number不在等于0
        }
        //成功打印说明main线程感知到number值变了,体现了可见性
        System.out.println(Thread.currentThread().getName()+" 线程" + "任务完成");
    }
}

在这里插入图片描述
main线程并没有打印任务完成,说明没有感知到number值发生了变化

添加volatile关键字修饰

class Data {
    
    
   volatile int number = 0;

    public void add(){
    
    
        this.number = 60;
    }
}

在这里插入图片描述

volatile 可见性的实现

  • 在生成汇编代码指令时会在 volatile 修饰的共享变量进行写操作的时候会多出 Lock 前缀的指令
  • Lock 前缀的指令会引起 CPU 缓存写回内存
  • 一个 CPU 的缓存回写到内存会导致其他 CPU 缓存了该内存地址的数据无效
  • volatile 变量通过缓存一致性协议保证每个线程获得最新值
  • 缓存一致性协议保证每个 CPU 通过嗅探在总线上传播的数据来检查自己缓存的值是不是修改
  • 当 CPU 发现自己缓存行对应的内存地址被修改,会将当前 CPU 的缓存行设置成无效状态,重新从内存中把数据读到 CPU 缓存

不保证原子性

什么是原子性?
即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

添加volatile关键字修饰

package com.jess.juc;

class Data {
    
    
   volatile int number = 0;

    public void sub(){
    
    
        number -= 1;
    }
}

public class VolatileDemo{
    
    
    public static void main(String[] args) {
    
    
       //验证volatile不保证原子性
        Data data = new Data();
        //生成20个线程
        for (int i = 0; i < 20; i++) {
    
    
            new Thread(()->{
    
    
                for (int j = 0; j < 100; j++) {
    
    
                    data.sub();
                }
            },String.valueOf(i)).start();
        }

        //需要等待20个线程都完成sub后,在执行main线程
        //后台默认2个线程,一个是main,一个是gc线程
       while (Thread.activeCount() > 2){
    
    
           Thread.yield();
       }
        System.out.println(Thread.currentThread().getName()+"  number:"+data.number);
    }
}

若volatile可以保证原子性,那么number最终值应该是 -2000
在这里插入图片描述
volatile为什么不保证原子性?
num初始为0,可能会出现线程1,2,3线程在更新了数据(+1)后都要写回主内存的时侯。因为CPU调度使得1,2线程挂起,执行3线程的写入操作(num=1)。此时3线程更新的数据写入了主内存,然后1,2线程再进行写入,覆盖了num的值,num的值依然为1(还没来得及收到num的更新通知)。若保证原子性num应该为3。(即出现了丢失写值的情况)

(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。

如何解决原子性?

  1. 加synchronized(不推荐)
  2. 如下代码所示
package com.jess.juc;

import java.util.concurrent.atomic.AtomicInteger;

class Data {
    
    
   volatile int number = 0;

    public void sub(){
    
    
        number -= 1;
    }

    AtomicInteger atomicInteger = new AtomicInteger();
    public void subMyAtomic(){
    
    
        atomicInteger.getAndDecrement();
    }
}

public class VolatileDemo{
    
    
    public static void main(String[] args) {
    
    
       //验证volatile不保证原子性
        Data data = new Data();
        //生成20个线程
        for (int i = 0; i < 20; i++) {
    
    
            new Thread(()->{
    
    
                for (int j = 0; j < 100; j++) {
    
    
                    data.subMyAtomic();
                }
            },String.valueOf(i)).start();
        }

        //需要等待20个线程都完成sub后,在执行main线程
        //后台默认2个线程,一个是main,一个是gc线程
       while (Thread.activeCount() > 2){
    
    
           Thread.yield();
       }
        System.out.println(Thread.currentThread().getName()+"  number:"+data.atomicInteger);
    }
}

在这里插入图片描述

禁止指令重排

计算机在执行程序时,为了能提高性能,编译器和处理器常会对指令进行重排。
在这里插入图片描述
简单来说,指令重排就是程序指令的执行顺序有可能和代码的顺序不一致。

  • 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
  • 编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
  • 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。

volatile怎样禁止指令重排呢?
内存屏障又称内存栅栏,是一个CPU指令,它的作用有两个:

  1. 保证特定操作的顺序
  2. 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
    在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/myjess/article/details/120294878