Java多線程併發編程中併發容器第二篇之List的併發類講解

Java多線程併發編程中併發容器第二篇之List的併發類講解

概述

本文我們將詳細講解list對應的併發容器以及用代碼來測試ArrayList、vector以及CopyOnWriteArrayList在100個線程向list中添加1000個數據後的比較

本文是《凱哥分享Java併發編程之J.U.C包講解》系列教程中的第六篇。如果想系統學習,凱哥(kaigejava)建議從第一篇開始看。

從本篇開始,我們就來講解講解Java的併發容器。大致思路:先介紹什麼是併發容器。然後講解list相關的、map相關的以及隊列相關的。這個系列會有好幾篇文章。大家最好跟著一篇一篇學。

正文開始

併發容器分類講解

CopyOneWriteArrayList

Copy-One-Write:即寫入時候複製。

我們知道在原來List子類中vactor是同步容器線程安全的。這個CopyOneWriteArrayList可以理解為是他的併發替代品。

其底層數據結構也是數值。和ArrayList的不同之處就在於:在list對象中新增或者是刪除元素的時候會把原來的集合copy一份,增刪操作是在新的對象中操作的。操作完成之後,會將新的數組替換原來的數組。

我們來看看CopyOnWriteArrayList源碼中的add方法:

Java多線程併發編程中併發容器第二篇之List的併發類講解

我們來看看setArray方法:

Java多線程併發編程中併發容器第二篇之List的併發類講解

發現了嗎?變量使用的是transient和volatile兩個關鍵之來修飾的。

在之前文章中,我們知道了volatile關鍵字是內存可見性。那麼transient關鍵字是幹嘛的呢?我們來看下百科解釋:

Java多線程併發編程中併發容器第二篇之List的併發類講解

關鍵的一句話:用transient關鍵字修飾的成員變量不用參與序列化過程。

添加註釋後的源碼:

Java多線程併發編程中併發容器第二篇之List的併發類講解

public boolean add(E e) {

final ReentrantLock lock = this.lock;

//獲取到鎖

lock.lock();

try {

//獲取到原集合

Object[] elements = getArray();

int len = elements.length;

//將原集合copy一份到新的集合中。並設置新的集合的長度為原集合長度+1

Object[] newElements = Arrays.copyOf(elements, len + 1);

//將需要新增的元數添加到新的素組中

newElements[len] = e;

//將新數組替換原來數據。 使用transient和volatitle關鍵字修飾的

setArray(newElements);

return true;

} finally {

lock.unlock();

}

}

代碼很簡單,大致流程如下:

先從ReentrantLock中獲取到鎖(這樣在多線程下可以防止其他線程來修改容器list裡面內容了);

通過arrays.copyOf方法copy出一份原有數組長度+1;

將要添加的元素賦值給copy出來的數組;

使用setArray方法將新得數組替換原有素組。

因為都是List集合。我們就拿ArrayList、vector以及CopyOnWriteArrayList進行比較:

ArrayList、vector以及CopyOnWriteArrayList比較

業務場景描述:

啟動100個線程,向對象中添加1000個數據。查看各自運行結果耗時及插入數據總數。代碼在文章最後凱哥會貼出來。

先用線程非安全的arrayList執行效果:

Java多線程併發編程中併發容器第二篇之List的併發類講解

執行arryList時間為 : 112毫秒!

List.size() : 93266

我們發現list的長度不對。正確的應該是100*1000.從結果來看,arrayList丟數據了。

使用Vector執行後的效果:

Java多線程併發編程中併發容器第二篇之List的併發類講解

執行vector時間為 : 98毫秒!

List.size() : 100000

執行的總數對,說下同步鎖沒有丟數據。

在來看看copyOnWriteArrayList執行效果:

Java多線程併發編程中併發容器第二篇之List的併發類講解

執行copyOnWriteArrayList時間為 : 5951毫秒!

List.size() : 100000

運行後數據比較:

從上面表格中我們可以看出非安全線程的容器會丟數據。使用copyOneWriteArrayList耗時很長。那是因為每次運行都要copyof一份。

總結

copyArrayList(這裡凱哥就簡寫了):是讀寫分離的。在寫的時候會複製一個新的數組來完成插入和修改或者刪除操作之後,再將新的數組給array.讀取的時候直接讀取最新的數據。

因為在寫的時候需要向主內存申請控件,導致寫操作的時候,效率非常低的(雖然在操作時候比較慢得,但是在刪除或者修改數組的頭和尾的時候還是很快的。因為其數據結構決定查找頭和尾快,而且執行不需要同步鎖)

從上面表中,可以看出copyArrayList雖然保證了線程的安全性,但是寫操作效率太low了。但是相比Vector來說,在併發安全方面的性能要比vector好;

CopyArrayList和Vector相比改進的地方:

Vector是在新增、刪除、修改以及查詢的時候都使用了Synchronized關鍵字來保證同步的。但是每個方法在執行的時候,都需要獲取到鎖,在獲取鎖等待的過程中性能就會大大的降低的。

CopyArrayList的改進:只是在新增和刪除的方法上使用了ReentrantLock鎖進行(這裡凱哥就不截圖源碼了,自己可以看看源碼)。在讀的時候不加鎖的。所以在讀的方面性能要不vector性能要好。

所以,CopyArrayList支持讀多寫少的併發情況

CopyOnWriteArrayList的使用場景:

由於讀操作不加鎖,增刪改三個操作加鎖的,因此適用於讀多寫少的場景,

侷限性:因為讀的時候不加鎖的,讀的效率和普通的arrayList是一樣的。但是請看讀操作:

Java多線程併發編程中併發容器第二篇之List的併發類講解

Java多線程併發編程中併發容器第二篇之List的併發類講解

在get的時候array使用的是volatile修飾的。是內存可見的。所以可以說copyArrayList在讀的時候不會出現arrayList讀取到髒數據的問題。

Get(i)方法比較如下:

Java多線程併發編程中併發容器第二篇之List的併發類講解


附件:arrayList、vector、copyOnwriteArrayList比較的代碼:

public static void main(String[] args) { //使用線程不安全的arrayList // List<string> arryList = new ArrayList<>(); //使用vector // List<string> arryList = new Vector<>();/<string>/<string>

//使用copyOnWriteArrayList List<string> arryList = new CopyOnWriteArrayList<>(); Random random = new Random(); Thread [] threadArr = new Thread[100]; CountDownLatch latch = new CountDownLatch(threadArr.length); Long beginTime = System.currentTimeMillis(); for(int i = 0;i<threadarr.>length;i++){ threadArr[i] = new Thread(new Runnable() { @Override public void run() { for(int j = 0; j < 1000; j++){ try { arryList.add("value" + random.nextInt(100000)); } catch (Exception e) { } } latch.countDown(); } }); } for(Thread t : threadArr){ t.start(); } try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); }

long endTime = System.currentTimeMillis(); System.out.println("執行copyOnWriteArrayList時間為 : " + (endTime-beginTime) + "毫秒!"); System.out.println("List.size() : " + arryList.size());}/<threadarr.>/<string>


分享到:


相關文章: