1.集合类不安全之并发修改异常

2.集合类不安全之加锁和写时复制

3.集合类不安全之SET

4.集合类不安全之MAP

1.集合类不安全之并发修改异常
本次我们来讲解集合类线程不安全操作的问题,我们先来看一看一般情况下,我们是如何操作集合类的:

        List<String> list = new ArrayList<>();;
        list.add("a");
        list.add("b");
        list.add("c");

一般来说,没有什么复杂的高并发业务逻辑场景的话,我们都是简单地对list进行add操作,但是,在高并发场景下呢?接下来我们模拟一下多线程操作一个list,会出现什么样的情况:

       //创建了三千个线程,对一个list进行随机增加字符串
        List<String> list = new ArrayList<>();;
        for (int i = 0; i < 3000; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }

运行一下,结果报了异常:
!image.png
java.util.ConcurrentModificationException
显示JAVA并发修改异常,这是因为多线程同时在操作一个对象
(举个例子,一个班有三千名学生,同时在往一签到表上写名字进行签到,但是如果不遵循先来后到原则的话,就会发生哄抢现象,签到表会坏掉。)

2.集合类不安全之加锁和写时复制
此时,有两个解决办法:

方法一:
List<String> list = Collections.synchronizedList(new ArrayList<>());

我们先来尝试一下:

        List<String> list = Collections.synchronizedList(new ArrayList<>());
        for (int i = 0; i < 3000; i++) {
            new Thread(() -> {
                list.add(UUID.randomUUID().toString().substring(0, 8));
                System.out.println(list);
            }, String.valueOf(i)).start();
        }

运行结果没有问题:

image.png

为什么使用Collections.synchronizedList()方法我们就可以将这个list变为线程安全的list呢,我们来看一下源码:

    public static <T> List<T> synchronizedList(List<T> list) {
        return (list instanceof RandomAccess ?
                new SynchronizedRandomAccessList<>(list) :
                new SynchronizedList<>(list));
    }

把ArrayList类传进去后,会进行类型判断,因为ArrayList实现了RandomAccess接口(是一个标志接口,表明实现这个这个接口的 List 集合是支持快速随机访问的,具体我们不展开),所以会调用

new SynchronizedRandomAccessList<>(list)

然后就会调用

        SynchronizedList(List<E> list) {
            super(list);
            this.list = list;
        }

此时,list是一个final对象

final List<E> list;

final就代表这个list被赋值一次就不能再被赋值了,但是list里的内容还是能够add和remove的。

接下来,我们看一下最重要的方法add:

        public void add(int index, E element) {
            synchronized (mutex) {list.add(index, element);}
        }

mutex是一个互斥变量,学过操作系统的人都知道,谁拥有mutex,谁就能进入这个方法进行操作,而别的线程只能在外面等待。

Collections.synchronizedList(new ArrayList<>());

上面这个类对list的方法进行了再封装,进行同步的add操作。

方法二:

List<String> list = new CopyOnWriteArrayList<>()

他的add方法在多线程的情况下也是不会报错的,在这里我们就不演示了,接下来我们来看一下源码,首先是构造方法:

new CopyOnWriteArrayList<>()

点进去之后

    public CopyOnWriteArrayList() {
        setArray(new Object[0]);
      }
    final void setArray(Object[] a) {
        array = a;
    }
   private transient volatile Object[] array;

他会先设置一个长度为0的object数组,并且这个array是volatile的Object数组,注意一下volatile的特性(可见性,禁止指令重排,但是不保证原子性)。

接下来我们看一下,CopyOnWriteArrayList最重要的add方法是怎么保证线程安全的。

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
           // 先获取现在的数组对象
            Object[] elements = getArray();
            int len = elements.length;
            //将现在的数组对象进行扩容+1后,取的新的数组对象
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //将新元素放在数组最后的位置上
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

由此源码,我们可以看到,add方法先后进行了
复制->扩容->放入最后一个位置 这三个操作。
并且还使用了加锁机制,每次只能有一个线程能进入这个方法。
还有volatile关键字:
每次使用array数组的时候,确保是从内存中取到的最新的数值。

3.集合类不安全之SET
因为list,set,map都是线程不安全的,多线程操作都会引起异常,所以我们就不演示了。
创建集合安全的类,有两种方法

//这种方式的add方法和synchronizedList方式并没有区别
Set<String> set = Collections.synchronizedSet(new HashSet<>());
//我们重点看第二种
Set<String> set = new CopyOnWriteArraySet();

我们先看构造方法

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

可见CopyOnWriteArraySet的底层是arrayList(这里不要被我误导了,HashSet的底层是HashMap,只不过value都是Object而已,只是我们现在看到的CopyOnWriteArraySet,底层是ArrayList)

我们来看一下最重要的add方法:

    public boolean add(E e) {
        return al.addIfAbsent(e);
    }
    /**
     * Appends the element, if not present.
     *
     * @param e element to be added to this list, if absent
     * @return {@code true} if the element was added
     */
    public boolean addIfAbsent(E e) {
        Object[] snapshot = getArray();
        return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
            addIfAbsent(e, snapshot);
    }

我们可以看到注解说,如果没有这个元素,就添加,否则就不添加。
indexOf是判断这个元素是否在set内存在并返回索引的,如果大于等于0就不存在,接着会调用addIfAbsent方法:

    /**
     * A version of addIfAbsent using the strong hint that given
     * recent snapshot does not contain e.
     */
    private boolean addIfAbsent(E e, Object[] snapshot) {
        //加锁操作
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] current = getArray();
            int len = current.length;
            //如果传进来的值和本地数组引用不是一个地址
            //我们就要进行比对,如果发现了未添加的元素已经在数组里了,就返回false
            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();
        }
    }

我们由此看出,set的add方法,也是用了扩容+写时复制的方法了。

4.集合类不安全之MAP
最后我们来看一下map

map的线程安全类为:

        Map<String,String> map = new ConcurrentHashMap<>();

concurrentHashMap采用的原理是cas,具体我会另外开一篇文章,因为很冗长,就不在这儿赘述了。

总结:
并发情况下操作一个数组会出现异常,我们大部分都会采用加锁和写时复制进行解决。


苏凌峰
73 声望38 粉丝

你的迷惑在于想得太多而书读的太少。