volatile关键字

1.Volatile介绍

  • volatile是Java虚拟机提供的轻量级的同步机制
  • 三大特性:
    • 保证变量的内存可见性
    • 不保证原子性
    • 禁止指令重排序
  • 非阻塞算法,不会造成线程上下文切换的开销

2. volatile的内存语义

  • 当写入了volatile变量值时就等价于线程退出synchronized代码块(把写入工作内存的值同步到主内存)
  • 当读取volatile变量值就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)

3.可见性问题

  1. 多核执行多线程的情况下,每个core读取变量不是直接从内存读,而是从L1, L2 …cache读,所以你在一个core中的write不一定会被其他core马上观测到。
  2. 解决这个的办法就是volatile关键字,加上它修饰后,变量在一个core中做了修改,会导致其他core的缓存立即失效,这样就会从内存中读出最新的值,保证了可见性。
public class VolatileExample{
    public static void main(String[] args){
        MyTread myTread = new MyTread();
        myThread.start();
        for(; ; ){
            if(myThread.isFlag()){
                System.out.println("主线称访问到flag变量");
            }
        }
    }
}
//子线程类
class MyTread extends Thread{
    private boolean flag = false;
    public void run(){
        try{
            Thread.sleep(100);
        }catch(InterruptedException e){
            e.printStackTrace();
        }
        flag = true;
        System.out.println("flag = " + flag);
    }
    public boolean isFlag(){
        return flag;
    }
    public void setFlag(boolean flag){
        this.flag = flag;
    }
}

  • 因为线程myTread start后休息了100毫秒,然后线程main调用isFlag方法时线程myThread线程还没将最新值写入主内存,产生了不可见性的问题
  • 而主线程一直在for循环中,没结束任务,还用的自己开始时工作内存中拷贝过来的值,无法从知道flag值已经被更新
import java.util.concurrent.TimeUnit;

class MyData{
    int number = 0;
    public void addTo60(){
        this.number = 60;
    }
}

/**
 * 验证volatile的可见性
 * 1.1假如int number = 0; number 变量之前根本没添加volatile关键字修饰,没有可见性
 *
 */
public class VolatileDemo{
    public static void main(String[] args){
        MyData myData = new MyData(); //资源类
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName()+"\t come in");
            try{
                TimeUnit.SECONDS.sleep(3);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            myData.addTo60();
            System.out.println(Thread.currentThread().getName()+"\t updated number value: 60"+ myData.number);
        },"AAA").start();
        //main为第二个线程
        while(myData.number == 0){

        }
        System.out.println(Thread.currentThread().getName()+"\t mission is over");
    }
}

4.可见性的解决方案

  • 加锁以及volatile关键字

  • 加锁:

    • 使用synchronized

    •   public static void main(String[] args){
            MyThread myThread = new MyThread();
            myThread.start();
            for(; ; ){
                synchronized(myThread){
                    if(myThread.isFlag())
                        System.out.println("主线程访问到flag变量");
                }
            }
        }
      
    • 当一个线程进入 synchronizer 代码块后,线程获取到锁,会清空本地内存,然后从主内存中拷贝共享变量的最新值到本地内存作为副本,执行代码,又将修改后的副本值刷新到主内存中,最后线程释放锁。

    • 在for这个死循环中,main线程每进入一次这个同步代码块,都会去主物理内存中拷贝最新的值。

  • 使用volatile关键字:

    public MyThread extends Tread{
        private volatile boolean flag = false;
        public void run(){
            try{
                Thread.sleep(1000);
            }catch(InterruptedException e){
                e.printStackTrace();
            }
            flag = true;
            System.out.println("flag = " + flag);
        }
        public boolean isFlag(){
            return flag;
        }
        public void setFlag(boolean flag){
            this.flag = flag;
        }
    }
    
    • 使用 volatile 修饰共享变量后,每个线程要操作变量时会从主内存中将变量拷贝到本地内存作为副本,当线程操作变量副本并写回主内存后,会通过 CPU 总线嗅探机制告知其他线程该变量副本已经失效,需要重新从主内存中读取。

    • volatile 保证了不同线程对共享变量操作的可见性,也就是说一个线程修改了 volatile 修饰的变量,当修改后的变量写回主内存时,其他线程能立即看到最新值。

    • volatile则是告知还在for循环的main线程,说flag的值更新了

5.原子性

在Java语言中,long,double型以外的任何类型的变量的写操作都是原子操作

即,byte\boolean\short\char\float\int 的变量和引用型变量的写操作都是原子的

原因:long、double占用8字节、64位,而32位的虚拟机对这种变量的写操作可能被分解为两个步骤来实施

class MyData{
    volatile int number = 0;
    public void addTo60(){
        this.number = 60;
    }
    public void addPlusPlus(){
        //在多线程下是线程b安全的
        number++;
    }
}

/**
 * 验证volatile的可见性
 * 1.1假如int number = 0; number 变量之前根本没添加volatile关键字修饰,没有可见性
 *
 */
public class VolatileDemo{
    public static void main(String[] args){
        MyData myData = new MyData();
        for(int i = 1; i <= 20; i++){
            new Thread(() -> {
                for(int j = 0; j <= 1000; j++){
                    myData.addPlusPlus();
                }
            },String.valueOf(i)).start();;
        }
        //加上while (Thread.activeCount() > 2)后,主线程就会一直在此循环里,知道子线程执行完并且Thread.activeCount() 小于2了
        while(Thread.activeCount() > 2){
            Thread.yield();
            //使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。
        }
        System.out.println(Thread.currentThread().getName()+"\t finally number value:"+myData.number);
 /*       try{
            TimeUnit.SECONDS.sleep(5);
        }catch (InterruptedException e){
            e.printStackTrace();
        }

  */
    }

  • 理论解释:

字节码文件中n++被拆分成3个指令、4个句子
  • 解决方法:

    • 加synchronized等锁

    • 原子类(JUC下AtomicInteger)

    ![](https://img2022.cnblogs.com/blog/2182638/202202/2182638-20220220160647453-1112939916.png)
    

6.有序性

  • 计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分以下三种:

7.Volatile原理

  • 当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序
  • 在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比synchronized关键字更轻量级的同步机制
  • 当对非volatile变量进行读写时,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。
  • 而声明变量是volatile的,JVM保证了每次读变量都从内存中读,跳过CPU Cache这一步
  • 当一个变量定义为volatile之后,将具备两种特性
    • 保证此变量对所有的线程的可见性
    • 禁止指令重排序优化。有volatile修饰的变量赋值后多执行了“load addl $0x0,(%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置)

8.volatile的原子性问题

  • 所谓原子性是指在一次操作或者多次操作中,要么所有的操作全部得到了执行并且不会受到任何因素的干扰而中断,要么所有操作都不执行
  • 在多线程环境下使用volatile修饰的变量是线程不安全的
  • 解决:
    • 锁机制
    • 原子类(AtomicInteger)
  • 注:对任意单个使用的volatile修饰的变量的读写是具有原子性的,但类似于 flag = !flag 这种符合操作不具有原子性。

9.禁止指令重排序

public class ReSortSeqDemo{
    int a = 0;
    boolean flag = false;
    public void method01(){
        a = 1;
        flag = true;
    }
    public void method01(){
        if(flag){
            a = a + 5;
            System.out.println("retValue: "+a);
        }
    }
}
  • 内存屏障(内存栅栏):是一个CPU指令

    • 作用:1.保证特定操作的执行顺序

      ​ 2.保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

    • 写volatile变量时,可以确保volatile写之前的操作不会被编译器重排序到volatile写之后

    • 读volatile变量时,可以确保volatile读之后的操作不会被编译器重排序到volatile读之后

10.总线嗅探机制

  • 工作原理:每个处理器通过监听在总线上传播的数据来检查自己的缓存值是不是过期了,如果处理器发现自己的缓存行对应的内存地址修改,就会将当前处理器的缓存行设置无效状态,当处理器对这个数据进行修改操作时,会重新从主内存中把数据读到处理器缓存中

11.Volatile 用处

  • 写入变量值不依赖变量的当前值时,因为如果依赖当前值,则将是获取--计算--写入 三步操作。而volatile不保证原子性

  • 读写变量值时没有加锁。

    如:

  1. 单例模式DCL代码

  2. 单例模式volatile分析

  3. 使用volatile变量作为状态标志

  4. 使用volatile保证可见性

12. 伪共享(False sharing)

伪共享的产生是因为多个变量被放入了一个缓存行中,并且多个线程同时去写入缓存行中的不同变量。

  1. JDK8之前都是通过字节填充的方式
  2. JDK1.8之后提供了一个sun.misc.Contended注解

小结

  1. CPU具有多级缓存,越接近CPU的缓存越小也越快;
  2. CPU缓存中的数据是以缓存行为单位处理的;
  3. CPU缓存行能带来免费加载数据的好处,所以处理数组性能非常高;
  4. CPU缓存行也带来了弊端,多线程处理不相干的变量时会相互影响,也就是伪共享;
  5. 避免伪共享的主要思路就是让不相干的变量不要出现在同一个缓存行中;
  6. 一是每两个变量之间加七个 long 类型;
  7. 二是创建自己的 long 类型,而不是用原生的;
  8. 三是使用 java8 提供的注解;
posted @ 2022-02-20 16:08  ftfty  阅读(38)  评论(0编辑  收藏  举报