Java集合的線程安全問題

多線程一直Java開發(fā)中的難點或粮,也是面試中的常客朵锣,趁著還有時間谬盐,打算鞏固一下JUC方面知識,我想機會隨處可見诚些,但始終都是留給有準(zhǔn)備的人的飞傀,希望我們都能加油!N芘搿砸烦!

沉下去,再浮上來绞吁,我想我們會變的不一樣的幢痘。

我想我們大家肯定都使用過ArrayList的吧。不知道你之前有沒有想過它也會牽扯到線程安全問題勒家破。

一颜说、問題引入:

我們一起先看看下面的程序吧,看你能看出什么問題嗎汰聋?

public static void main(String[] args) {
    List list = new ArrayList();
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            list.add(UUID.randomUUID().toString());
            System.out.println(list);
        }, "線程" + i).start();
    }
}

你覺得能每次都能正常輸出嗎门粪?

答案是否定的,也許好幾次運行程序都不會出錯烹困,但是偶爾就會遇上一次的玄妈。會報一個ConcurrentModificationException的異常,中文名為:并發(fā)修改異常。

原因:就是我們正在讀的時候拟蜻,正好也遇上了寫操作绎签,我們這里又沒有同步代碼塊、鎖什么的瞭郑,那么此時肯定是不可以繼續(xù)往下執(zhí)行的辜御。

還有ArrayList的add方法并非線程同步的。(jdk11源碼)

public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}

我們該如何解決這個問題呢屈张??袱巨?

二阁谆、解決方式

第一種方式:使用 Vector<E>

我們可以使用Vector來代替ArrayList,因為Vector 繼承了 AbstractList 類并且實現(xiàn)了List<E> 愉老、RandmoAccess 接口场绿。

RandmoAccess 是 java 中用來被 List 實現(xiàn),為 List 提供快速訪問功能的嫉入。在 Vector 中焰盗,我們即可以通過元素的序號快速獲取元素對象;這就是快速隨機訪 問咒林。

public class Vector<E> extends AbstractList<E> implements List<E> ,RandomAccess 

我們將上面的程序修改后熬拒,程序?qū)⒉辉俪霈F(xiàn)異常。

public static void main(String[] args) {
    List list = new Vector();
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            list.add(UUID.randomUUID().toString());
            System.out.println(list);
        }, "線程" + i).start();
    }
}

原因其實就在 Vector 的代碼中垫竞。

public synchronized boolean add(E e) {
    modCount++;
    add(e, elementData, elementCount);
    return true;
}

add方法上加了synchronized關(guān)鍵字澎粟,讓這個方法成為了同步方法塊。

第二種方式:使用 Collections

Collections 提供了方法 synchronizedList 保證 list 是同步線程安全的欢瞪。

Collections 僅包含對集合進(jìn)行操作或返回集合的靜態(tài)方法活烙,所以我們通常也稱Collections 為集合的工具類。

public static void main(String[] args) {
    List list = Collections.synchronizedList(new ArrayList<>());
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            list.add(UUID.randomUUID().toString());
            System.out.println(list);
        }, "線程" + i).start();
    }
}

這樣也不會發(fā)生異常遣鼓。源碼上也都有體現(xiàn)

public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}

大多數(shù)方法都提供了同步和不同步兩種api啸盏。

第三種方式: 使用 CopyOnWriteArrayList

CopyOnWriteArrayList和ArrayList 一樣,它是個可變數(shù)組骑祟。

有以下幾個特點:

  1. 更新操作開銷大(add()回懦、set()、remove()等等)曾我,因為要復(fù)制整個數(shù)組
  2. 是線程安全的粉怕。
  3. 它最適合于具有以下特征的應(yīng)用程序:List 大小通常保持很小,只讀操作遠(yuǎn)多 于可變操作抒巢,需要在遍歷期間防止線程間的沖突贫贝。
  4. 獨占鎖效率低:采用讀寫分離思想
  5. 寫線程獲取到鎖,其他寫線程阻塞
  6. 復(fù)制思想

CopyOnWriteArrayList 的思想和原理

當(dāng)我們要添加一個元素的時候,不直接往當(dāng)前容器中添加稚晚,而是應(yīng)該先將當(dāng)前容器復(fù)制一份崇堵,然后在新的容器中進(jìn)行添加操作,等到添加完成后客燕,我們再讓原容器的引用指向新的容器鸳劳。

當(dāng)然,這個時候會拋出來一個新的問題也搓,也就是數(shù)據(jù)不一致的問題赏廓。如果寫線程還沒來得及寫進(jìn)內(nèi)存,那么其他的線程就會讀到了臟數(shù)據(jù)傍妒。

public static void main(String[] args) {
    List list = new CopyOnWriteArrayList();
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            list.add(UUID.randomUUID().toString());
            System.out.println(list);
        }, "線程" + i).start();
    }
}

為什么不會產(chǎn)生線程安全問題呢幔摸?

我們從"動態(tài)數(shù)組"和“線程安全”兩個方面來看待:

動態(tài)數(shù)組機制 :

  • 它內(nèi)部有個volatile 數(shù)組(array)來保持?jǐn)?shù)據(jù)

  • 它在涉及到更新操作時颤练,都會新建數(shù)組既忆,所以CopyOnWriteArrayList效率都會很低;但如果只是單單進(jìn)行遍歷查找的話嗦玖, 效率是能夠達(dá)到比較高的患雇。

    public boolean add(E element) {
        synchronized (lock) {
            checkForComodification();
            CopyOnWriteArrayList.this.add(offset + size, element);
            expectedArray = getArray();
            size++;
        }
        return true;
    }
    // CopyOnWriteArrayList.this.add(offset + size, element);
    public void add(int index, E element) {
        synchronized (lock) {
            Object[] es = getArray();
            int len = es.length;
            if (index > len || index < 0)
                throw new IndexOutOfBoundsException(outOfBounds(index, len));
            Object[] newElements;
            int numMoved = len - index;
            if (numMoved == 0)
                newElements = Arrays.copyOf(es, len + 1);
            else {
                newElements = new Object[len + 1];
                System.arraycopy(es, 0, newElements, 0, index);
                System.arraycopy(es, index, newElements, index + 1,
                                 numMoved);
            }
            newElements[index] = element;
            setArray(newElements);
        }
    }
    

線程安全機制:

  • 通過 volatile 和互斥鎖(synchronized)來實現(xiàn)的。

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;
    
  • 通過“volatile 數(shù)組”來保存數(shù)據(jù)的

    • 一個線程讀取 volatile 數(shù)組時宇挫,總能看 到其它線程對該 volatile 變量最后的寫入苛吱;就這樣,通過 volatile 提供了“讀 取到的數(shù)據(jù)總是最新的”這個機制的保證捞稿。
  • 通過互斥鎖來保護(hù)數(shù)據(jù)

    • 在更新操作時又谋,都會率先去獲取互斥鎖, 在修改完畢之后娱局,先將數(shù)據(jù)更新到“volatile 數(shù)組”中彰亥,然后再“釋放互斥鎖”,這樣就能夠保證數(shù)據(jù)的安全衰齐。

另外補充
除了ArrayList是線程不安全的任斋,還有HashMap、HashSet都是不安全的耻涛。
HashMap废酷、HashSet的解決方式可以用Hashtable解決,還有CopyOnWriteArraySet解決抹缕,當(dāng)然不局限于這一種哈澈蟆,(還沒看完??) HashMap還可以用ConcurrentHashMap解決。

三卓研、自言自語

最近又開始了JUC的學(xué)習(xí)趴俘,感覺Java內(nèi)容真的很多睹簇,但是為了能夠走的更遠(yuǎn),還是覺得應(yīng)該需要打牢一下基礎(chǔ)寥闪。

最近在持續(xù)更新中太惠,如果你覺得對你有所幫助,也感興趣的話疲憋,關(guān)注我吧凿渊,讓我們一起學(xué)習(xí),一起討論吧缚柳。

你好埃脏,我是博主寧在春,Java學(xué)習(xí)路上的一顆小小的種子秋忙,也希望有一天能扎根長成蒼天大樹剂癌。

希望與君共勉??

待我們,別時相見時翰绊,都已有所成

簡書 | 寧在春

CSDN | 寧在春

掘金 | 寧在春

知乎 | 寧在春

博客園 | 寧在春

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末旁壮,一起剝皮案震驚了整個濱河市监嗜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌抡谐,老刑警劉巖裁奇,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異麦撵,居然都是意外死亡刽肠,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進(jìn)店門免胃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來音五,“玉大人,你說我怎么就攤上這事羔沙√衫裕” “怎么了?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵扼雏,是天一觀的道長坚嗜。 經(jīng)常有香客問我,道長诗充,這世上最難降的妖魔是什么苍蔬? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮蝴蜓,結(jié)果婚禮上碟绑,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好蜈敢,可當(dāng)我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布辜荠。 她就那樣靜靜地躺著,像睡著了一般抓狭。 火紅的嫁衣襯著肌膚如雪伯病。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天否过,我揣著相機與錄音午笛,去河邊找鬼。 笑死苗桂,一個胖子當(dāng)著我的面吹牛药磺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播煤伟,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼癌佩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了便锨?” 一聲冷哼從身側(cè)響起围辙,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎放案,沒想到半個月后姚建,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡吱殉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年掸冤,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片友雳。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡稿湿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出沥阱,到底是詐尸還是另有隱情缎罢,我是刑警寧澤,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布考杉,位于F島的核電站策精,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏崇棠。R本人自食惡果不足惜咽袜,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望枕稀。 院中可真熱鬧询刹,春花似錦谜嫉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至蔽挠,卻和暖如春住闯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背澳淑。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工比原, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人杠巡。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓量窘,卻偏偏與公主長得像,于是被迫代替她去往敵國和親氢拥。 傳聞我的和親對象是個殘疾皇子蚌铜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,509評論 2 348

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