天天看點

[Java 并發程式設計實戰] 對 volatile 變量進行執行個體驗證(内含源碼)volatile 之可見性驗證volatile 之重排序問題說明volatile 之非原子性問題驗證參考

「 天行健,君子以自強不息。地勢坤,君子以厚德載物。」———《易經》

volatile 變量,在上一篇文章中已經有簡單提及相關概念和用法,這一篇主要對 Volatile 變量的特性進行源碼驗證。驗證它的涉及到的三個特性:

  • 可見性
  • 指令重排序
  • 非原子性

volatile 之可見性驗證

上一篇文章中,講到 volatile 變量通常被當做狀态标記使用。其中典型的應用是,檢查标記狀态,以确定是否退出循環。下面我們直接舉個反例,源碼如下:

public class Volatile {

        boolean ready=true;  //volatile 狀态标志變量

        private final static int SIZE = ; //建立10個對象,可改變

        public static void main(String[] args) throws InterruptedException{

            Volatile vs[]=new Volatile[SIZE];

            for(int n=;n<SIZE;n++)
                (vs[n]=new Volatile()).test(); 

            System.out.println("mainThread end");//調用結束列印,死循環時不列印
        }

        public void test() throws InterruptedException{
            Thread t2=new Thread(){
                public void run(){
                    while(ready);//變量為true時,讓其死循環
                }
            };
            Thread t1=new Thread(){
                public void run(){
                    ready=false;
                }
            };
            t2.start();
            Thread.yield();
            t1.start();
            t1.join();//保證一次隻運作一個測試,以此減少其它線程的排程對 t2對boolValue的響應時間 的影響
            t2.join();
        }
    }
           

其中,ready 變量是我們要驗證的 volatile 變量。一開始 ready 初始化為 true,其次啟動 t2 線程讓其進入死循環;接着,t1 線程啟動,并且讓 t1 線程先執行,将 ready 改為 false。理論上來講,此時 t2 線程應該跳出死循環,但是實際上并沒有。此時 t2 線程讀到的 ready 的值仍然為 true。是以這段程式一直沒有列印出結果。這便是多線程間的不可見性問題,官方話術為: 線程 t1 修改後的值對線程 t2 來說并不可見。下圖可以看到程式一直處于運作狀态:

[Java 并發程式設計實戰] 對 volatile 變量進行執行個體驗證(内含源碼)volatile 之可見性驗證volatile 之重排序問題說明volatile 之非原子性問題驗證參考

解決辦法是:對變量 ready 聲明為 volatile,再次執行者段程式,能夠順利列印出 “mainTread end”。volatile 保證了變量 ready 的可見性。

[Java 并發程式設計實戰] 對 volatile 變量進行執行個體驗證(内含源碼)volatile 之可見性驗證volatile 之重排序問題說明volatile 之非原子性問題驗證參考

另外補充說明我這個例子用的 Java 版本:

[Java 并發程式設計實戰] 對 volatile 變量進行執行個體驗證(内含源碼)volatile 之可見性驗證volatile 之重排序問題說明volatile 之非原子性問題驗證參考

volatile 之重排序問題說明

有序性:表示程式的執行順序按照代碼的先後順序執行。通過下面代碼,我們将更加直覺的了解有序性。

int a = ;
int b = ;
a = ;     //語句A
b = ;     //語句B
           

上面代碼,語句 A 一定在語句 B 之前執行嗎? 答案是否定的。因為這裡可能發生指令重排序。語句 B 可能先于語句 A 先自行。

什麼是指令重排序?處理器為了提高運作效率,可能對輸入代碼進行優化,他不保證程式中各個語句的執行先後順序同代碼中的順序一緻,但是他會保證程式最終執行的結果和代碼順序執行的結果是一緻的。

但是下面這種情況,語句 B 一定在 語句 A 之後執行。

int a = ;
int b = ;
a = ;     //語句A
b = a + ;     //語句B
           

原因是,變量 b 依賴 a 的值,重排序時處理器會考慮指令之間的依賴性。

當然,這個 volatile 有什麼關系呢?

volatile 變量可以一定程度上保證有序性,volatile 關鍵字禁止指令重排序。

//x、y為非volatile變量
//flag為volatile變量

x = ;        //語句1
y = ;        //語句2
flag = true;  //語句3

x = ;         //語句4
y = ;       //語句5
           

這裡要說明的是,flag 為 volatile 變量;能保證

  1. 語句1,語句2 一定是在語句3的前面執行,但不保證語句1,語句2的執行順序。
  2. 語句4,語句5 一定是在語句3的後面執行,但不保證語句4,語句5的執行順序。
  3. 語句1,語句2 的執行結果,對語句3,語句4,語句5是可見的。

以上,就是關于 volatile 的禁止重排序的說明。、

volatile 之非原子性問題驗證

volatile 關鍵字并不能保證原子性,如自增操作。下面看一個例子:

public class Volatile{

    private volatile int count = ;

    public static void main(String[] args) {

        final Volatile v = new Volatile();

        for(int i = ; i < ; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    v.count++;
                }

            }).start();
        }

        while(Thread.activeCount() > )
            Thread.yield();

        System.out.println(v.count);
    }

}
           

這個程式執行的結果并沒有達到我們的期望值,1000。并且每次的運作結果可能都不一樣,如下圖,有可能是 997 等。

[Java 并發程式設計實戰] 對 volatile 變量進行執行個體驗證(内含源碼)volatile 之可見性驗證volatile 之重排序問題說明volatile 之非原子性問題驗證參考

來看下面一副圖,分解自增操作的步驟。

  1. read&load 從主記憶體複制變量到目前工作記憶體。
  2. use&assign 執行代碼,改變共享變量的值。
  3. store&write 用工作記憶體資料重新整理主記憶體相關内容。
[Java 并發程式設計實戰] 對 volatile 變量進行執行個體驗證(内含源碼)volatile 之可見性驗證volatile 之重排序問題說明volatile 之非原子性問題驗證參考

但是,這一系列的操作并不是原子的。也就是在 read&load 之後,如果主記憶體 count 發生變化,線程工作記憶體中的值由于已經加載,不會産生對應的變化。是以計算出來的結果和我們預期不一樣。

對于 volatile 修飾的變量,jvm 虛拟機隻是保證從主記憶體加載到線程工作記憶體中的值是最新的。

是以,假如線程 A 和 B 在read&load 過程中,發現主記憶體中的值都是5,那麼都會加載這個最新的值 5。線程 A 修改後寫到主記憶體,更新主記憶體的值為6。線程 B 由于已經 read & load,注意到此時線程 B 工作記憶體中的值還是5, 是以修改後也會将6更新到主記憶體。

那麼兩個線程分别進行一次自增操作後,count 隻增加了1,結果也就錯了。

當然,我們可以通過并發安全類AomicInteger, 内置鎖 sychronized,顯示鎖 ReentrantLock,來規避這個問題,讓程式運作結果達到我們的期望值 1000.

1)采用并發安全類 AomicInteger 的方式:

import java.util.concurrent.atomic.AtomicInteger;

public class Volatile{

    private AtomicInteger  count = new AtomicInteger();

    public static void main(String[] args) {

        final Volatile v = new Volatile();

        for(int i = ; i < ; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    v.count.incrementAndGet();
                }

            }).start();
        }

        while(Thread.activeCount() > )
            Thread.yield();

        System.out.println(v.count);
    }

}
           

2) 采用内置鎖 synchronized 的方式:

public class Volatile{

    private int count = ;

    public static void main(String[] args) {

        final Volatile v = new Volatile();

        for(int i = ; i < ; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                    synchronized  (this) {
                        v.count++;
                    }
                }

            }).start();
        }

        while(Thread.activeCount() > )
            Thread.yield();

        System.out.println(v.count);
    }

}
           

3)采用顯示鎖的方式

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Volatile{

    private int count = ;

    Lock lock = new ReentrantLock();

    public static void main(String[] args) {

        final Volatile v = new Volatile();

        for(int i = ; i < ; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    // TODO Auto-generated method stub
                        v.lock.lock();
                        try {
                            v.count++;
                        }finally {
                            v.lock.unlock();
                        }
                }

            }).start();
        }

        while(Thread.activeCount() > )
            Thread.yield();

        System.out.println(v.count);
    }

}
           

參考

http://www.cnblogs.com/dolphin0520/p/3920373.html

https://blog.csdn.net/gao_chun/article/details/45095995

https://blog.csdn.net/xilove102/article/details/52437581

本文原創首發于微信公衆号 [ 林裡少年 ],歡迎關注第一時間擷取更新。
[Java 并發程式設計實戰] 對 volatile 變量進行執行個體驗證(内含源碼)volatile 之可見性驗證volatile 之重排序問題說明volatile 之非原子性問題驗證參考

繼續閱讀