线程不安全集合类及源码剖析:

常用的线程不安全集合:
ArrayList
LinkedList
ArraySet
HashMap

不安全集合之List

python线程不安全的变量 线程不安全集合_java

1.ArrayList

举一个List线程不安全的例子: 开10个线程对List进行添加并访问。

public static void main(String[] args) {
    //新建一个ArrayList集合 
    List<String> strings = new ArrayList<>();
    for (int i = 1; i <= 10; i++) {
        new Thread(() -> {
            strings.add(UUID.randomUUID().toString().substring(0, 8));
            System.out.println(Thread.currentThread().getName()+":"+strings);
        },String.valueOf(i)).start();
    }
}

我们会发现,后台会报出如下异常:Exception in thread "14" java.util.ConcurrentModificationException(并发修改异常),当方法检测到对象的并发修改,但不允许这种修改时,就会抛出此异常。

我们来看ArrayList类 add方法的源码:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!(自动扩容)
    elementData[size++] = e;
    return true;
}

我们可以看到,ArrayList的底层维护了一个Obj类型的数组,它首先更新的数组的长度,然后将参数赋给指定的数组。其并没有加锁,因此是线程不安全的。

举个例子:

比如当A线程在操作数据时,B线程突然进来,将A线程要添加的位置占用,后续A线程将此数据(B线程的数据)修改为自己的数据这时就会抛出异常(java.util.ConcurrentModificationException)

2.LinkedList

我们来看LinkedList的add方法源码:

public boolean add(E e) {
        linkLast(e);
        return true;
    }
    /**
     * Links e as last element.
     */
    void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

LinkedList 底层是基于双向链表实现的,也是实现了 List 接口,所以也拥有 List 的一些特点 ,可见每次插入都是移动指针而且没有加锁,因此LinkedList 为线程不安全集合。

3.解决办法

  1. 使用线程安全的Vector()类 List list = new Vector() ,Vector的add方法使用了synchronized锁。因为加了锁,所以导致效率低。
public synchronized void addElement(E obj) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = obj;
}
  1. 使用Collections的synchronizedList方法。Collections.synchronizedList(new ArrayList<>()); 它是集合框架的工具类,从方法名来看,就是创建一个线程安全的arraylist。
List<String> list = new CopyOnWriteArrayList<>();

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

// 源码
public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    
    try{
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyof(element, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
  1. 使用JUC包下的CopyOnWriteArrayList,它也是大厂经常使用的并发类之一。
    CopyOnWrite 容器即写时复制容器。往一个容器添加元素时,不直接往当前容器Object[] 添加,而是先将当前容器Object[] 进行Copy, 复制出一个新容器 Object[] newElements, 然后新的容器 Object[] newElements
    里添加元素,添加完元素之后,再将原容器的引用指向新的容器 setArray(newElements); 这样做的好处是可以对CopyOnWrite 容器进行并发读, 而不需要加锁,因为当前容器不会添加任何元素。 所以CopyOnWrite 容器也是一种读写分离的思想,读和写不同容器。

不安全线程之Set,Map

Set<String> set = new HashSet<>();

//HashSet()底层用的是HashMap
//源码:
    public HashSet() {
        map = new HashMap<>();
    }

我们将HashSet HashMap 做上面同样的实验会发现,都会抛出并发修改异常。

解决方案:

Set集合的解决方案:

  1. Collections.synchronizedSet(new HashSet()) ;
  2. new CopyOnWriteArraySet();

Map集合的解决方案:

  1. 使用HashTable,源码如下,我们发现其put方法使用了synchronized锁。
public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }

    addEntry(hash, key, value, index);
    return null;
}
  1. 使用ConcurrentHashMap(线程安全)
    Map<String,String> maps = new ConcurrentHashMap<String, String>()