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 image](https://img2018.cnblogs.com/blog/1653712/201911/1653712-20191119160821711-655065525.png)
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迭代方式