ConcurrentHashMap分析

前言

最近在看并發(fā)編程藝術(shù)這本書费变,對看書的一些總結(jié)及個人理解。

為什么使用ConcurrentHashMap圣贸?
jdk5.0以后提供了多種并發(fā)類容器來替代同步類容器從而改善性能挚歧。同步類容器的狀態(tài)都是串行化的。他們雖然實現(xiàn)了線程安全吁峻,但是嚴重降低了并發(fā)性滑负,在多線程環(huán)境時,嚴重降低了應用程序的吞吐量用含。

線程不安全的HashMap

public class ConcurrentPutHashMap {

    public static void main(String[] args) throws InterruptedException {
        final HashMap<String, String> map = new HashMap<>(2);
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10000; i++) {
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            map.put(UUID.randomUUID().toString(), "");
                        }
                    }, "ftf" + i).start();
                }
            }
        }, "ftf");
        t.start();
        t.join();
    }

}

效率低下的HashTable

HashTable容器使用synchronized來保證線程安全矮慕,但在線程競爭激烈的情況下HashTable的效率非常低下。因為當一個線程訪問HashTable的同步方法啄骇,其他線程也訪問HashTable的同步方法時痴鳄,會進入阻塞或輪詢狀態(tài)。如線程1使用put進行元素添加缸夹,線程2不但不能使用put方法添加元素痪寻,也不能使用get方法來獲取元素,所以競爭越激烈效率越低虽惭。

   public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }

ConcurrentHashMap的鎖分段技術(shù)可有效提升并發(fā)訪問率

HashTable容器在競爭激烈的并發(fā)環(huán)境下表現(xiàn)出效率低下的原因是所有訪問HashTable的線程都必須競爭同一把鎖橡类,假如容器里有多把鎖,每一把鎖用于鎖容器其中一部分數(shù)據(jù)芽唇,那么當多線程訪問容器里不同數(shù)據(jù)段的數(shù)據(jù)時顾画,線程間就不會存在鎖競爭,從而可以有效提高并發(fā)訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術(shù)研侣。首先將數(shù)據(jù)分成一段一段地存儲谱邪,然后給每一段數(shù)據(jù)配一把鎖,當一個線程占用鎖訪問其中一個段數(shù)據(jù)的時候庶诡,其他段的數(shù)據(jù)也能被其他線程訪問虾标。

ConcurrentHashMap的結(jié)構(gòu)

ConcurrentHashMap是由Segment數(shù)組結(jié)構(gòu)和HashEntry數(shù)組結(jié)構(gòu)組成。Segment是一種可重入鎖(ReentrantLock)灌砖,在ConcurrentHashMap里扮演鎖的角色璧函;HashEntry則用于存儲鍵值對數(shù)據(jù)。一個ConcurrentHashMap里包含一個Segment數(shù)組基显。Segment的結(jié)構(gòu)和HashMap類似蘸吓,是一種數(shù)組和鏈表結(jié)構(gòu)。一個Segment里包含一個HashEntry數(shù)組撩幽,每個HashEntry是一個鏈表結(jié)構(gòu)的元素库继,每個Segment守護著一個HashEntry數(shù)組里的元素,當對HashEntry數(shù)組的數(shù)據(jù)進行修改時窜醉,必須首先獲得與它對應的Segment鎖.

ConcurrentHashMap初始化

     /**
     * Creates a new, empty map with the default initial table size (16).
     */
    public ConcurrentHashMap() {
    }

默認的無參數(shù)構(gòu)造就是生成segments數(shù)組的長度為16的一個對象宪萄,即容器中的鎖也是16個。

既然ConcurrentHashMap使用分段鎖Segment來保護不同段的數(shù)據(jù)榨惰,那么在插入和獲取元素的時候拜英,必須先通過散列算法定位到Segment±糯撸可以看到ConcurrentHashMap會首先使用Wang/Jenkins hash的變種算法對元素的hashCode進行一次再散列居凶。

ConcurrentHashMap的操作

get操作

Segment的get操作實現(xiàn)非常簡單和高效。先經(jīng)過一次再散列藤抡,然后使用這個散列值通過散列運算定位到Segment侠碧,再通過散列算法定位到元素,代碼如下缠黍。

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

get操作的高效之處在于整個get過程不需要加鎖弄兜,除非讀到的值是空才會加鎖重讀。我們知道HashTable容器的get方法是需要加鎖的瓷式,那么ConcurrentHashMap的get操作是如何做到不加鎖的呢替饿?原因是它的get方法里將要使用的共享變量都定義成volatile類型,如用于統(tǒng)計當前Segement大小的count字段和用于存儲值的HashEntry的value蒿往。定義成volatile的變量盛垦,能夠在線程之間保持可見性湿弦,能夠被多線程同時讀瓤漏,并且保證不會讀到過期的值,但是只能被單線程寫(有一種情況可以被多線程寫,就是寫入的值不依賴于原值)蔬充,在get操作里只需要讀不需要寫共享變量count和value蝶俱,所以可以不用加鎖。之所以不會讀到過期的值饥漫,是因為根據(jù)Java內(nèi)存模型的happen before原則榨呆,對volatile字段的寫入操作先于讀操作,即使兩個線程同時修改和獲取volatile變量庸队,get操作也能拿到最新的值积蜻,這是用volatile替換鎖的經(jīng)典應用場景。

put操作

由于put方法里需要對共享變量進行寫入操作彻消,所以為了線程安全竿拆,在操作共享變量時必須加鎖。put方法首先定位到Segment宾尚,然后在Segment里進行插入操作丙笋。插入操作需要經(jīng)歷兩個步驟,第一步判斷是否需要對Segment里的HashEntry數(shù)組進行擴容煌贴,第二步定位添加元素的位置御板,然后將其放在HashEntry數(shù)組里。

在插入元素前會先判斷Segment里的HashEntry數(shù)組是否超過容量(threshold)牛郑,如果超過閾值怠肋,則對數(shù)組進行擴容。
ConcurrentHashMap不會對整個容器進行擴容淹朋,而只對某個segment進行擴容灶似。

size操作

如果要統(tǒng)計整個ConcurrentHashMap里元素的大小,就必須統(tǒng)計所有Segment里元素的大小后求和瑞你。Segment里的全局變量count是一個volatile變量酪惭,那么在多線程場景下,是不是直接把所有Segment的count相加就可以得到整個ConcurrentHashMap大小了呢者甲?不是的春感,雖然相加時可以獲取每個Segment的count的最新值,但是可能累加前使用的count發(fā)生了變化虏缸,那么統(tǒng)計結(jié)果就不準了鲫懒。所以,最安全的做法是在統(tǒng)計size的時候把所有Segment的put刽辙、remove和clean方法全部鎖住窥岩,但是這種做法顯然非常低效。
因為在累加count操作過程中宰缤,之前累加過的count發(fā)生變化的幾率非常小颂翼,所以ConcurrentHashMap的做法是先嘗試2次通過不鎖住Segment的方式來統(tǒng)計各個Segment大小晃洒,如果統(tǒng)計的過程中,容器的count發(fā)生了變化朦乏,則再采用加鎖的方式來統(tǒng)計所有Segment的大小球及。
那么ConcurrentHashMap是如何判斷在統(tǒng)計的時候容器是否發(fā)生了變化呢?使用modCount變量呻疹,在put吃引、remove和clean方法里操作元素前都會將變而得知容器的大小是否發(fā)生變化。

總結(jié)

ConcurrentHashMap內(nèi)部使用段(Segment)來表示這些不同的部分刽锤,每個段其實就是一個小的HashTable镊尺,它們有自己的鎖。只要多個修改操作發(fā)生在不同的段上并思,他們就可以并發(fā)進行鹅心。把一個著呢個體分成了16個段(Segment)。
也就是最高支持16個線程的并發(fā)修改操作纺荧。這也是在多線程場景時減小鎖的粒度從而降低鎖競爭的一種方案旭愧,并且代碼中大多共享變量使用volatile關(guān)鍵字聲明(比如size方法中的count),目的是第一時間獲取修改的內(nèi)容宙暇,性能非常好输枯。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市占贫,隨后出現(xiàn)的幾起案子桃熄,更是在濱河造成了極大的恐慌,老刑警劉巖型奥,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瞳收,死亡現(xiàn)場離奇詭異,居然都是意外死亡厢汹,警方通過查閱死者的電腦和手機螟深,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來烫葬,“玉大人界弧,你說我怎么就攤上這事〈钭郏” “怎么了垢箕?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長兑巾。 經(jīng)常有香客問我条获,道長,這世上最難降的妖魔是什么蒋歌? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任帅掘,我火速辦了婚禮委煤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘锄开。我一直安慰自己素标,他們只是感情好称诗,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布萍悴。 她就那樣靜靜地躺著,像睡著了一般寓免。 火紅的嫁衣襯著肌膚如雪癣诱。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天袜香,我揣著相機與錄音撕予,去河邊找鬼。 笑死蜈首,一個胖子當著我的面吹牛实抡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播欢策,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼吆寨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了踩寇?” 一聲冷哼從身側(cè)響起啄清,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎俺孙,沒想到半個月后辣卒,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡睛榄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年荣茫,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片场靴。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡计露,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出憎乙,到底是詐尸還是另有隱情票罐,我是刑警寧澤,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布泞边,位于F島的核電站该押,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏阵谚。R本人自食惡果不足惜蚕礼,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一烟具、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧奠蹬,春花似錦朝聋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至狸演,卻和暖如春言蛇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背宵距。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工腊尚, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人满哪。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓婿斥,卻偏偏與公主長得像,于是被迫代替她去往敵國和親哨鸭。 傳聞我的和親對象是個殘疾皇子民宿,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容