總共有兩大接口:Collection 和 Map ,一個元素集合,一個是鍵值對集合; 其中 List 和 Set 接口繼承了 Collection 接口,一個是有序元素集合,一個是無序元素集合; 而 ArrayList 和 LinkedList 實現了 List 接口,HashSet 實現了 Set 接口,這幾個都比較經常使用; HashMap 和 HashTable 實現了 Map 接口,而且 HashTable 是線程安全的,可是 HashMap 性能更好;javascript
java.util.Collection 是一個集合接口(集合類的一個頂級接口)。它提供了對集合對象進行基本操做的通用接口方法。Collection 接口在 Java 類庫中有不少具體的實現。Collection 接口的意義是爲各類具體的集合提供了最大化的統一操做方式,其直接繼承接口有 List 與 Set。html
Collections 則是集合類的一個工具類/幫助類,其中提供了一系列靜態方法,用於對集合中元素進行排序、搜索以及線程安全等各類操做。java
快速失敗:當你在迭代一個集合的時候,若是有另外一個線程正在修改你正在訪問的那個集合時,就會拋出一個 ConcurrentModification 異常。
在 java.util 包下的都是快速失敗,不能在多線程下發生併發修改(迭代過程當中被修改)。web
安全失敗:你在迭代的時候會去底層集合作一個拷貝,因此你在修改上層集合的時候是不會受影響的,不會拋出 ConcurrentModification 異常。
在 java.util.concurrent 包下的全是安全失敗的。能夠在多線程下併發使用,併發修改。數組
首先咱們查看以下案例:安全
public class ListTest {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for(int i=0;i<10;i++){
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
複製代碼
執行上述代碼,會拋出 java.util.ConcurrentModificationException
併發異常。多線程
public class ListTest {
public static void main(String[] args) {
List<String> list = new Vector<>();
for(int i=0;i<10;i++){
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
複製代碼
Vector 類是在 JDK1.0 出現的,比 ArrayList 還早,那爲何不推薦該方法呢?查看源碼得知:併發
public synchronized boolean add(E var1) {
++this.modCount;
this.ensureCapacityHelper(this.elementCount + 1);
this.elementData[this.elementCount++] = var1;
return true;
}
複製代碼
使用 Synchronized 關鍵字來實現同步操做,效率較低。緣由:在 Java 早期版本中,synchronized 屬於重量級鎖,效率低下,由於監視器鎖(monitor)是依賴於底層的操做系統的 Mutex Lock 來實現的,Java 的線程是映射到操做系統的原生線程之上的。若是要掛起或者喚醒一個線程,都須要操做系統幫忙完成,而操做系統實現線程之間的切換時須要從用戶態轉換到內核態,這個狀態之間的轉換須要相對比較長的時間,時間成本相對較高,這也是爲何早期的 synchronized 效率低的緣由。慶幸的是在 Java 6 以後 Java 官方對從 JVM 層面對 synchronized 較大優化,因此如今的 synchronized 鎖效率也優化得很不錯了。JDK1.6 對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減小鎖操做的開銷。app
List<String> list = Collections.synchronizedList(new ArrayList<>());
複製代碼
一樣咱們來查看源碼,Collections.synchronizedList()
的定義以下:框架
public static <T> List<T> synchronizedList(List<T> var0) {
return (List)(var0 instanceof RandomAccess ? new Collections.SynchronizedRandomAccessList(var0) : new Collections.SynchronizedList(var0));
}
複製代碼
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable
複製代碼
一路跳轉後,最終定位到 SynchronizedCollection 靜態內部類,List 對象也就變爲了 SynchronizedCollection 類型,查看其 add 方法定義:
public boolean add(E var1) {
synchronized(this.mutex) {
return this.c.add(var1);
}
}
複製代碼
同 Vector 同樣,仍是採用的 Synchronized 關鍵字來作同步操做,只是封裝在 Collections 工具類中。
咱們來經過看源碼的方式來理解 CopyOnWriteArrayList,實際上 CopyOnWriteArrayList 內部維護的也是一個數組
private transient volatile Object[] array;
複製代碼
只是該數組是被 volatile 修飾,注意這裏僅僅修飾的是數組引用,與被 volatile 修飾的普通變量有所區別,關於這點我在以前的文章中有分析,對 volatile 還不是太瞭解的朋友也能夠去看一下。對 list 來講,咱們天然而然最關心的就是讀寫的時候,分別爲 get 和 add 方法的實現。
如下方式利用 CopyOnWriteArrayList 來保證線程安全。
List<String> list = new CopyOnWriteArrayList<>();
複製代碼
CopyOnWriteArrayList 比 Vector 更高級,由於加鎖的方式不同,前者使用 Lock 鎖。查看其 add 方法定義:
public boolean add(E var1) {
ReentrantLock var2 = this.lock;
var2.lock();
boolean var6;
try {
Object[] var3 = this.getArray();
int var4 = var3.length;
Object[] var5 = Arrays.copyOf(var3, var4 + 1);
var5[var4] = var1;
this.setArray(var5);
var6 = true;
} finally {
var2.unlock();
}
return var6;
}
複製代碼
除了經過 Lock 來加鎖處理,每次寫數據前,都會另外經過 Arrays.copyOf 方法複製一份數據,在寫入的時候避免覆蓋,形成數據問題。
咱們須要注意 CopyOnWriteArrayList 中的這兩個字段屬性以及相關方法。
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
final Object[] getArray() {
return this.array;
}
final void setArray(Object[] var1) {
this.array = var1;
}
複製代碼
注意事項:
CopyOnWriteArrayList 的缺點:
CopyOnWriteArrayList 的優勢:
關於優缺點中各自的第二條,經過如下案例向你們說明:
public class CowTest {
public static void main(String[] args) {
// 初始化一個list,放入5個元素
final List<Integer> list = new CopyOnWriteArrayList<>();
// final List<Integer> list = new Vector<>();
// final List<Integer> list = Collections.synchronizedList(new ArrayList<>());
for(int i = 0; i < 5; i++) {
list.add(i);
}
// 線程一:經過Iterator遍歷List
Thread t1 = new Thread(()-> {
for(int item : list) {
System.out.println("遍歷元素:" + item);
// 因爲程序跑的太快,這裏sleep了1秒來調慢程序的運行速度
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 線程二:add一個元素
Thread t2 = new Thread(() ->{
// 因爲程序跑的太快,這裏sleep了1秒來調慢程序的運行速度
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.add(5);
});
t1.start();
t2.start();
try {
t1.join();
t2.join();
System.out.println(list);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
複製代碼
執行結果爲:
遍歷元素:0
遍歷元素:1
遍歷元素:2
遍歷元素:3
遍歷元素:4
[0, 1, 2, 3, 4, 5]
複製代碼
當線程1在遍歷集合,線程2往集合中新增一個數據,若是使用前兩種線程安全的替換方案,有可能產生 ConcurrentModificationException 異常。爲何 CopyOnWriteArrayList 就沒問題呢?緣由在於它保證線程安全採用的是寫時複製並加鎖,因此線程1在遍歷時拿到的集合是舊的,這也就是結果中爲啥沒有輸出5的緣由,雖然拿到的是舊集合,可是至少不會報錯。
CopyOnWriteArrayList 只能保證數據最終一致性,這點從結果中能夠看出來,當在線程2中給集合新增了元素,可是沒法當即通知到線程1,因此沒法保證數據實時性。可是當其餘線程再次讀取集合時,才能讀取到完整的新數據。
最後總結一下,CopyOnWriteArrayList
適合在讀多寫少的場景使用,實時性要求不高,否則讀取到的多是舊數據。添加數據能夠採用批量添加 addAll 方法,減小內存佔用。
首先咱們查看以下案例:
public class SetTest {
public static void main(String[] args) {
Set<String> set= new HashSet<>();
for (int i = 0; i < 20; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
}
複製代碼
執行上述代碼,會拋出 java.util.ConcurrentModificationException
併發異常。
Set<String> set = Collections.synchronizedSet(new HashSet<>());
複製代碼
同 List 使用 Collections.synchronizedList()
同樣,經過 synchronized 來實現線程安全。
Set<String> set = new CopyOnWriteArraySet<>();
複製代碼
咱們點擊查看 CopyOnWriteArraySet 類,發現其背後經過 CopyOnWriteArrayList 來存儲數據。
private final CopyOnWriteArrayList<E> al;
public CopyOnWriteArraySet() {
this.al = new CopyOnWriteArrayList();
}
複製代碼
咱們知道 Set 集合中的元素是不可重複,那麼這是怎麼作到的呢?首先來查看 add 方法。
public boolean add(E var1) {
return this.al.addIfAbsent(var1);
}
複製代碼
其實是執行 CopyOnWriteArrayList 中的 addIfAbsent 方法。
public boolean addIfAbsent(E var1) {
Object[] var2 = this.getArray(); //獲取當前數組信息
//indexOf方法用於比對新增值在原數組中是否存在,若存在,則返回值不小於0,不然返回-1,即要執行addIfAbsent方法
return indexOf(var1, var2, 0, var2.length) >= 0 ? false : this.addIfAbsent(var1, var2);
}
private boolean addIfAbsent(E var1, Object[] var2) {
ReentrantLock var3 = this.lock;
var3.lock();
try {
Object[] var4 = this.getArray();
int var5 = var4.length;
boolean var13;
if (var2 != var4) {
int var6 = Math.min(var2.length, var5);
for(int var7 = 0; var7 < var6; ++var7) {
if (var4[var7] != var2[var7] && eq(var1, var4[var7])) {
boolean var8 = false;
return var8;
}
}
if (indexOf(var1, var4, var6, var5) >= 0) {
var13 = false;
return var13;
}
}
Object[] var12 = Arrays.copyOf(var4, var5 + 1);
var12[var5] = var1;
this.setArray(var12);
var13 = true;
return var13;
} finally {
var3.unlock();
}
}
複製代碼
同 CopyOnWriteArrayList 中的 add 方法相似,addIfAbsent 方法也是先加鎖,而後寫前複製來保證線程安全。
首先咱們查看以下案例:
public class MapTest {
public static void main(String[] args) {
// Map<String,Object> map = new HashMap<>();
//加載因子和初始容量
//等價於:new HashMap(16,0.75)
for (int i = 0; i < 30; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
複製代碼
執行上述代碼,會拋出 java.util.ConcurrentModificationException
併發異常。
Map<String,Object> map = Collections.synchronizedMap(new HashMap<>());
複製代碼
Map<String,Object> map = new Hashtable<>();
複製代碼
該類經過對讀寫進行加鎖(synchronized)操做,一個線程在讀寫元素,其他線程必須等待,性能較低。
Map<String,Object> map = new ConcurrentHashMap<>();
複製代碼
關於 ConcurrentHashMap 的詳細學習,能夠參考這篇文章。