写入null_ArrayList并发写出现Null值
ArrayList并非線程安全的容器,這一點大家可能都非常清楚,但是在并發寫入的情況下,不安全的情況具體有哪些,大家是否很清楚呢?本篇文章重點聊一下出現null的情況,然后對于其他并發寫的安全做一個簡單的敘述
我們看下面的代碼,打印List的元素數量以及打印存儲的元素
List list = new ArrayList<>(); for (int i=0;i<10;i++) { int finalI = i; new Thread(()->{ list.add(finalI +1); }).start(); } System.out.println(list.size()); System.out.println(list.toString());最理想的情況下,打印結果應該如下:
10[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]但是有可能出現一些其他問題,就像下面結果List元素出現null值的結果
10[null, 1, 3, 4, 5, 6, 7, 8, 9, 10]或者10[null, 2, 3, 4, 5, 6, 7, 8, 9, 10]或者10[null, null, null, 1, 5, 6, 7, 8, 9, 10]......在我看百度看到的所有答案中,關于并發寫出現Null值,幾乎都是將原因歸咎到add方法中的size++上,這里我個人認為這種回答應該是錯誤的,出現null值的原因應該是擴容所造成的。
public boolean add(E e) { ensureCapacityInternal(size + 1); elementData[size++] = e;}首先說一下為什么我覺得網上的答案是錯誤的,我們模擬add方法,然后使用javap命令拿到class的字節碼看一下:
#### Java程序int size = 0;int[] elementDate = new int[5];public void add() { elementDate[size++] = 10;}#### Javap 得到的字節碼 public void add(); Code: 0: aload_0 1: getfield #3 // Field elementDate:[I 4: aload_0 5: dup 6: getfield #2// Field size:I 9: dup_x1 10: iconst_1 11: iadd 12: putfield #2// Field size:I 15: bipush 10 17: iastore 18: return在add方法的字節碼中,通過getfield拿到elementDate數組放入棧頂(操作數棧),然后dup命令復制棧頂的數組并將復制值壓入棧頂,然后再通過getfield獲取size數值,下一步dup_x1命令會將棧頂的數值size復制兩份,并將兩個復制值壓入棧頂,然后iconst_1命令將數值1壓入棧頂,再使用iadd命令對棧頂的兩個元素進行相加,并通過putfield將size更新,最后iastore更新數組(因為dup_x1復制了兩份,所以數組的索引仍然是更新前的size)。大家可以好好想一下這個操作,無論size++多么不安全,因為索引復制兩份被保存的操作數棧中,所以不可能在list中出現null值,只會出現覆蓋的可能。
如果大家理解了上面的過程,我們思考下為什么null值出現了呢?由于ArrayList是基于數組實現,由于數組大小一旦確定就無法更改,所以其每次擴容都是將舊數組容器的元素拷貝到新大小的數組中(Arrays.copyOf函數),由于我們通過new ArrayList<>()實例的對象初始化的大小是0,所以第一次插入就會擴容,由于ArrayList并非線程安全,第二次插入時,第一次擴容可能并沒完成,于是也會進行一次擴容(第二次擴容),這次擴容所拿到list的elementDate是舊的,并不是第一次擴容后對象,于是會因為第一次插入的值并不在舊的elementDate中,而將null值更新到新的數組中。這里我們舉一個詳細的例子:
現在有線程A和B分別要插入元素1和2,當線程A調用add方法時size是0,于是會進行一次擴容,此時線程B調用add方法時size仍然是0,所以也會進行擴容,假設此時線程A比線程B擴容先完成,此時list的elementDate是新的數組對象(由線程A構建),然后開始執行elementDate[size++] = 1的程序,這個過程中線程B擴容拿到的數組仍然是舊的elementDate,于是線程B構造一個新的數組(數據全部為null),然后使list的elementDate指向線程B構造的對象,那么線程A之前構造的elementDate也就被丟掉了,但是由于size已經自增,所以線程B會在索引為1的位置賦予2,那么此時數組元素就成了[null,2],當然如果線程B擴容比線程A先完成那么就可能為[null,1]。
大家如果在初始化的時候就已經開辟好足夠大的容量,那么就不會出現上面的問題,關于上面的解釋大家可以作為參考,因為不同的編譯器可能javap得到的字節碼可能會不同吧(這里我編譯結果是size被復制兩份,然后使用其中的一份加一更新到size中,然后用復制的另一份作為索引更新數組,但是網上得到信息大家都認為是數組先賦值,然后size自增)。
除了上面元素為null的情況外,還會有其他錯誤
- 數量錯誤,集合數據正確
大家是不是第一反應是不是覺得這種結果是由ArrayList本身的不安全特效造成的呢?實際上這種結果和ArrayList本身沒有關系,只是因為我們打印不具有原子性所造成的。因為我們啟用了多線程,主線程調用size方法時,可能多線程內部對list還在繼續執行增加元素的操作,當主線程調用toString方法時,多線程已經執行完畢,所以元素數量正確,當然也有可能你調用toString方法時,多線程仍然未執行完,此時size和toString結果都不正確,如下:
8[1, 2, 3, 4, 5, 6, 7, 8, 9]- 覆蓋,這種情況的原因在上面的分析中以及提到,因為size++并不是原子性的,所以可能線程A自增的時候,線程B也進行一次自增,但是兩次自增的結果是一樣的,所以先完成的線程更新的數據會被后完成的線程覆蓋掉
總結
以上是生活随笔為你收集整理的写入null_ArrayList并发写出现Null值的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: freemarker html 引入sc
- 下一篇: 仍然报错_only_full_group