解决volatile关键字不保证原子性问题以及禁止指令重排详解

64 阅读3分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

解决volatile关键字不保证原子性问题 使用juc下的AtomicInteger

原子操作,天生单位最小不可分割

package com.wsx;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * volatile不保证原子性
 * 解决方案:
 * 1.加sync关键字
 * 2.使用atomicInteger
 */
class MyData{

    volatile int i = 0;

    public void numberPlusPlus(){
        i++;
    }

    AtomicInteger atomicInteger = new AtomicInteger();

    public void numberPlus(){
        atomicInteger.getAndIncrement();//相当于i++,此构造函数默认无参构造器是从0开始累加,可以传入int初始值
    }
}

public class VolatileSolveNoAtomic {
    public static void main(String[] args) {
        MyData myData = new MyData();

        for (int i = 1; i <= 20; i++) {//二十个线程
            new Thread(()->{
                for (int j = 1; j <= 1000; j++) {//一个线程打印2000次
                    myData.numberPlusPlus();
                    myData.numberPlus();
                }
            },String.valueOf(i)).start();
        }

        while (Thread.activeCount()>2){//activeCount是因为main一个线程gc一个线程然后如果这二十个线程组打印完则会小于2,故停止对main线程的休眠
            Thread.yield();//Thread是main线程
        }

        System.out.println(Thread.currentThread().getName()+"number加后的值:"+myData.i);
        System.out.println(Thread.currentThread().getName()+"number加后的值:"+myData.atomicInteger);
    }
}

线程安全性获得保证:可见性,原子性,有序性

volatile指令重排案例1

高考时,做题,先找会的做,做题顺序不一定和出题顺序一致,这就是指令重排

有序性

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

源代码 编译器优化的重排 指令并行的重排+内存系统的重排 最终执行的指令

单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重排序时必须要考虑指令之间的数据依赖性 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测

编译器,系统重排,内存重排

以下情况禁止指令重排

image.png 有可能是5也有可能是6

因为a=1和flag=true没有依赖性,可能先flag=true先执行然后a=a+5马上跟着执行然后就成a=0+5

使用volatile关键字可以解决指令重排

volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象

内存屏障(Memory Barrier) 又称内存栅栏,是一个CPU指令,它的作用有两个:

一是保证特定操作的执行顺序, 二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性) 。

由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。

image.png