Photo By Instagram sooyaaa
昨天的問題
往期問題中,我們介紹了 LongAdder 工具類。我們知道了 LongAdder 中爲了分散熱點數據,存在一個 volatile 修飾的 Cell 數組。由於數組的連續存儲特性,會存在僞共享問題,你知道 LongAdder 是如何解決的嗎?
我的答案
在探討 LongAdder 是如何解決僞共享問題之前,我們要先梳理清一個概念,什麼是 僞共享 和 共享 ?
共享在 Java 編程裏面我們可以這樣理解,有一個 Share 類,它有一個 value 的屬性。如下:
public class Share {
int value;
}
我們初始化 Share 的一個實例,然後啓動多個線程去操作它的 value 屬性,此時的 Share 變量被多個線程操作的這種情況我們稱之爲 共享。
大家都知道在不添加任何互斥措施的情況,多線程操作這個 Share 變量的 value 屬性肯定存在線程安全性的問題。那有什麼辦法可以解決這個問題呢?我們可以使用 volatile 和 CAS 技術來保證共享變量可以安全的被多個線程共享操作使用,不知道 volatile 和 CAS 技術點的同學可以參考往期文章 ReentranLock 實現原理居然是這樣?。
但是由於 volatile 的引入,會帶來一些問題。大家都知道 JMM(Java 內存模型)規範了 volatile 具有內存可見性和禁止指令重排序的語義。這倆條語義使得某個線程更新本地緩存中的 value 值後會將其他線程的本地緩存中的 value 值失效,然後其他線程再次讀取 value 值的時候需要去主存裏面獲取 value 值,這樣即保證了 value 的內存可見性。
當然啦,這沒有任何問題,但是由於線程本地緩存的操作是以緩存行爲單位的,一個緩存行大小通常爲 64B(不同型號的電腦緩存行大小會有不同)。因此一個緩存行中不會只單單存儲 value 一個變量,可能還會存儲其他變量。這樣當一個線程更新了 value 之後,如果其他線程本地緩存中同樣緩存了 value, value 所在的緩存行就會失效,這意味着該緩存行上的其他變量也會失效,那麼線程對這個該緩存行上所有變量的訪問都需要從主存中獲取。我們都知道 CPU 訪問主存的速度相對於訪問緩存的速度有着數量級的差距,這就帶了很大的性能問題,我們將這個問題稱之爲 僞共享。
理解了僞共享到底是什麼鬼以後,我們來看看 Java 大師們是怎麼解決這個問題的。在早期版本的 JDK 裏面你應該見到過類似如下的代碼:
public class Share {
volatile int value;
long p1, p2, p3, p4, p5, p6;
}
你可能猜到了,定義了幾個無用的變量作爲填充物,他們會保證一個緩存行裏面只保存了 Share 變量,這樣更新 Share 變量的時候就不會存在僞共享問題了。但是這種方法存在什麼問題呢?
首先基於每臺運行 Java 程序的機器的緩存行大小可能不同,其次由於這些類似填充物的變量並沒有被實際使用,可以被 JVM 優化掉,這樣就失效了。
基於此,在 Java 8 的時候,官方給出瞭解決策略,這就是 Contended 註解。依賴於這個註解,我們在 Java 8 環境下可以這樣改善代碼:
public class Share {
@Contended
volatile int value;
}
使用如上註解,並且在 JVM 啓動參數中加入 -XX:-RestrictContended,這樣 JVM 在運行時就會自動的爲我們的 Share 類添加合適大小的填充物(padding)來解決僞共享問題,而不需要我們手寫變量來作爲填充物了,這樣就更加便捷優雅的解決了僞共享問題。悄悄的告訴你,LongAdder 就是使用 Contended 來解決僞共享問題噠。
好了,相信你已經瞭解了什麼是僞共享問題,以及早期併發編程大師是如何解決僞共享問題的,最後我們也介紹了在 Java 8 中使用 Contended 來更優雅的解決僞共享問題。Contended 還提供了一個緩存行分組的功能,在上文中我們沒有介紹,歡迎有興趣的小夥伴們自行探索吧(嘿嘿)。
以上即爲昨天的問題的答案,小夥伴們對這個答案是否滿意呢?歡迎留言和我討論。
又要到年末了,你是不是又悄咪咪的開始看機會啦。爲了廣大小夥伴能充足電量,能順利通過 BAT 的面試官無情三連炮,我特意推出大型刷題節目。每天一道題目,第二天給答案,前一天給小夥伴們獨立思考的機會。
點下“在看”,鼓勵一下?