乐趣区

关于java:JAVA并发编程集合类不安全问题以及源码解析

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();}

运行一下,后果报了异样:
!
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();}

运行后果没有问题:

为什么应用 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,具体我会另外开一篇文章,因为很简短,就不在这儿赘述了。

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

退出移动版