并發(fā)編程(二):三大特性之可見性

一、volatile簡(jiǎn)介

Java語(yǔ)言規(guī)范第三版中對(duì)volatile的定義如下:

java編程語(yǔ)言允許線程訪問(wèn)共享變量,為了確保共享變量能被準(zhǔn)確和一致的更新,線程應(yīng)該確保通過(guò)排他鎖單獨(dú)獲得這個(gè)變量。Java語(yǔ)言提供了volatile,在某些情況下比鎖更加方便。如果一個(gè)字段被聲明成volatile,java線程內(nèi)存模型確保所有線程看到這個(gè)變量的值是一致的。

術(shù)語(yǔ)定義

術(shù)語(yǔ) 英文單詞 描述
共享變量 在多個(gè)線程之間能夠被共享的變量被稱為共享變量。共享變量包括所有的實(shí)例變量,靜態(tài)變量和數(shù)組元素。他們都被存放在堆內(nèi)存中,Volatile只作用于共享變量。
內(nèi)存屏障 Memory Barriers 是一組處理器指令,用于實(shí)現(xiàn)對(duì)內(nèi)存操作的順序限制。
緩沖行 Cache line 緩存中可以分配的最小存儲(chǔ)單位。處理器填寫緩存線時(shí)會(huì)加載整個(gè)緩存線,需要使用多個(gè)主內(nèi)存讀周期。
原子操作 Atomic operations 不可中斷的一個(gè)或一系列操作。
緩存行填充 cache line fill 當(dāng)處理器識(shí)別到從內(nèi)存中讀取操作數(shù)是可緩存的,處理器讀取整個(gè)緩存行到適當(dāng)?shù)木彺妫↙1,L2,L3的或所有)
緩存命中 cache hit 如果進(jìn)行高速緩存行填充操作的內(nèi)存位置仍然是下次處理器訪問(wèn)的地址時(shí),處理器從緩存中讀取操作數(shù),而不是從內(nèi)存。
寫命中 write hit 當(dāng)處理器將操作數(shù)寫回到一個(gè)內(nèi)存緩存的區(qū)域時(shí),它首先會(huì)檢查這個(gè)緩存的內(nèi)存地址是否在緩存行中,如果存在一個(gè)有效的緩存行,則處理器將這個(gè)操作數(shù)寫回到緩存,而不是寫回到內(nèi)存,這個(gè)操作被稱為寫命中。
寫缺失 write misses the cache 一個(gè)有效的緩存行被寫入到不存在的內(nèi)存區(qū)域。

實(shí)現(xiàn)原理

有volatile變量修飾的共享變量進(jìn)行寫操作的時(shí)候會(huì)將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到系統(tǒng)內(nèi)存中,這個(gè)寫回內(nèi)存的操作會(huì)引起其它CPU里緩存的該內(nèi)存地址的數(shù)據(jù)無(wú)效。具體實(shí)現(xiàn)細(xì)節(jié)參考這篇文章聊聊并發(fā)(一)深入分析Volatile的實(shí)現(xiàn)原理。

二、適用場(chǎng)景

volatile具有sychronized的可見性和有序性,但是不具備原子性。volatile的可見性確保了所有線程看到volatile聲明的共享變量值是最新的。另外在訪問(wèn)volatile聲明的變量時(shí)線程不會(huì)加鎖,也就不會(huì)引起線程的阻塞,這就使得相對(duì)于sychronized來(lái)說(shuō)它只是輕量級(jí)的同步機(jī)制。在某些情況下,如果讀操作遠(yuǎn)遠(yuǎn)大于寫操作時(shí),使用volatile能夠在性能上優(yōu)于鎖。舉個(gè)volatile不具備原子性的例子:

2.1 volatile非原子性代碼示例(不要這樣做)

package com.game.lll.syn;  
  
/** 
 * Volatile不具備原子性 
 * @author liulongling 
 * 
 */  
class VolatileExample3{  
  
    private volatile int count;  
      
    public void inc()  
    {  
        count++;  
    }  
  
  
    public static void main(String[] args) {  
        final VolatileExample3 test = new VolatileExample3();  
        for(int i=0;i<100000;i++){  
            new Thread(){  
                public void run() {  
                    test.inc();  
                };  
            }.start();  
        }  
  
        while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完  
            Thread.yield();  
        System.out.println("count:"+test.count);  
    }  
}  

控制臺(tái)輸出:

count:99994

Volatile不具備原子性:在代碼第六行雖然使用了volatile來(lái)修飾count,但是從結(jié)果可以看出count++的次數(shù)等于99994,而我們預(yù)期的結(jié)果是100000.所以volatile不足以使自增count++原子化,除非你能保證只有一個(gè)線程對(duì)變量執(zhí)行寫操作。在上一篇文章中:原子性(一) 我對(duì)出現(xiàn)這種結(jié)果做過(guò)原因分析。

2.2 模式一:狀態(tài)標(biāo)志

package com.game.lll.syn;  
  
class VolatileExample2{  
    private volatile boolean isShuttingDown = false;   // 標(biāo)志服務(wù)是否正在關(guān)閉  
      
    public boolean isShuttingDown() {  
        return isShuttingDown;  
    }  
  
    public void userLogin()  
    {  
        if(isShuttingDown)  
        {  
            // 正在關(guān)服,拒絕登陸  
            return;  
        }  
        if(!isShuttingDown)  
        {  
            shutdown();  
        }  
        System.out.println(Thread.currentThread()+"玩家登錄");  
    }  
    /** 
     * 關(guān)閉服務(wù)(斷開所有與玩家的連接并且不接受新玩家連接,保存數(shù)據(jù),關(guān)閉程序) 
     */  
    private void shutdown() {  
        // 停服處理  
        // 將在線玩家踢下線,并在關(guān)服過(guò)程中不接受新登陸需求  
        isShuttingDown = true;  
        System.out.println(Thread.currentThread()+"關(guān)閉服務(wù)器");  
    }  
  
    public static void main(String[] args) {  
        final VolatileExample2 test = new VolatileExample2();  
        for(int i=0;i<100000;i++){  
            new Thread(){  
                public void run() {  
                    test.userLogin();  
                };  
            }.start();  
        }  
  
        while(Thread.activeCount()>1)  //保證前面的線程都執(zhí)行完  
            Thread.yield();  
    }  
}  

控制臺(tái)輸出:

Thread[Thread-0,5,main]關(guān)閉服務(wù)器
Thread[Thread-0,5,main]玩家登錄

在游戲停服處理時(shí),我們會(huì)聲明一個(gè)volatile修飾的bool變量。在上面例子已經(jīng)證明volatile不具有原子性,那么在運(yùn)行過(guò)程中可能會(huì)出現(xiàn)當(dāng)線程一在執(zhí)行userLogin()過(guò)程中,線程二調(diào)用了shutdown()將isShuttingDown布爾變量設(shè)置為true。這時(shí)如果使用volatile ,線程二會(huì)馬上通知線程一isShuttingDown的狀態(tài)發(fā)生改變。

2.3 模式二:開銷較低的“讀-寫鎖” 策略

public synchronized void inc()  
{  
    count++;  
}  

控制臺(tái)輸出:

count:100000

在讀操作多余寫操作時(shí),我們可以使用synchronized來(lái)保證inc的操作是原子的,并使用volatile保證當(dāng)前結(jié)果的可見性。這樣的策略不用每次在讀取count變量時(shí)只允許一個(gè)線程訪問(wèn)它,使得在性能上支持多線程的volatile性能更好一些。

另外volatile還適應(yīng)以下三個(gè)場(chǎng)景,詳細(xì)參考文章:http://blog.csdn.net/vking_wang/article/details/9982709#t5

只有滿足下面所有的標(biāo)準(zhǔn)后,你才能使用volatile變量:

  • 寫入變量時(shí)并不依賴變量的當(dāng)前值;或者能夠確保只有單一的線程改變變量的值。
  • 變量不需要與其他的狀態(tài)變量共同參與不變約束
  • 訪問(wèn)變量時(shí),沒(méi)有其它的原因需要加鎖

作者:小毛驢,一個(gè)Java游戲服務(wù)器開發(fā)者 原文地址:https://liulongling.github.io/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容