volatile关键字专题

98 阅读4分钟

1、案例

计算机的模型:

img

计算机的数据是存储于硬盘的,但是因为要计算,而硬盘的数据存取很慢,所以要把数据加载到主存中,可是因为cpu执行的还是比主存要快很多,所以执行效率还是会被主存的加载拖累。所以这里旧加上了cpu的高速缓存,一些需要执行的数据会被加入到高速缓存中。

Java内存模型和这个同理:

img

数据还是加载到每个线程的本地内存中执行,再交给缓存,这里就有个疑问,每个线程把这个值拿到自己加里,别人修改了该线程知道吗?很明显不知道,比如下面这个例子:

public class TestVolatile {
    public static boolean initFlag = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(){
            @Override
            public void run() {
                System.out.println("第一个线程准备接收消息");
                while(!initFlag){
                }

                System.out.println("第一个线程接收到了消息");
            }
        }.start();

        Thread.sleep(2000);

        new Thread(){
            @Override
            public void run() {
                changeFlag();
            }
        }.start();

    }

    public static void changeFlag(){
        System.out.println("准备修改flag");
        initFlag = true;
        System.out.println("修改flag完毕");
    }
}

//控制台:
//第一个线程准备接收消息
//准备修改flag
//修改flag完毕

这里可以修改:

public static volatile boolean initFlag = false;

然后就能执行出想要的效果了。

因为volatile具有能保证数据的可见性和禁止重排的特性。

2、JMM数据的原子操作

  • lock(锁定):作用于主内存,它把一个变量标记为一条线程独占状态;
  • read(读取):作用于主内存,它把变量值从主内存传送到线程的工作内存中,以便随后的load动作使用;
  • load(载入):作用于工作内存,它把read操作的值放入工作内存中的变量副本中;
  • use(使用):作用于工作内存,它把工作内存中的值传递给执行引擎,每当虚拟机遇到一个需要使用这个变量的指令时候,将会执行这个动作;
  • assign(赋值):作用于工作内存,它把从执行引擎获取的值赋值给工作内存中的变量,每当虚拟机遇到一个给变量赋值的指令时候,执行该操作;
  • store(存储):作用于工作内存,它把工作内存中的一个变量传送给主内存中,以备随后的write操作使用;
  • write(写入):作用于主内存,它把store传送值放到主内存中的变量中。
  • unlock(解锁):作用于主内存,它将一个处于锁定状态的变量释放出来,释放后的变量才能够被其他线程锁定;
image-20211214130028630

3、MESI解决缓存一致性问题

线程之间是不能传递参数的,那么线程2修改了值怎么让线程1拿到的呢?

黄色的是总线,当某个线程修改了变量值会马上传回主内存,当到达总线的时候,cpu总线嗅探机制一旦发现有人修改了变量值,线程会马上把自己这个线程的这个值给失效掉,一旦发现自己的值失效了,会马上去主存中取。一旦使用了volatile关键字,就会开启MESI缓存一致性协议,上面的这些步骤都会执行。

volatile的底层实现原理:

底层是通过汇编lock指令,锁定该区域的缓存,并且写回到主内存中。这一步骤对应着上面的assign步骤,底层当看到lock指令会马上把工作内存中的intFlag = true马上同步到主内存中,然后触发总线嗅探进而让其他线程的值失效。

在这里加lock也能保证线程安全的一些问题,比如经过总线的时候触发了嗅探,但是还没有write进主内存的时候,另一个线程马上感知,进而快速读取initFlag,这样读到的还是写入之前的false,或者两个线程同时写回主内存,也可能出现线程安全问题。

4、volatile不能保证原子性

public class TestAtomic {
    public static volatile int num = 0;

    public static void increase(){
        num++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(){
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        increase();
                    }
                }
            };
            threads[i].start();
        }

        for (Thread thread:threads) {
            thread.join();
        }

        System.out.println(num);
    }
}

这样跑完计算num肯定是小于等于10000的。所以说volatile是不能保证原子性的。