Java 集合類的線程安全問題及解決方法


一、List

1.1 模擬多線程環境

       多線程環境下,會拋出 java.util.ConcurrentModificationException 異常

  1 public static void listNotSafe() {
  2     List<String> list = new CopyOnWriteArrayList<>();
  3 
  4     for (int i = 0; i < 30; i++) {
  5         new Thread(() -> {
  6             list.add(UUID.randomUUID().toString().substring(0, 8));
  7             System.out.println(list);
  8         }).start();
  9     }
 10 }
image

1.2 異常原因

   多線程環境下,並發爭搶修改導致出現該異常。

1.3 解決辦法

  1 // 1. 使用線程安全類 Vector
  2 new Vector();
  3 
  4 // 2. 使用 Collections 工具類封裝 ArrayList
  5 Collections.synchronizedList(new ArrayList<>());
  6 
  7 // 3. 使用 java.util.concurrent.CopyOnWriteArrayList;
  8 new CopyOnWriteArrayList<>();

1.4 寫時復制思想

   CopyOnWrite 容器即寫時復制的容器。往一個容器添加元素的時候,不直接往當前容器Object[]添加,而是先將當前容器Object[]進行Copy, 復制出一個新的容器Object[] newElements, 然后新的容器Object[] newElements 里添加元素,添加完元素之后,再將原容器的引用指向新的容器 setArray(newElements); 這樣做的好處是可以對CopyOnWrite容器進行並發的讀,而不需要加鎖,因為當前容器不會添加任何元素。所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。
  1 // CopyOnWriteArrayList.java
  2 public boolean add(E e) {
  3     final ReentrantLock lock = this.lock;
  4     lock.lock();
  5     try {
  6         Object[] elements = getArray();
  7         int len = elements.length;
  8         Object[] newElements = Arrays.copyOf(elements, len + 1);
  9         newElements[len] = e;
 10         setArray(newElements);
 11         return true;
 12     } finally {
 13         lock.unlock();
 14     }
 15 }

二、Set

2.1 線程安全問題

       與 List 接口的測試方法相似,同樣會拋出 java.util.ConcurrentModificationException 異常。

2.2 解決辦法

  1 // 1. 使用 Collections 工具類封裝
  2 Collections.synchronizedSet(new HashSet<>());
  3 
  4 // 2. 使用 java.util.concurrent.CopyOnWriteArraySet;
  5 new CopyOnWriteArraySet<>();

2.3 CopyOnWriteArraySet

       final ReentrantLock lock = this.lock; 為什么聲明為 final?參考可以看看這個 https://blog.csdn.net/zqz_zqz/article/details/79438502

  1 // 底層實際上是一個 CopyOnWriteArrayList
  2 public class CopyOnWriteArraySet<E> extends AbstractSet<E>
  3         implements java.io.Serializable {
  4     private static final long serialVersionUID = 5457747651344034263L;
  5 
  6     private final CopyOnWriteArrayList<E> al;
  7 
  8     // ...
  9 }
  1 // 添加元素,相當於調用 CopyOnWriteArrayList 的 addIfAbsent() 方法
  2 public class CopyOnWriteArraySet<E> {
  3     public boolean add(E e) {
  4         return al.addIfAbsent(e);
  5     }
  6 }
  7 
  8 /**
 9  * CopyOnWriteArrayList 的 addIfAbsent() 方法
 10  * Set 集合中的元素不可重復,如果原集合中有要添加的元素,則直接返回 false
 11  * 否則,將該元素加入集合中
 12  */
 13 public boolean addIfAbsent(E e) {
 14     Object[] snapshot = getArray();
 15     return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
 16         addIfAbsent(e, snapshot);
 17 }
 18 
 19 /**
 20  * 重載的 addIfAbsent() 方法,用於真正添加元素加鎖后,再次獲取集合,與剛才拿到的集合比較,
 21  * 兩次拿到的不一樣,說明集合被其他線程修改過了,重新比較最新集合中有沒有該元素,如果比較
 22  * 后,沒有返回 false,說明沒有該元素,執行下面的添加方法。
 23  */
 24 private boolean addIfAbsent(E e, Object[] snapshot) {
 25     final ReentrantLock lock = this.lock;
 26     lock.lock();
 27     try {
 28         Object[] current = getArray();
 29         int len = current.length;
 30         if (snapshot != current) {
 31             // Optimize for lost race to another addXXX operation
 32             int common = Math.min(snapshot.length, len);
 33             for (int i = 0; i < common; i++)
 34                 if (current[i] != snapshot[i] && eq(e, current[i]))
 35                     return false;
 36             if (indexOf(e, current, common, len) >= 0)
 37                 return false;
 38         }
 39         Object[] newElements = Arrays.copyOf(current, len + 1);
 40         newElements[len] = e;
 41         setArray(newElements);
 42         return true;
 43     } finally {
 44         lock.unlock();
 45     }
 46 }

三、Map

3.1 線程安全問題

       和上面一樣,多線程環境下,會拋出 java.util.ConcurrentModificationException 異常。

3.2 解決辦法

  1 // 使用 Collections 工具類
  2 Collections.synchronizedMap(new HashMap<>());
  3 
  4 // 使用 ConcurrentHashMap
  5 new ConcurrentHashMap<>();

3.3 HashMap、Hashtable 和 ConcurrentHashMap 的區別

繼承不同:HashMap繼承AbstractMap, Hashtable繼承Dictonary,ConcurrentHashMap除了繼承AbstractMap還實現了ConcurrentMap接口

線程是否安全:HashMap非線程安全,ConcurrentHashMap 和 Hashtable 線程安全,但是他們的實現機制不同,Hashtabl e使用synchronized實現同步方法,而ConcurrentHashMap降低鎖的粒度,擁有更好的並發性能。

Key-Value值:ConcurrentHashMap和Hashtable都不允許value和key為null,但是HashMap允許唯一的key為null,和任意個value為null

哈希算法不同:HashMap 和 Jdk 8 中的 ConcurrentHashMap 的算法一致都是使用 key 的 hashcode 值進行高16位和低16位異或再取模長度,而Hashtable是直接對 key 的hashcode值進行取模操作 。

擴容機制不同:ConcurrentHashMap和HashMap的擴容機制和初始容量一致,擴容為原有數組長度的兩倍,初始容量為16,但是hashtable中的初始容量為11,容量為原有長度的兩倍+1。

失敗機制:ConcurrentHashMap支持安全失敗,HashMap和hashtable支持的快速失敗

查詢方法:HashMap沒有contains方法,但是擁有containsKey和containsValue方法,Hashtable和ConcurrentHashMap還支持contains方法

迭代方式:ConcurrentHashMap和Hashtable還支持Enumeration迭代方式


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM