【Java并发编程】volatile(一):三个示例说明 volatile 作用

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

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

事实上,这两种理解都是完全错误的。下面我们通过三个连续的小 demo 来看看 volatile 到底有什么用。

简单来说,使用 volatile,将会强制我们对成员变量的读/写都直接与内存

将会强制所有线程都去堆内存中读取成员变量最新的值,而不是读取自己线程工作空间的缓存。

注:volatile 并不能保证多个线程共同修改成员变量时所带来的不一致问题,也就是说 volatile 不能替代 synchronized

volatile 作用示例

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

1.代码示例一

public class VolatileTest {  

	// volatile修饰
    private static volatile int COUNTER = 0; 
 
    public static void main(String[] args) {  
    	// 启动ChangeListener线程  
        new ChangeListener().start();  
        // 启动ChangeMaker线程
        new ChangeMaker().start(); 
    }
 	
 	// ChangeListener 线程
    // 监听COUNTER变量,当COUNTER发生变化时就将变化的值打印出来
    static class ChangeListener extends Thread {
        @Override     
        public void run() {   
        	// threadValue,用于对比 COUNTER 是否发生变化
        	// 将 COUNTER 的值赋给 threadValue 变量
            int threadValue = COUNTER;  
            while (threadValue < 5){         
            	// 当 COUNTER 发生变化,打印
                if( threadValue!= COUNTER){     
                    System.out.println("Got Change for COUNTER : " + COUNTER + ""); 
                    threadValue= COUNTER;            
                }     
            }    
        }  
    }
    
   // ChangeMaker 线程
   // 监听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++
               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

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

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

=> 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 线程里面是看不见的。

猜你喜欢

转载自blog.csdn.net/qq_33762302/article/details/114297535
今日推荐