1. 程式人生 > >【JAVA】多執行緒之記憶體可見性

【JAVA】多執行緒之記憶體可見性

                                    多執行緒之記憶體可見性

一、什麼是可見性?

一個執行緒對共享變數值的修改,能夠及時地被其他執行緒所看到。

共享變數:如果一個變數在多個執行緒的工作記憶體中都存在副本,那麼這個變數就是這幾個執行緒的共享變數。

工作記憶體:每個執行緒擁有自己的工作記憶體,只能對自己工作記憶體中的變數副本進行修改,而不能直接修改主記憶體中的變數。

變數副本:主記憶體中變數的一份拷貝

二、主記憶體與工作記憶體之間的關係

注意:

(1)執行緒對共享變數的所有操作都必須在自己的工作記憶體中進行,不能繞過工作記憶體直接從主記憶體中讀寫變數

(2)不同執行緒之間無法直接訪問其他執行緒工作記憶體中的變數,執行緒之間變數值的傳遞需要通過主記憶體來完成

三、共享變數可見性實現的原理

執行緒1對共享變數的修改要想被執行緒2及時看到,必須要經過如下的2個步驟

(1)把工作記憶體1中更新過的共享變數重新整理到主記憶體中

(2)將主記憶體中最新的共享變數的值更新到工作記憶體2中

變數傳遞順序

四、可見性實現方式

(1)synchronized,能夠實現原子性(同步)與可見性

(2)volatile,能夠實現變數可見性,但不能保證變數的原子性

【1】synchronized實現可見性

JMM(java記憶體模型)中關於synchronized的兩條規定

  • 執行緒解鎖前,必須把共享變數的最新值重新整理到主記憶體中
  • 執行緒加鎖時,清空工作記憶體中的共享變數的值,從而使用共享變數時需要從主記憶體中讀取最新的值

那麼,執行緒執行互斥程式碼的流程就是:

(1)獲得互斥鎖

(2)清空工作記憶體

(3)從主記憶體中拷貝變數的最新副本到工作記憶體中

(4)執行互斥程式碼

(5)將更改後的共享變數的值重新整理到主記憶體中

(6)釋放互斥鎖

【2】volatile實現可見性

通過加入記憶體屏障和禁止指令重排序優化來實現的。每次讀取用volatile修飾的變數的值,都會從主記憶體中讀取該變數。

通俗地講:volatile變數在每次被執行緒訪問時,都強迫從主記憶體中重讀該變數的值,而當該變數發生變化時,又會強迫執行緒將最新的值重新整理到主記憶體。這樣在任何時刻,不同的執行緒總能看到該變數的最新值。

那麼,執行緒寫volatile變數的過程:

(1)改變執行緒工作記憶體中volatile變數副本的值

(2)將改變後的副本的值從工作記憶體重新整理到主記憶體

執行緒讀volatile變數的值的過程:

(1)從主記憶體中讀取volatile變數的最新值到執行緒的工作記憶體中

(2)從工作記憶體中讀取volatile變數的副本

【3】volatile不能保證變數複合操作的原子性

例如:以下操作不是原子操作

private int number=0;
number++;

可以分解成如下操作:

(1)讀取number的值

(2)將number的值加1

(3)寫入最新的number的值

假如我們使得volatile int  i=0;並且大量執行緒呼叫i的自增操作,那麼volatile可以保證變數的安全嗎?

不可以保證,volatile不能保證變數操作的原子性,自增操作包括三個步驟,分別是讀取,加一,寫入,由於這三個子操作的原子性不能被保證,那麼n個執行緒總共呼叫n次i++的操作後,最後的i的值並不是大家想的n,而是一個比n小的數。

解釋:比如A執行緒執行自增操作,剛讀取到i的初始值0,然後就被阻塞了。B執行緒現在開始執行,還是讀取到i的初始值0,執行自增操作,此時i的值為1。然後A執行緒阻塞結束,對剛才拿到的0執行加一與寫入操作,執行成功後,i的值被寫成1了,我們預期輸出2,可是輸出的是1,輸出比預期小。

程式碼驗證:

package day0829;
 
import java.util.ArrayList;
import java.util.List;
 
public class VolatileTest {
    public volatile int i = 0;
 
    public void increase() {
        i++;
    }
 
    public static void main(String args[]) throws InterruptedException {
        List<Thread> threadList = new ArrayList<>();
        VolatileTest test = new VolatileTest();
        for (int j = 0; j < 10000; j++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    test.increase();
                }
            });
            thread.start();
            threadList.add(thread);
        }
 
        //等待所有執行緒執行完畢
        for (Thread thread : threadList) {
            thread.join();
        }
        System.out.print(test.i);
    }
}

此時輸出為:

那麼如何保證i自增操作的原子性呢?

(1)使用synchronized關鍵字

(2)使用ReentranLock

(3)使用AtomicInteger

相關問題:

(1)即使沒有保證可見性的措施,很多時候共享變數依然能夠在主記憶體和工作記憶體中得到及時的更新?

一般只有在短時間內高併發的情況下才會出現變數得不到及時更新的情況,因為cpu在執行時會很快地重新整理快取,所以一般情況下很難看到這種不可見的問題。

五、synchronized和volatile的區別

六、多執行緒中其他知識點

指令重排序

程式碼書寫的順序與程式實際執行的順序不同,指令重排序是編譯器或處理器為了提高效能而做的優化。

as-if-serial語義

無論如何進行重排序,程式執行的結果與程式碼原本順序執行的結果一致(java會保證在單執行緒下遵循此語義)

重排序不會給單執行緒帶來記憶體可見性問題,但在多執行緒中,程式交錯執行,重排序可能會造成記憶體可見性問題