Java 集合类的线程安全问题及解决方法

73 阅读3分钟

一、List

1.1 模拟多线程环境

多线程环境下,会抛出 java.util.ConcurrentModificationException 异常

public static void listNotSafe() {
    List<String> list = new CopyOnWriteArrayList<>();

    for (int i = 0; i < 30; i++) {
        new Thread(() -> {
            list.add(UUID.randomUUID().toString().substring(0, 8));
            System.out.println(list);
        }).start();
    }
}

image.png

1.2 异常原因

多线程环境下,并发争抢修改导致出现该异常。

1.3 解决办法

// 1. 使用线程安全类 Vector
new Vector();

// 2. 使用 Collections 工具类封装 ArrayList
Collections.synchronizedList(new ArrayList<>());

// 3. 使用 java.util.concurrent.CopyOnWriteArrayList;
new CopyOnWriteArrayList<>();

1.4 写时复制思想

CopyOnWrite 容器即写时复制的容器。往一个容器添加元素的时候,不直接往当前容器Object[]添加,而是先将当前容器Object[]进行Copy, 复制出一个新的容器Object[] newElements, 然后新的容器Object[] newElements 里添加元素,添加完元素之后,再将原容器的引用指向新的容器 setArray(newElements); 这样做的好处是可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

// CopyOnWriteArrayList.java
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();
    }
}

二、Set

2.1 线程安全问题

与 List 接口的测试方法相似,同样会抛出 java.util.ConcurrentModificationException 异常。

2.2 解决办法

// 1. 使用 Collections 工具类封装
Collections.synchronizedSet(new HashSet<>());
 
// 2. 使用 java.util.concurrent.CopyOnWriteArraySet;
new CopyOnWriteArraySet<>();

2.3 CopyOnWriteArraySet

final ReentrantLock lock = this.lock; 为什么声明为 final?参考可以看看这个 blog.csdn.net/zqz_zqz/art…

// 底层实际上是一个 CopyOnWriteArrayList
public class CopyOnWriteArraySet<E> extends AbstractSet<E>
        implements java.io.Serializable {
    private static final long serialVersionUID = 5457747651344034263L;

    private final CopyOnWriteArrayList<E> al;

    // ...
}
// 添加元素,相当于调用 CopyOnWriteArrayList 的 addIfAbsent() 方法
public class CopyOnWriteArraySet<E> {
    public boolean add(E e) {
        return al.addIfAbsent(e);
    }
}

/**
 * CopyOnWriteArrayList 的 addIfAbsent() 方法
 * Set 集合中的元素不可重复,如果原集合中有要添加的元素,则直接返回 false
 * 否则,将该元素加入集合中
 */
public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
        addIfAbsent(e, snapshot);
}

/**
 * 重载的 addIfAbsent() 方法,用于真正添加元素加锁后,再次获取集合,与刚才拿到的集合比较,
 * 两次拿到的不一样,说明集合被其他线程修改过了,重新比较最新集合中有没有该元素,如果比较
 * 后,没有返回 false,说明没有该元素,执行下面的添加方法。
 */
private boolean addIfAbsent(E e, Object[] snapshot) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) {
            // Optimize for lost race to another addXXX operation
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;
            if (indexOf(e, current, common, len) >= 0)
                return false;
        }
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

三、Map

3.1 线程安全问题

和上面一样,多线程环境下,会抛出 java.util.ConcurrentModificationException 异常。

3.2 解决办法

// 使用 Collections 工具类
Collections.synchronizedMap(new HashMap<>());

// 使用 ConcurrentHashMap
new ConcurrentHashMap<>();

3.3 HashMap、Hashtable 和 ConcurrentHashMap 的区别

继承不同: HashMap继承AbstractMap, Hashtable继承DictonaryConcurrentHashMap除了继承AbstractMap还实现了ConcurrentMap接口

线程是否安全: HashMap非线程安全,ConcurrentHashMapHashtable 线程安全,但是他们的实现机制不同,Hashtable使用synchronized实现同步方法,而ConcurrentHashMap降低锁的粒度,拥有更好的并发性能。

Key-Value值: ConcurrentHashMapHashtable都不允许value和key为null,但是HashMap允许唯一的key为null,和任意个value为null

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

扩容机制不同: ConcurrentHashMapHashMap的扩容机制和初始容量一致,扩容为原有数组长度的两倍,初始容量为16,但是hashtable中的初始容量为11,容量为原有长度的两倍+1。

失败机制: ConcurrentHashMap支持安全失败,HashMaphashtable支持的快速失败

查询方法: HashMap没有contains方法,但是拥有containsKeycontainsValue方法,HashtableConcurrentHashMap还支持contains方法

迭代方式: ConcurrentHashMapHashtable还支持Enumeration迭代方式