成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

一文澄清網上對 ConcurrentHashMap 的一個流傳甚廣的誤解!

開發 前端
其實 Java 8 對 CHM 進行了一番比較徹底的重構,讓它的性能大幅度得到了提升,比如棄用 segment 這種設計,改用對每個槽位做分段鎖,使用紅黑樹來降低查詢時的復雜度,擴容時多個線程可以一起參與擴容等等。

大家好,我是坤哥!

上周我在極客時間某個課程看到某個講師在討論 ConcurrentHashMap(以下簡稱 CHM)是強一致性還是弱一致性時,提到這么一段話。

這個解釋網上也是流傳甚廣,那么到底對不對呢,在回答這個問題之前,我們得想清楚兩個問題。

  • 什么是強一致性,什么是弱一致性
  • 上文提到 get 沒有加鎖,所以沒法即時獲取 put 的數據,也就意味著如果加鎖就可以立即獲取到 put 的值了?那么除了加鎖之外,還有其他辦法可以立即獲取到 put 的值嗎?

強一致性與弱一致性

強一致性

首先我們先來看第一個問題,什么是強一致性

一致性(Consistency)是指多副本(Replications)問題中的數據一致性。可以分為強一致性、弱一致性。

強一致性也被可以被稱做原子一致性(Atomic Consistency)或線性一致性(Linearizable Consistency),必須符合以下兩個要求

任何一次讀都能立即讀到某個數據的最近一次寫的數據

系統中的所有進程,看到的操作順序,都和全局時鐘下的順序一致

簡單地說就是假定對同一個數據集合,分別有兩個線程 A、B 進行操作,假定 A 首先進行了修改操作,那么從時序上在 A 這個操作之后發生的所有 B 的操作都應該能立即(或者說實時)看到 A 修改操作的結果。

弱一致性

與強一致性相對的就是弱一致性,即數據更新之后,如果立即訪問的話可能訪問不到或者只能訪問部分的數據。如果 A 線程更新數據后 B 線程經過一段時間后都能訪問到此數據,則稱這種情況為最終一致性,最終一致性也是弱一致性,只不過是弱一致性的一種特例而已。

那么在 Java 中產生弱一致性的原因有哪些呢,或者說有哪些方式可以保證強一致呢,這就得先了解兩個概念,可見性和有序性。

一致性的根因:可見性與有序性

可見性

首先我們需要了解一下 Java 中的內存模型

上圖是 JVM 中的 Java 內存模型,可以看到,它主要由兩部分組成,一部分是線程獨有的程序計數器,虛擬機棧,本地方法棧,這部分的數據由于是線程獨有的,所以不存在一致性問題(我們說的一致性問題往往指多線程間的數據一致性),一部分是線程共享的堆和方法區,我們重點看一下堆內存。

我們知道,線程執行是要占用 CPU 的,CPU 是從寄存器里取數據的,寄存器里沒有數據的話,就要從內存中取,而眾所周知這兩者的速度差異極大,可謂是一個天上一個地上,所以為了緩解這種矛盾,CPU 內置了三級緩存,每次線程執行需要數據時,就會把堆內存的數據以 cacheline(一般是 64 Byte) 的形式先加載到 CPU 的三級緩存中來,這樣之后取數據就可以直接從緩存中取從而極大地提升了 CPU 的執行效率(如下圖示)

但是這樣的話由于線程加載執行完數據后數據往往會緩存在 CPU 的寄存器中而不會馬上刷新到內存中,從而導致其他線程執行如果需要堆內存中共享數據的話取到的就不會是最新數據了,從而導致數據的不一致

舉個例子,以執行以下代碼為例

//線程1執行的代碼
int i = 0;
i = 10;

//線程2執行的代碼
j = i;

在線程 1 執行完后 i 的值為 10,然后 2 開始執行,此時 j 的值很可能還是 0,因為線程 1 執行時,會先把 i = 0 的值從內存中加載到 CPU 緩存中,然后給 i 賦值 10,此時的 10 是更新在 CPU 緩存中的,而未刷新到內存中,當線程 2 開始執行時,首先會將 i 的值從內存中(其值為 0)加載到 CPU 中來,故其值依然為 0,而不是 10,這就是典型的由于 CPU 緩存而導致的數據不一致現象。

那么怎么解決可見性導致的數據不一致呢,其實只要讓 CPU 修改共享變量時立即寫回到內存中,同時通過總線協議(比如 MESI)通過其他 CPU 所讀取的此數據所在 cacheline 無效以重新從內存中讀取此值即可。

有序性

除了可見性造成的數據不一致外,指令重排序也會造成數據不一致。

public class Reordering {

private static boolean flag;
private static int num;

public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) {
Thread.yield();
}

System.out.println(num);
}
}, "t1");
t1.start();
num = 5; ①
flag = true; ②
}
}

以上代碼執行步驟可能很多人認為是按正常的 ①,②,③ 執行的,但實際上很可能編譯器會將其調換一下位置,實際的執行順序可能是 ①③②,或 ②①③,也就是說 ①③ 是緊鄰的,為什么會這樣呢,因為執行 1 后,CPU 會把 x = 1 從內存加載到寄存器中,如果此時直接調用 ③ 執行,那么 CPU 就可以直接讀取 x 在寄存器中的值 1 進行計算,反之,如果先執行了語句 ②,那么有可能 x 在寄存器中的值被覆蓋掉從而導致執行 ③ 后又要重新從內存中加載 x 的值,有人可能會說這樣的指令重排序貌似也沒有多大問題呀,那么考慮如下代碼:

public class Reordering {

private static boolean flag;
private static int num;

public static void main(String[] args) {
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
while (!flag) {
Thread.yield();
}

System.out.println(num);
}
}, "t1");
t1.start();
num = 5; ①
flag = true; ②
}
}

以上代碼最終輸出的值正常情況下是 5,但如果上述 ① ,② 兩行指令發生重排序,那么結果是有可能為 0 的,從而導致我們觀察到的數據不一致的現象發生,所以顯然解決方案是避免指令重排序的發生,也就是保證指令按我們看到的代碼的順序有序執行,也就是我們常說的有序性,一般是通過在指令之間添加內存屏障來避免指令的重排序。

那么如何保證可見性與有序性呢?

相信大家都非常熟悉了,使用 volatile 可以保證可見性與有序性,只要在聲明屬性變量時添加上 volatile 就可以讓此變量實現強一致性,也就是說上述的 Reordering 類的 flag 只要聲明為 volatile,那么打印結果就永遠是 5!

好了,現在問題來了,CHM 到底是不是強一致性呢,首先我們以 Java 8 為例來看下它的設計結構(和之前的版本相差不大,主要加上了紅黑樹提升了查詢效率)。

來看下這個 table 數組和節點的聲明方式(以下定義 8 和 之前的版本中都是一樣的):

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
transient volatile Node<K,V>[] table;
...
}

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
...
}

可以看到 CHM 的 table 數組,Node 中的 值 val,下一個節點 next 都聲明為了 volatile,于是有學員就提出了一個疑問?

講師的回答也提到 CHM 為弱一致性的重要原因:即如果 table 中的某個槽位為空,此時某個線程執行了 key,value 的賦值操作,那么此槽位會新增一個 Node 節點,在 JDK 8 以前,CHM 是通過以下方式給槽位賦 Node 的。

V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
...
tab[index] = new HashEntry<K,V>(...);
...
unlock();
}

然后是通過以下方式來根據 key 來讀取 value 的。

V get(Object key, int hash) {
if (count != 0) { // read-volatile
HashEntry<K,V> e = getFirst(hash);
while (e != null) {
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
}

可以看到 put 時是直接給數組中的元素賦值的,而由于 get 沒有加鎖,所以無法保證線程 A put 的新元素對執行 get 的線程可見。

put 是有加鎖的,所以其實如果 get 也加鎖的話,那么毫無疑問 get 是可以立即拿到 put 的值的。為什么加鎖也可以呢,其實這是 JLS(Java Language Specification Java 語言規范) 規定的幾種情況,簡單地說就是支持 happens before 語義的可以保證數據的強一致性,在官網(https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html)中列出了幾種支持 Happens before 的情況,其中指出使用 volatile,synchronize,lock 是可以確保 happens before 語義的,也就是說使用這三者可以保證數據的強一致性,可能有人就問了,到底什么是 happens before 呢,其實本質是一種能確保線程及時刷新數據到內存,另一線程能實時從內存讀取最新數據以保證數據在線程之間保持一致性的一種機制,我們以 lock 為例來簡單解釋下:

public class LockDemo {
private int x = 0;

private void test() {
lock();
x++;
unlock();
}
}

如果線程 1 執行 test,由于拿到了鎖,所以首先會把數據(此例中為 x = 0)從內存中加載到 CPU 中執行,執行 x++ 后,x 在 CPU 中的值變為 1,然后解鎖,解鎖時會把 x = 1 的值立即刷新到內存中,這樣下一個線程再執行 test 方法時再次獲取相同的鎖時又從內存中獲取 x 的最新值(即 1),這就是我們通常說的對一個鎖的解鎖, happens-before 于隨后對這個鎖的加鎖,可以看到,通過這種方式可以保證數據的一致性。

至此我們明白了:在 Java 8 以前,CHM 的 get,put 確實是弱一致性的,可能有人會問為什么不對 get 加鎖呢,加上了鎖不就可以確保數據的一致性了嗎,可以是可以,但別忘了 CHM 是為高并發設計而生的,加了鎖不就導致并發性大幅度下降了么,那 CHM 存在的意義是啥?

所以 put,get 就無法做到強一致性了嗎?

我們在上文中已經知道,使用 volatile,synchronize,lock 是可以確保 happens before 語義的,同時經過分析我們知道使用 synchronize,lock 加鎖的設計是不滿足我們設計 CHM 的初衷的,那么只剩下 volatile 了,遺憾的是由于 Java 數組在元素層面的元數據設計上的缺失,是無法表達元素是 final、volatile 等語義的,所以 volatile 可以修飾變量,卻無法修飾數組中的元素,還有其他辦法嗎?來看看 Java 8 是怎么處理的(這里只列出了寫和讀方法中的關鍵代碼)。

private static final sun.misc.Unsafe U;

// 寫
final V putVal(K key, V value, boolean onlyIfAbsent) {
...
for (Node<K,V>[] tab = table;;) {
if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break;
}
}
...
}

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}


// 讀
public V get(Object key) {

if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
...
}
return null;
}

@SuppressWarnings("unchecked")
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

可以看到在 Java 8 中,CHM 使用了 unsafe 類來實現讀寫操作。

  • 對于寫首先使用 compareAndSwapObject(即我們熟悉的 CAS)來更新內存中數組中的元素。
  • 對于讀則使用了 getObjectVolatile 來讀取內存中數組中的元素(在底層其實是用了 C++ 的 volatile 來實現 java 中的 volatile 效果,有興趣可以看看)。

由于讀寫都是直接對內存操作的,所以通過這樣的方式可以保證 put,get 的強一致性,至此真相大白!Java 8 以后 put,get 是可以保證強一致性的!CHM 是通過 compareAndSwapObject 來取代對數組元素直接賦值的操作,通過 getObjectVolatile 來補上無法表達數組元素是 volatile 的坑來實現的。

注意并不是說 CHM 所有的操作都是強一致性的,比如 Java 8 中計算容量的方法 size() 就是弱一致性(Java 7 中此方法反而是強一致性),所以我們說強/弱一致性一定要確定好前提(比如指定 Java 8 下 CHM 的 put,get 這種場景)。

總結其實 Java 8 對 CHM 進行了一番比較徹底的重構,讓它的性能大幅度得到了提升,比如棄用 segment 這種設計,改用對每個槽位做分段鎖,使用紅黑樹來降低查詢時的復雜度,擴容時多個線程可以一起參與擴容等等,可以說 Java 8 的 CHM 的設計非常精妙,集 CAS,synchroinize,泛型等 Java 基礎語法之大成,又有巧妙的算法設計,讀后確實讓人大開眼界,有機會我會再和大家分享一下其中的設計精髓,另外我們對某些知識點一定要多加思考,最好能自己去翻翻源碼驗證一下真偽,相信你會對網上的一些謬誤會更容易看穿。

責任編輯:武曉燕 來源: 碼海
相關推薦

2022-05-14 22:20:23

公網IP地址

2018-07-31 13:01:00

人工智能

2020-04-13 16:05:25

JS裝飾器前端

2021-09-04 19:04:14

配置LogbackJava

2021-01-25 21:45:22

軟件測試學習技術

2024-02-01 11:57:31

this指針代碼C++

2021-08-13 05:50:01

ContainerdDockerKubernetes

2019-09-17 08:18:19

HTTP網絡協議狀態碼

2015-07-23 10:39:41

2019-10-09 16:14:30

Web服務器Tomcat

2015-10-13 17:11:46

藍牙物聯網

2019-03-21 09:45:11

TypeScript編程語言Javascript

2022-03-13 18:27:09

Redis數據庫開源

2020-05-15 16:37:13

PowerBI數據分析

2023-07-04 08:56:07

指針類型Golang

2025-03-03 08:40:00

JavaScriptthis開發

2020-05-15 15:29:36

Stata數據分析

2022-05-12 10:53:42

keepalivevrrp協議

2025-06-27 02:15:00

芯片流程數字芯片

2023-07-14 08:00:00

ORMRust ORMSQL
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 不卡一二三区 | 久久久久久高潮国产精品视 | 九色国产| 日韩影院一区 | 欧美成人一区二免费视频软件 | 99久久精品视频免费 | 九九导航 | 玖玖玖在线观看 | 天天干在线播放 | 91九色在线观看 | 国产成人久久av免费高清密臂 | 色噜噜狠狠色综合中国 | 欧美日韩视频 | 一区二区三区视频在线观看 | 久久久久国产一区二区三区不卡 | 一区二区三区在线 | 欧 | 免费的色网站 | 91在线网 | 亚洲精品欧美一区二区三区 | 99热最新| 人人干人人玩 | 成人三级在线播放 | 亚洲第一视频网 | 曰批视频在线观看 | 精品国产一区二区三区观看不卡 | 国产乱码精品一区二区三区忘忧草 | 国产精品一区三区 | 粉嫩av在线 | 成人精品在线观看 | 久久99精品久久 | 在线电影日韩 | 黑人精品欧美一区二区蜜桃 | 国产午夜高清 | 日韩一级黄色毛片 | 狠狠爱综合网 | 男女在线免费观看 | 欧美自拍第一页 | 久久久网| 最近中文字幕在线视频1 | 久久一区二 | av网站在线免费观看 |