共計 7757 個字符,預計需要花費 20 分鐘才能閱讀完成。
本篇文章為大家展示了如何理解 Java 并發容器 J.U.C,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
J.U.C 是 java.util.concurrent 的簡寫, 里面提供了很多線程安全的集合。
CopyOnWriteArrayList 介紹
CopyOnWriteArrayList 相比于 ArrayList 是線程安全的, 字面意思是寫操作時復制。CopyOnWriteArrayList 使用寫操作時復制技術, 當有新元素需要加入時, 先從原數組拷貝一份出來。然后在新數組里面加鎖添加, 添加之后, 將原來數組的引用指向新數組。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 加鎖
try { Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// 引用指向更改
setArray(newElements);
return true;
} finally { lock.unlock(); // 釋放鎖
}
}
從上面的源碼中得到 CopyOnWriteArrayList 的 add 操作是在加鎖的保護下完成的。加鎖是為了多線程對 CopyOnWriteArrayList 并發 add 時, 復制多個副本, 把數據搞亂。
public E get(int index) { return get(getArray(), index);
}
以上代碼顯示 get 是沒有加鎖的
如果出現并發 get, 會有以下 3 中情況。
如果寫操作未完成,那么直接讀取原數組的數據;
如果寫操作完成,但是引用還未指向新數組,那么也是讀取原數組數據;
如果寫操作完成,并且引用已經指向了新的數組,那么直接從新數組中讀取數據。
CopyOnWriteArrayList 多線程代碼演示。
package com.rumenz.task;
import java.util.List;
import java.util.concurrent.*;
public static Integer clientTotal=5000;
public static Integer threadTotal=200;
private static List integer list=new CopyOnWriteArrayList();
public static void main(String[] args) throws Exception{ ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore=new Semaphore(threadTotal);
final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
for (int i = 0; i clientTotal; i++) {
final Integer j=i;
executorService.execute(()- {
try{ semaphore.acquire();
update(j);
semaphore.release();
}catch (Exception e){ e.printStackTrace();
}
countDownLatch.countDown();
});
private static void update(Integer j) { list.add(j);
}
//size:5000
CopyOnWriteArrayList 使用場景
由于在 add 的時候需要拷貝原數組, 如果原數組內容比較多, 比較大, 可能會導致 young gc 和 full gc。
不能用于實時讀的場景, 像拷貝數組, 新增元素都需要時間, 所以調用 get 操作后, 有可能得到的數據是舊數據, 雖然 CopyOnWriteArrayList 能做到最終一致性, 但是沒有辦法滿足實時性要求。
CopyOnWriteArrayList 適合讀多寫少的場景, 比如白名單,黑名單等場景
CopyOnWriteArrayList 由于 add 時需要復制數組, 所以不適用高性能的互聯網的應用。
CopyOnWriteArraySet 介紹
public CopyOnWriteArraySet() { al = new CopyOnWriteArrayList e}
CopyOnWriteArraySet 底層是用 CopyOnWriteArraySet 來實現的。可變操作 (add,set,remove 等) 都需要拷貝原數組進行操作, 一般開銷很大。迭代器支持 hasNext(),netx()等不可變操作, 不支持可變的 remove 操作, 使用迭代器速度很快, 并且不會與其它線程沖突, 在構造迭代器時, 依賴不變的數組快照。
CopyOnWriteArraySet 多線代碼演示
package com.rumenz.task;
import java.util.List;
import java.util.Set;
import java.util.concurrent.*;
public static Integer clientTotal=5000;
public static Integer threadTotal=200;
private static Set integer set=new CopyOnWriteArraySet();
public static void main(String[] args) throws Exception{ ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore=new Semaphore(threadTotal);
final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
for (int i = 0; i clientTotal; i++) {
final Integer j=i;
executorService.execute(()- {
try{ semaphore.acquire();
update(j);
semaphore.release();
}catch (Exception e){ e.printStackTrace();
}
countDownLatch.countDown();
});
private static void update(Integer j) { set.add(j);
}
//size:5000
CopyOnWriteArraySet 使用場景
適用于 set 大小一般很小,讀操作遠遠多于寫操作的場景
ConcurrentSkipListSet
public ConcurrentSkipListSet() { m = new ConcurrentSkipListMap e,object}
ConcurrentSkipListSet e 是 jdk6 新增的類, 支持自然排序, 位于 java.util.concurrent。ConcurrentSkipListSet e 都是基于 Map 集合的, 底層由 ConcurrentSkipListMap 實現。
在多線程環境下,ConcurrentSkipListSet e 的 add,remove,contains 是線程安全的。但是對于批量操作 addAll,removeAll,containsAll 并不能保證原子操作, 所以是線程不安全的, 原因是 addAll,removeAll,containsAll 底層調用的還是 add,remove,contains 方法, 在批量操作時, 只能保證每一次的 add,remove,contains 是原子性的(即在進行 add,remove,contains, 不會被其它線程打斷), 而不能保證每一次批量操作都不會被其它線程打斷, 因此在 addAll、removeAll、retainAll 和 containsAll 操作時,需要添加額外的同步操作。
public boolean addAll(Collection !--? extends E-- c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
public boolean removeAll(Collection !--?-- c) { Objects.requireNonNull(c);
boolean modified = false;
Iterator !--?-- it = iterator();
while (it.hasNext()) { if (c.contains(it.next())) { it.remove();
modified = true;
}
}
return modified;
public boolean containsAll(Collection !--?-- c) { for (Object e : c)
if (!contains(e))
return false;
return true;
}
ConcurrentSkipListSet 代碼演示
package com.rumenz.task;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.*;
public static Integer clientTotal=5000;
public static Integer threadTotal=200;
private static Set integer set= new ConcurrentSkipListSet();
public static void main(String[] args) throws Exception{ ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore=new Semaphore(threadTotal);
final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
for (int i = 0; i clientTotal; i++) {
final Integer j=i;
executorService.execute(()- {
try{ semaphore.acquire();
update(j);
semaphore.release();
}catch (Exception e){ e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(size: +set.size());
}
private static void update(Integer r) { set.add(r);
}
//size:5000
ConcurrentHashMap
ConcurrentHashMap 中 key 和 value 都不允許為 null,ConcurrentHashMap 針對讀操作做了大量的優化。在高并發場景很有優勢。
在多線程環境下, 使用 HashMap 進行 put 操作會引起死循環, 導致 CPU 利用率到 100%, 所以在多線程環境不能隨意使用 HashMap。原因分析:HashMap 在進行 put 的時候, 插入的元素超過了容量就會發生 rehash 擴容, 這個操作會把原來的元素 hash 到新的擴容新的數組, 在多線程情況下, 如果此時有其它線程在進行 put 操作, 如果 Hash 值相同, 可能出現在同一數組下用鏈表表示, 造成閉環, 導致 get 的時候出現死循環, 所以是線程不安全的。
HashTable 它是線程安全的, 它涉及到多線程的操作都 synchronized 關鍵字來鎖住整個 table, 這就意味著所有的線程都在競爭同一把鎖, 在多線程環境下是安全的, 但是效率很低。
HashTable 有很多的優化空間,鎖住整個 table 這么粗暴的方法可以變相的柔和點,比如在多線程的環境下,對不同的數據集進行操作時其實根本就不需要去競爭一個鎖,因為他們不同 hash 值,不會因為 rehash 造成線程不安全,所以互不影響,這就是鎖分離技術,將鎖的粒度降低,利用多個鎖來控制多個小的 table,多線程訪問容器里不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效的提高并發訪問效率,這就是 ConcurrentHashMapJDK1.7 版本的核心思想。
ConcurrentHashMap 代碼演示案例
package com.rumenz.task;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;
public static Integer clientTotal=5000;
public static Integer threadTotal=200;
private static Map integer,integer map=new ConcurrentHashMap integer,integer
public static void main(String[] args) throws Exception{ ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore=new Semaphore(threadTotal);
final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
for (int i = 0; i clientTotal; i++) {
final Integer j=i;
executorService.execute(()- {
try{ semaphore.acquire();
update(j);
semaphore.release();
}catch (Exception e){ e.printStackTrace();
}
countDownLatch.countDown();
});
}
countDownLatch.await();
executorService.shutdown();
System.out.println(size: +map.size());
}
private static void update(Integer j) { map.put(j, j);
}
//size:5000
ConcurrentSkipListMap
ConcurrentSkipListMap 內部使用 SkipList 結構實現。跳表是一個鏈表, 但是通過跳躍式的查找方式使得插入, 讀取數據時的時間復雜度變成 O(log n)。
跳表(SkipList): 使用空間換時間的算法, 令鏈表的每個結點不僅記錄 next 結點位置,還可以按照 level 層級分別記錄后繼第 level 個結點。
ConcurrentSkipListMap 代碼案例
package com.rumenz.task;
import java.util.Map;
import java.util.concurrent.*;
public static Integer clientTotal=5000;
public static Integer threadTotal=200;
private static Map integer,integer map=new ConcurrentSkipListMap ();
public static void main(String[] args) throws Exception{ ExecutorService executorService = Executors.newCachedThreadPool();
final Semaphore semaphore=new Semaphore(threadTotal);
final CountDownLatch countDownLatch=new CountDownLatch(clientTotal);
for (int i = 0; i clientTotal; i++) {
final Integer j=i;
executorService.execute(()- {
try{ semaphore.acquire();
update(j);
semaphore.release();
}catch (Exception e){ e.printStackTrace();
}
countDownLatch.countDown();
});
private static void update(Integer j) { map.put(j, j);
}
//size:5000
ConcurrentHashMap 與 ConcurrentSkipListMap 的對比
ConcurrentHashMap 比 ConcurrentSkipListMap 性能要好一些。
ConcurrentSkipListMap 的 key 是有序的,ConcurrentHashMap 做不到。
ConcurrentSkipListMap 支持高并發, 它的時間復雜度是 log(N), 和線程數無關, 也就是說任務一定的情況下, 并發的線程越多,ConcurrentSkipListMap 的優勢就越能體現出來。
上述內容就是如何理解 Java 并發容器 J.U.C,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注丸趣 TV 行業資訊頻道。