集合类的线程安全

  • 为什么不是线程安全的
  • 出错原因
  • 二、如何保证线程安全
  • List的线程安全
  • CopyOnWriteArrayList
  • Set
  • Map


为什么不是线程安全的

我们都知道在java中,经常会用到三大集合类Set,List,Map。但是像ArrayList, HashMap,HashSet这些常用的集合类是线程不安全的。在高并发的场景下使用这些集合类会导致很多的问题,比如丢失数据,数据的不一致性等等,甚至导致异常,给生产环境带来严重的损失。

首先我们以List集合来举一个例子,来看看会导致的问题。

List<String> list = new ArrayList<>();
	// 3个线程向list集合中添加数据
    for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0,4));
                System.out.println(list);
            }).start();
        }

来看一下运行结果:

第一种运行结果
[null, e6bd]
[null, e6bd]
[null, e6bd]

第二种运行结果
[ce2f, 0c63, 3ce0]
[ce2f, 0c63, 3ce0]
[ce2f, 0c63, 3ce0]

第三种运行结果
[55fe, e03f]
[55fe, e03f]
[55fe, e03f]

可能会出现空的情况,也可能丢失数据,也有可能正常,这个只是3个线程同时访问,我们加大线程的数量到30(代码省略,只需将for循环的3改成30即可!),就会报错:java.util.ConcurrentModificationException

Exception in thread "Thread-2" Exception in thread "Thread-3" java.util.ConcurrentModificationException
	at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:911)
	at java.util.ArrayList$Itr.next(ArrayList.java:861)
	at java.util.AbstractCollection.toString(AbstractCollection.java:461)
	at java.lang.String.valueOf(String.java:2994)
	at java.io.PrintStream.println(PrintStream.java:821)
	at com.chunqiu.learn.Test1.lambda$main$0(Test1.java:20)
	at java.lang.Thread.run(Thread.java:748)

当然,Set和Map集合的线程不安全也是一样的,下边看一下两者线程不安全的体现。

Set:

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

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

Map:

Map<String, String> map = new HashMap<>();
	
    for (int i = 0; i < 3; i++) {
            new Thread(() -> {
            	String param = UUID.randomUUID().toString().substring(0,4);
                map.put(param, param);
                System.out.println(map);
            }).start();
        }

当然,报错都是java.util.ConcurrentModificationException。

出错原因

这个异常翻译成中文就是并发修改异常。为什么会报这样的一个异常呢?

首先先举一个生活中的例子:假如我们进考场签到,进考场之前,我们需要签到,每个人在花名册上签上自己的名字,假如张三正在签到,但是忘了自己的名字怎么写了,签的很慢,后边的李四等不及了,很嫌弃张三,就从张三的手中抢签到的笔,此时张三正在写字,这样就容易导致在争抢的过程中在花名册上留下一道长长的笔迹。这个由于多个线程同时在争抢添加操作所导致的异常就是我们的java.util.ConcurrentModificationException。

我们来看一下List中的添加方法。

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

ArrayList的add方法为了保证效率并没有加锁,无法保证线程安全。那么应该如何保证线程的安全性呢?

二、如何保证线程安全

List的线程安全

List保证线程安全有三种方式:

  1. 使用线程安全的子类:Vector。 Vector的方法使用synchronized修饰,进行了加锁,可以保证线程安全。
  2. 使用集合的工具类:Collections。Collections可以创建线程安全的集合类。Collections.synchronizedList(new ArrayList<>())
  3. 使用JUC包中的一个类:CopyOnWriteArrayList

CopyOnWriteArrayList

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

查看CopyOnWriteArrayList的源码,我们发现有两个变量:ReentrantLock类型的lock和volatile关键字修饰的数组对象array。 (volatile保证多线程对共享变量的可见性。)

/** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

我们看一下CopyOnWriteArrayList的add方法。

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

Set保证线程安全有两种方式:

  1. 使用集合的工具类:Collections。Collections可以创建线程安全的集合类。Collections.synchronizedSet(new HashSet<>())
  2. 使用JUC包中的一个类:CopyOnWriteArraySet

查看CopyOnWriteArraySet的源码发现,其实底层还是CopyOnWriteArrayList。

private final CopyOnWriteArrayList<E> al;

    /**
     * Creates an empty set.
     */
    public CopyOnWriteArraySet() {
        al = new CopyOnWriteArrayList<E>();
    }

在此呢多说一个面试题吧。
HashSet的底层是一个HashMap。 但是HashMap是K,V键值对的形式,HashSet只有一个,那HashSet的底层怎么是一个HashMap呢?
那就看一下源码:

private transient HashMap<E,Object> map;

    private static final Object PRESENT = new Object();

	/**
		构造方法: 
		构造一个新的空集; 后备HashMap实例具有默认初始容量 (16) 和负载因子 (0.75)。
	*/
 	public HashSet() {
        map = new HashMap<>();
 	}

	/**
		添加方法,发现添加的元素食作为map的key,value是一个固定的对象
	*/
 	public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

Map

Map保证线程安全有两种方式:

  1. 使用集合的工具类:Collections。Collections可以创建线程安全的集合类。Collections.synchronizedSet(new HashSet<>())
  2. 使用JUC包中的一个类:ConcurrentHashMap。ConcurrentHashMap的内容较多,后续会专门出一片文章进行讲解。