1. 程式人生 > >多執行緒程式設計:偽共享以及其解決方案

多執行緒程式設計:偽共享以及其解決方案

首先本文是根據多篇部落格的整合而來,依照本人的理解所寫

1.基本概念的瞭解

回到正題,建議先從下面的部落格連結看起以便對下列概念有個基本的瞭解:

1)CPU快取
2)MESI協議以及RFO請求
3)快取行

具體部落格連結:https://www.cnblogs.com/cyfonly/p/5800758.html

當然如果你不想讀冗長的文章,我這裡對上面的概念做一個簡單的解釋:

1.1 CPU快取

首先CPU的快取結構是下面這樣的:

假如CPU要計算一個數據,如果在自身的暫存器上找不到,那麼它就會從L1,L2,L3,記憶體這樣去找,其依次耗費的時間如下:

在這裡插入圖片描述

1.2 MESI協議

假如某個資料在CPU核心1號上面有,但是CPU核心2號也想訪問,那麼該如何做?

一般來說,該由2號直接從1號去拿便是,但是麻煩

在這裡插入圖片描述
但是更好的方法是讓1號直接把資料的副本發一份給2號即可:
在這裡插入圖片描述
但是隻是簡單的發過去是不行的,

假如該資料是隻讀的,如何弄?

或者假如該資料是可以寫的,但是隻準一個執行緒寫,其他執行緒不能寫又該如何約束?

所以就有了MESI協議:
在這裡插入圖片描述
四種具體狀態的轉換如下:
在這裡插入圖片描述

其中造成偽共享最重要的原因就是遠端寫

在這裡插入圖片描述

簡單的來說就是:假如核1和核2同時要對 資料A 做寫操作,那麼核1和核2都會向對方傳送RFO(Request For Owner)的請求,阻止對方不要寫,但為了排程,那麼核1 和 核2 的行為是彼此間隔的,核1不動 核 2 寫 , 核2寫完後,核1再寫,迴圈…

這樣做的話效率就非常非常低了

但是下一個問題就是核1和核2是如何對資料進行界定的?這個也是為什麼會產生偽共享的關鍵

這裡引用cyfonly關於快取行的描述:

快取系統中是以快取行(cache line)為單位儲存的。快取行通常是 64 位元組(譯註:本文基於 64 位元組,其他長度的如 32 位元組等不適本文討論的重點),並且它有效地引用主記憶體中的一塊地址。一個 Java 的 long 型別是 8 位元組,因此在一個快取行中可以存 8 個 long 型別的變數。所以,如果你訪問一個 long 陣列,當陣列中的一個值被載入到快取中,它會額外載入另外 7 個,以致你能非常快地遍歷這個陣列。事實上,你可以非常快速的遍歷在連續的記憶體塊中分配的任意資料結構。而如果你在資料結構中的項在記憶體中不是彼此相鄰的(如連結串列),你將得不到免費快取載入所帶來的優勢,並且在這些資料結構中的每一個項都可能會出現快取未命中。

簡單的來說:CPU讀取 快取 是以 快取行的形式去讀取的

快取行 是由64個位元組組成,不僅是陣列資料,還是其他結構的資料也是這樣儲存的,對,沒錯,連續儲存的,這也是快取快的原因!

所以問題就來了:

假如有4個執行緒去修改下面的longs陣列中的元素會如何?

假如執行緒1修改longs[0],執行緒2修改longs[1],以此類推


	private static VolatileLong[] longs = new VolatileLong[4];

    public final static class VolatileLong {
        public volatile long value = 0L;
	}
  

但是後面會出問題,因為

由於快取行是連續的,long[0],long[1]…等是在一個快取行中的!

所以執行緒在對各自要操作的元素進行寫操作的時候,假如有四個核,各自執行對應執行緒的寫操作,讀到了連續的快取行,由於要操作的元素在其中,那麼必然避免不了彼此發RFO(Request For Owner)請求!

而這就是典型的偽共享問題!

在這裡插入圖片描述

如何避免快取行連續帶來的偽共享問題?

簡單的來說,核1如果要操作longs[0],核2要操作longs[1],

2.1 解決對策:padding

那麼就必然要做到彼此要寫的內容不能在同一個快取行上!

那麼常見的方法就是padding(填充),就是填充無用的物件,來避免被快取到同一個行上

比如上面的VolatileLong可以改為:

public final static class VolatileLong {
        volatile long p0, p1, p2, p3, p4, p5, p6;
        public volatile long value = 0L;
        volatile long q0, q1, q2, q3, q4, q5, q6;
}

這樣就算是N個執行緒操作VolatileLong 的陣列的不同元素也不會再發生類似的問題了

2.2 Java8 的Contended

但是如果每個類都需要這樣寫就很麻煩,

所以這裡引入了Java8的Contended註解來解決這個問題:

    @sun.misc.Contended
    public final static class VolatileLong3 {
        public volatile long value = 0L;
    }

附:引入@sun.misc.Contended報錯

如果你用的是STS或者Ecplise,可能會有類似的問題,點選專案,remove掉Ecplise自帶的JDK,換上自己安裝的JDK8即可

具體參考:https://blog.csdn.net/u014471160/article/details/78523440

想要做參照實驗的,可以參考:該作者有較為詳細的介紹 https://www.jianshu.com/p/c3c108c3dcfd

想明白後面具體的原理,可以參考這篇專家文章,雖然是引用的,但是內容的非常透徹,系統和完整,並且介紹了一個非常牛逼的多執行緒框架Disruptor:
https://blog.csdn.net/qq_27680317/article/details/78486220?locationNum=5&fps=1