問題描述:
對一個源list使用並行流對其進行遍歷的時候往宿list添加元素,再次遍歷宿list的時候會拋出空指針異常問題而且會現宿list size大小也有問題。
問題復原:
@org.junit.Test
public void test2() {
List<Integer> source = new LinkedList<>();
List<String> target = new LinkedList<>();
for (int i = 0; i < 1000; i++) {
source.add(i);
}
source.parallelStream().forEach(i -> {
target.add(String.valueOf(i));
});
System.out.println("target size -> " + target.size());
for (String s : target) {
System.out.println(s);
}
}
運行結果
問題分析:
不開啟多線程的時候targetList的size一定是1000的,而且不會出現空指針異常。
這兩個問題其實都和size++這句話有關
1. size大小為什么不是1000
分析源碼:
add是尾插,源碼如下
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
模擬一個場景
- size現在的值為100
- 線程a拿到了size大小100,此時cpu讓出執行權給線程b
- 線程b拿到了size大小也為100
- 那么無論他們誰先加1,最后size的值會被101覆蓋兩次,導致size的大小不會是102
2. 再次遍歷target的時候為什么會報空指針異常問題
其實也和尾插法這段代碼有關
模擬場景:
-
設現在的size為100
-
線程a拿到了鏈表尾部元素last 之后讓出執行權給線程b
-
線程b也拿到了相同的last之后一直執行完了add操作,此時size = 101
-
線程a也執行了 l.next = newNode;覆蓋了線程b的在size = 101 位置上的值,之后也進行了size++的操作,size = 102
-
此時出現的問題就是size = 101 位置上的值被覆蓋了兩次,但是102位置上的值是null。
-
所以遍歷的時候會報空指針異常。
-
因為last = newNode;這一步的重復覆蓋,可以預測所有的null都會是鏈表末尾。
3. 關於為什么foreach增強for循環為什么會報空指針的問題
Linkedlist的foreach循環,是依賴Iterator的,LinkedList也有自己的迭代器,源碼如下
可以看出hasnext的判斷並不是node.next != null 而是 nextIndex < size 所以即使null都在末尾也會在 next = next.next的時候報空指針異常。
3. 問題解決
List<String> target = Collections.synchronizedList(new LinkedList<>());
讓需要進行add操作的list,轉換成線程安全的。
關於ArrayList
其實也是和size++有關,與LinkedList不同的是由於實現尾插的方式不一致,所以導致null可能在中間
源碼如下:
結果如下: