【Java并发编程】volatile(二):深析volatile原理(代码示例到CPU高速缓存)

首先抛出一个问题:“volatile 这个关键字有什么作用?”。常见的回答或许有两种:

  • 一种是把 volatile 当成一种锁机制,认为给变量加上了 volatile,就好像是给函数加了 sychronized 关键字一样,不同的线程对于特定变量的访问会去加锁;
  • 另一种是把 volatile 当成一种原子化的操作机制,认为加了 volatile 之后,对于一个变量的自增的操作就会变成原子性的

事实上,这两种理解都是完全错误的。volatile 关键字的核心知识点,要关系到 Java 内存模型(JMM,Java Memory Model)上。虽然 JMM 只是 Java 虚拟机这个进程级虚拟机里的一个内存模型,但是这个内存模型,和计算机组成里的 CPU、高速缓存和主内存组合在一起的硬件体系非常相似。理解了 JMM, 可以让你很容易理解计算机组成里 CPU、高速缓存和主内存之间的关系。

1.volatile作用示例

我们先来一起看一段 Java 程序。这是一段经典的 volatile 代码,来自知名的 Java 开发者网站 dzone.com,后续我们会修改这段代码来进行各种小实验。

1.1 代码示例一

public class VolatileTest {
    
      
    private static volatile int COUNTER = 0; // volatile修饰
 
    public static void main(String[] args) {
    
      
        new ChangeListener().start();  // 启动ChangeListener线程  
        new ChangeMaker().start(); // 启动ChangeMaker线程
    }
 	
    // 监听COUNTER变量,当COUNTER发生变化时就将变化的值打印出来
    static class ChangeListener extends Thread {
    
    
        @Override     
        public void run() {
    
       
            int threadValue = COUNTER;  
            while ( threadValue < 5){
    
             
                if( threadValue!= COUNTER){
    
         
                    System.out.println("Got Change for COUNTER : " + COUNTER + ""); 
                    threadValue= COUNTER;            
                }     
            }    
        }  
    }
    
   // 监听COUNTER变量,当COUNTER小于5时就每个500毫秒将COUNTER自增1
   static class ChangeMaker extends Thread{
    
       
       @Override 
       public void run() {
    
      
           int threadValue = COUNTER;  
           while (COUNTER <5){
    
                 
               System.out.println("Incrementing COUNTER to : " + (threadValue+1) + "");  
               COUNTER = ++threadValue;     
               try {
    
                     
                   Thread.sleep(500);      
               } catch (InterruptedException e) {
    
     
                   e.printStackTrace(); 
               }    
           }     
       }  
   }
}

程序的输出结果并不让人意外。ChangeMaker 函数会一次一次将 COUNTER 从 0 增加到 5。因为这个自增是每 500 毫秒一次,而 ChangeListener 去监听 COUNTER 是忙等待的,所以 每一次自增都会被 ChangeListener 监听到,然后对应的结果就会被打印出来

Incrementing COUNTER to : 1 
Got Change for COUNTER : 1
Incrementing COUNTER to : 2 
Got Change for COUNTER : 2
Incrementing COUNTER to : 3
Got Change for COUNTER : 3 
Incrementing COUNTER to : 4 
Got Change for COUNTER : 4 
Incrementing COUNTER to : 5
Got Change for COUNTER : 5

1.2 代码示例二

这个时候,如果我们把上面的程序小小地修改一行代码,把我们定义 COUNTER 这个变量的时候,设置的 volatile 关键字给去掉,会发生什么事情呢?

private static int COUNTER = 0;

结果是 ChangeMaker 还是能正常工作的,每隔 500ms 仍然能够对 COUNTER 自增 1,但是 ChangeListener 不再工作了。在 ChangeListener 眼里,它似乎一直觉得 COUNTER 的 值还是一开始的 0。似乎 COUNTER 的变化,对于我们的 ChangeListener 彻底“隐身”了。

Incrementing COUNTER to : 1 
Incrementing COUNTER to : 2 
Incrementing COUNTER to : 3 
Incrementing COUNTER to : 4 
Incrementing COUNTER to : 5

1.3 代码示例三

这个有意思的小程序还没有结束,我们可以再对程序做一些小小的修改。我们不再让 ChangeListener 进行完全的忙等待,而是在 while 循环里面,小小地等待上 5 毫秒,看看 会发生什么情况。

static class ChangeListener extends Thread {
    
    

    @Override  
    public void run() {
    
       
        int threadValue = COUNTER;  
        while ( threadValue < 5){
    
         
            if( threadValue!= COUNTER){
    
       
                System.out.println("Sleep 5ms, Got Change for COUNTER : " + COUNTER + "");
                threadValue= COUNTER;       
             }        
             try {
    
               
                 Thread.sleep(5);    
             } catch (InterruptedException e) {
    
     
                 e.printStackTrace();
             }     
         }   
    } 
 }

又一个令人惊奇的现象要发生了。虽然我们的 COUNTER 变量,仍然没有设置 volatile 这个关键字,但是我们的 ChangeListener 似乎“睡醒了”。在通过 Thread.sleep(5) 在每个循环里“睡上“5 毫秒之后, ChangeListener 又能够正常取到 COUNTER 的值了。

Incrementing COUNTER to : 1 
Sleep 5ms, Got Change for COUNTER : 1 
Incrementing COUNTER to : 2 
Sleep 5ms, Got Change for COUNTER : 2 
Incrementing COUNTER to : 3 
Sleep 5ms, Got Change for COUNTER : 3 
Incrementing COUNTER to : 4 
Sleep 5ms, Got Change for COUNTER : 4 
Incrementing COUNTER to : 5 
Sleep 5ms, Got Change for COUNTER : 5

2.volatile作用解释

2.1 volatile原理

这些有意思的现象,其实来自于我们的 Java 内存模型以及关键字 volatile 的含义。那 volatile 关键字究竟代表什么含义呢?它会确保我们对于这个变量的读取和写入,都一定会 同步到主内存里,而不是从 Cache 里面读取。该怎么理解这个解释呢?我们通过刚才的例子来进行分析。

  • 刚刚第一个使用了 volatile 关键字的例子里,因为所有数据的读和写都来自主内存。那么自然地,我们的 ChangeMaker 和 ChangeListener 之间,看到的 COUNTER 值就是一样的。
  • 到了第二段进行小小修改的时候,我们去掉了 volatile 关键字。这个时候, ChangeListener 又是一个忙等待的循环,它尝试不停地获取 COUNTER 的值,这样就会从当前线程的“Cache”里面获取。于是,这个线程就没有时间从主内存里面同步更新后的 COUNTER 值。这样,它就一直卡死在 COUNTER=0 的死循环上了。
  • 而到了我们再次修改的第三段代码里面,虽然还是没有使用 volatile 关键字,但是短短 5ms 的 Thead.Sleep 给了这个线程喘息之机。既然这个线程没有这么忙了,它也就有机会把新的数据从主内存同步到自己的高速缓存里面了。于是,ChangeListener 在下一次查 看 COUNTER 值的时候,就能看到 ChangeMaker 造成的变化了。

虽然 Java 内存模型是一个隔离了硬件实现的虚拟机内的抽象模型,但是它给了我们一个很好的“缓存同步”问题的示例。也就是说,如果我们的数据,在不同的线程或者 CPU 核里面去更新,因为不同的线程或 CPU 核有着自己各自的缓存,很有可能在 A 线程的更新,到 B 线程里面是看不见的。

2.2 延伸至CPU高速缓存

事实上,我们可以把 Java 内存模型和计算机组成里的 CPU 结构对照起来看。

我们现在用的 Intel CPU,通常都是多核的的。每一个 CPU 核里面,都有独立属于自己的 L1、L2 的 Cache,然后再有多个 CPU 核共用的 L3 的 Cache、主内存。

因为 CPU Cache 的访问速度要比主内存快很多,而在 CPU Cache 里面,L1/L2 的 Cache 也要比 L3 的 Cache 快。所以,CPU 始终都是尽可能地从 CPU Cache 中去获取数据,而不是每一次都要从主内存里面去读取数据。

在这里插入图片描述

这个层级结构,就好像我们在 Java 内存模型里面,每一个线程都有属于自己的线程栈。线程在读取 COUNTER 的数据的时候,其实是从本地的线程栈的 Cache 副本里面读取数据, 而不是从主内存里面读取数据。

3.volatile实现原理:内存屏障

volatile实际上通过内存屏障同时实现了可见性有序性

  • 作用

    • 阻止屏障两侧指令重排序
    • 强制把写修改的数据写回主存–>其余缓存中相应的数据失效–>别的线程再读时需要到主存中重新读取
  • 分类

  • Store:更新主存

  • Load:让高速缓存失效,强行刷新

    • LoadLoad:volatile读之后,避免volatile读操作和后面普通的读操作进行重排序
    • StoreStore:volatile写之前,禁止上面的普通写与后面的volatile写重排序
  • LoadStore:volatile读之后,避免volatile读操作和后面普通的写操作进行重排序
  • StoreLoad:volatile写之后,避免volatile写操作与后面可能存在的volatile读写操作发生重排序
    在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_43935927/article/details/108630814