天天看點

Java并發程式設計的藝術(三)——volatile1. 并發程式設計的兩個關鍵問題 3. Java多線程的記憶體模型(簡化版) 4. volatile是什麼? 5. volatile的使用 6. volatile的特性

版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。 https://blog.csdn.net/qq_34173549/article/details/79612458

1. 并發程式設計的兩個關鍵問題

并發是讓多個線程同時執行,若線程之間是獨立的,那并發實作起來很簡單,各自執行各自的就行;但往往多條線程之間需要共享資料,此時在并發程式設計過程中就不可避免要考慮兩個問題:通信 與 同步。

  • 通信 

    通信是指消息在兩條線程之間傳遞。 

    既然要傳遞消息,那接收線程 和 發送線程之間必須要有個先後關系,此時就需要用到同步。通信和同步是相輔相成的。

  • 同步 

    同步是指,控制多條線程之間的執行次序。

2. 通信的方式

2.1 通信方式的種類

線程之間的通信一共有兩種方式:共享記憶體 和 消息傳遞。

  • 共享記憶體 

    共享記憶體指的是多條線程共享同一片記憶體,發送者将消息寫入記憶體,接收者從記憶體中讀取消息,進而實作了消息的傳遞。 

    但這種方式有個弊端,即需要程式員來控制線程的同步,即線程的執行次序。

這種方式并沒有真正地實作消息傳遞,隻是從結果上來看就像是将消息從一條線程傳遞到了另一條線程。

  • 消息傳遞 

    顧名思義,消息傳遞指的是發送線程直接将消息傳遞給接收線程。 

    由于執行次序由并發機制完成,是以不需要程式員添加額外的同步機制,但需要聲明消息發送和接收的代碼。

綜上所述:對于共享記憶體的通信方式,需要進行顯示的同步,隐式的通信; 

而對于消息傳遞的通信方式,需要隐式的同步,顯示的通信。

2.2 Java使用的通信方式

Java使用共享記憶體的方式實作多線程之間的消息傳遞。是以,程式員需要寫額外的代碼用于線程之間的同步。

PS:其實共享記憶體的方式從實作過程來看,跟消息傳遞一點關系都沒有:一條線程将消息存入共享記憶體,另一條線程從共享記憶體中讀這條消息。 

但從結果來看,整個過程就好像是一條消息被從線程A傳遞到了線程B。 

這種方式之是以能實作消息傳遞,依托于兩點:

  • 必須有一片共享的記憶體
  • 必須要實作多線程的同步

3. Java多線程的記憶體模型(簡化版)

所有線程都共享一片記憶體,用于存儲共享變量; 

此外,每條線程都有各自的存儲空間,存儲各自的局部變量、方法參數、異常對象。

4. volatile是什麼?

Java采用共享記憶體的方式實作消息傳遞,而共享記憶體需要依托于同步。Java提供了synchronized、volatile關鍵字實作同步。此外volatile關鍵字還擁有一些額外的功能。

5. volatile的使用

在成員變量前加上該關鍵字即可。

public volatile boolean flag;           
  • 1

6. volatile的特性

6.1 重排序

重排序是計算機為了提高程式執行效率而對代碼的執行順序進行調整。你以為代碼是一行行順序執行的,但實際并非如此,重排序詳解請移步至:

Java并發程式設計的藝術(二)——重排序

若兩行指令之間沒有依賴關系,那麼計算機可以對他們的順序進行重排序,但若兩行之間的某個變量被volatile修飾後,重排序規則會發生變化。

在以下情況下,即使兩行代碼之間沒有依賴關系,也不會發生重排序:

  • volatile讀
    • 若volatile讀操作的前一行為volatile讀/寫,則這兩行不會發生重排序
    • volatile讀操作和它後一行代碼都不會發生重排序
  • volatile寫
    • volatile寫操作和它前一行代碼都不會發生重排序;
    • 若volatile寫操作的後一行代碼為volatile讀/寫,則這兩行不會發生重排序。

6.2 可見性

什麼是記憶體可見性?

“記憶體可見性”指的是一條線程修改完一個共享變量後,另一個線程若通路這個變量将會通路到修改後的值。即:一條線程對共享變量的修改,對其他線程立即可見。

但如果未對共享變量采用同步機制,那麼共享變量的修改不會對其他線程立即可見。

為什麼會出現記憶體不可見的情況?

通過上文可知,在Java中每條線程都有各自獨立的存儲空間,此外還有一個所有線程共享的記憶體空間。 

當開啟線程時,系統會将共享記憶體中的所有共享變量拷貝一份到線程專屬的存儲空間中。接下來該線程在結束前的所有操作都是基于自己的存儲空間進行的。是以,若一條線程改變了一個共享變量,僅僅改變的是這條線程專屬存儲空間中的變量值;此時若其他線程通路這個變量,通路的仍然是先前從共享存儲空間讀出來的值。 

然而我們希望一條線程将某個共享變量修改後,其他線程能立即通路到這個最新的值,而不是失效值。 

這時就需要同步機制來解決這個問題。

如何確定共享變量的可見性?

要確定所有共享變量對所有線程是可見的,就需要給所有共享變量使用同步。在Java中你可以選擇将共享變量用同步代碼塊包裹或用volatile修飾共享變量。

為什麼volatile能保證共享變量的記憶體可見性?

volatile修飾了一個成員變量後,這個變量的讀寫就會比普通變量多一些步驟。

  • volatile變量寫 

    當被volatile修飾的變量進行寫操作時,這個變量将會被直接寫入共享記憶體,而非線程的專屬存儲空間。

  • volatile變量讀 

    當讀取一個被volatile修飾的變量時,會直接從共享記憶體中讀,而非線程專屬的存儲空間中讀。

通過對volatile變量讀寫的限制,就能保證線程每次讀到的都是最新的值,進而確定了該變量的記憶體可見性。

volatile變量贈送的附加功能

進行volatile寫操作時,不僅會将volatile變量寫入共享記憶體,系統還會将目前線程專屬空間中的所有共享變量寫入共享記憶體。 

進行volatile讀操作時,系統也會一次性将共享記憶體中所有共享變量讀入線程專屬空間。 

這就意味着,如果普通變量在volatile寫操作之前被修改,那麼在volatile讀操作之後就能正确讀到他們。 

但是,在volatile寫操作之後被修改的普通變量 和 在volatile讀操作之前被通路的普通變量 都不具有記憶體可見性。

6.3 原子性

什麼是原子性?

原子性指的是一組操作必須一起完成,中途不能被中斷。

volatile能確定long、double讀寫的原子性

在Java中的所有類型中,有long、double類型比較特殊,他們占據8位元組(64比特),其餘類型都小于64比特。在32位作業系統中,CPU一次隻能讀取/寫入32位的資料,是以對于64位的long、double變量的讀寫會進行兩步。在多線程中,若一條線程隻寫入了long型變量的前32位,緊接着另一條線程讀取了這個隻有“一半”的變量,進而就讀到了一個錯誤的資料。 

為了避免這種情況,需要在用volatile修飾long、double型變量。

在記憶體可見性與原子性上,volatile就相當于是同步的setter和getter函數。但并不具有volatile的重排序規則,同步塊隻確定同步塊内部的指令不發生重排序,并不確定同步塊以外的指令的重排序。

PS1:Java中的byte竟然是位元組,bit才是比特(位)。 

PS2:char和short-2位元組、int和float-4位元組、long和double-8位元組、byte-1位元組

QA:在同步塊中調用wait函數是否會破壞原子性?