1.概述

在日常开发中,ArrayList、Set在大量的场景下使用,然而我们都知道ArrayList和Set是线程不安全的,那么为什么ArrayList和Set是线程不安全的?如何保证在高并发场景下其线程安全性呢?本文将通过以下案例来剖析ArrayList、Set线程不安全的原因及现象,以及避免线程不安全的几种方案,并通过分析源码来阐述为什么这些方案能保证线程安全。

2.测试及解决方案

2.1 关于ArrayList的测试代码

public class ArrayListTest {
    public static void main(String[] args) {
        List<String> strings = Arrays.asList("1", "2", "3");
        List<String> lists = new ArrayList<>();
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                String value = UUID.randomUUID().toString().substring(0,5);
                lists.add(value);
                System.out.println(lists);
            },String.valueOf(i)).start();
        }
    }
}

2.2 测试结果

上述代码可能会出现以下并发修改异常,具体如下图所示:
在这里插入图片描述

2.3 分析原因

首先来看一下ArrayList的add方法底层源码(基于jdk1.8):

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

这段代码在高并发下大概率会出现问题的,同一时刻多个线程同时进入执行,其中一个线程add正常,其它线程必然无法正常add,因此就会异常。

2.4 解决方案

2.4.1 解决方案一

将上述代码中: List<String> lists = new ArrayList<>();
改为: List<String> lists = new Vector<>();

为什么利用Vector代替ArrayList生成的对象就能避免上述问题呢?首先来看下Vector和ArrayList的关系图,具体如下所示:
在这里插入图片描述
由上述类图可知,Vector和ArrayList均继承于AbstractList,且底层都是通过数组去实现对元素的快速存取。数组的缺点显而易见,可以快速获取指定位置的数据,适合遍历,但在数据存储时,效率较低。再来看一下Vector的add方法,具体如下:

   public synchronized void addElement(E obj) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = obj;
    }

这两个add方法的实现大同小异,区别在于synchronized 关键字的使用,synchronized 的作用毋庸置疑,保证某一时刻只能有一个线程执行该代码块,因此Vector是线程安全的。其实通过查看Vector与ArrayList的源码可以发现,两个类大部分关键方法都是类似的,区别在于Vector在类似的方法上都加了关键字synchronized来保证线程安全。
这种解决方案的缺陷是:
会降低线程执行效率(肯定的麦,小猪都知道,本来是一下就能执行,这要排队执行,那效率能不低),还有就是加锁和释放锁的这个过程,在系统中是有开销的,因此Vector效率相对较低。

2.4.2 解决方案二

将上述代码中:List<String> lists = new ArrayList<>();
替换为:List<String> strings = Collections.synchronizedList(new ArrayList<>());

接下来分析一下为什么通过这中种方案就可以变得线程安全呢?首先我们来看下synchronizedList这个部分的源码:

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

由上述源码可以看出这是一个静态方法,通过传入的list类型来生成SynchronizedRandomAccessList还是SynchronizedList,接下来再看一下SynchronizedList的源码:

static class SynchronizedList<E>
        extends SynchronizedCollection<E>
        implements List<E> {
        private static final long serialVersionUID = -7754090372962971524L;

        final List<E> list;

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

        public boolean equals(Object o) {
            if (this == o)
                return true;
            synchronized (mutex) {return list.equals(o);}
        }
        public int hashCode() {
            synchronized (mutex) {return list.hashCode();}
        }

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

        public int indexOf(Object o) {
            synchronized (mutex) {return list.indexOf(o);}
        }
        public int lastIndexOf(Object o) {
            synchronized (mutex) {return list.lastIndexOf(o);}
        }

        public boolean addAll(int index, Collection<? extends E> c) {
            synchronized (mutex) {return list.addAll(index, c);}
        }

        public ListIterator<E> listIterator() {
            return list.listIterator(); // Must be manually synched by user
        }

        public ListIterator<E> listIterator(int index) {
            return list.listIterator(index); // Must be manually synched by user
        }

        public List<E> subList(int fromIndex, int toIndex) {
            synchronized (mutex) {
                return new SynchronizedList<>(list.subList(fromIndex, toIndex),
                                            mutex);
            }
        }

        @Override
        public void replaceAll(UnaryOperator<E> operator) {
            synchronized (mutex) {list.replaceAll(operator);}
        }
        @Override
        public void sort(Comparator<? super E> c) {
            synchronized (mutex) {list.sort(c);}
        }

        /**
         * SynchronizedRandomAccessList instances are serialized as
         * SynchronizedList instances to allow them to be deserialized
         * in pre-1.4 JREs (which do not have SynchronizedRandomAccessList).
         * This method inverts the transformation.  As a beneficial
         * side-effect, it also grafts the RandomAccess marker onto
         * SynchronizedList instances that were serialized in pre-1.4 JREs.
         *
         * Note: Unfortunately, SynchronizedRandomAccessList instances
         * serialized in 1.4.1 and deserialized in 1.4 will become
         * SynchronizedList instances, as this method was missing in 1.4.
         */
        private Object readResolve() {
            return (list instanceof RandomAccess
                    ? new SynchronizedRandomAccessList<>(list)
                    : this);
        }
    }

由上述源码可知,在其关键的get、set、remove等方法上,都有关键字synchronized 修饰,因此是线程安全的。
这种方案的缺陷:
与方案一类似,效率较低。

2.4.2 解决方案三

将上述代码:List<String> lists = new ArrayList<>();
改为:List<String> lists = new CopyOnWriteArrayList<>();

举一反三(小猪都会了),直接来看CopyOnWriteArrayList种add方法的源码:

 /** The array, accessed only via getArray/setArray. */
 private transient volatile Object[] array;
 
 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();
        }
    }
     final void setArray(Object[] a) {
        array = a;
    }

由上述add方法可知,执行add方法时,首先会加lock锁,再将原数组中的元素copy一份,然后在新copy的数组上将元素添加进去,最后切换引用,将新数组赋给底层元数组。这是一种典型的读写分离策略,在日常生产中,大部分场景都是读多写少,CopyOnWriteArrayList容器允许并发读,读操作是无锁的,性能较高。至于写操作,比如向容器中添加一个元素,则先将当前容器复制一份,然后在新副本上执行写操作,结束之后再将原容器的引用指向新容器。所以synchronized保证线程安全,里面的数据安全则交由volatile。
这种方案的优势在于:
读操作效率很高,因为没有任何同步策略,同时由于其读写分离策略,遍历和修改分别在不同的数组容器中,因而不会抛出ConcurrentModificationException异常。
这种方案的缺陷在于:
缺陷在于会占用大量的内存,因为每次读写操作都需要将元数组中元素拷贝一份,数据量大时,会对内存造成较大压力,可能会引起频繁GC;另一点无法保证读写一致性,由于其读写操作是不同的元素组,进行写操作时,可能读取到未写之前的原数组。

2.5 Set案例

2.5.1 关于Set测试

Set与List类似,由于Set与List同属于集合Collection的子类,具体类图如下图所示:
在这里插入图片描述
List与Set的主要区别如下:

1. List和Set之间很重要的一个区别是是否允许重复元素的存在,在List中允许插入重复的元素,而在Set中不允许重复元素存在;
2. 与元素先后存放顺序有关,List是有序集合,会保留元素插入时的顺序,Set是无序集合;
3. List可以通过下标来访问,而Set不能。

测试代码类似,如下:

public class SetTest {
    public static void main(String[] args) {
        HashSet<String> objects = new HashSet<>();
        for (int i = 1; i <= 100; i++) {
            new Thread(() -> {
                objects.add(UUID.randomUUID().toString().substring(0, 5));
                System.out.println(objects);
            }).start();
        }
    }
}

测试结果也类似,如下:
在这里插入图片描述

2.5.2 解决方案

1.方案一

将上述代码中:HashSet<String> objects = new HashSet<>();
修改为:Set<String> objects = Collections.synchronizedSet(new HashSet<>());

原理与synchronizedList类似,可自行查看源码进行分析,这里不再赘述。
2.方案二

将上述代码中:HashSet<String> objects = new HashSet<>();
修改为:Set<String> objects = new CopyOnWriteArraySet<>();

同理可证(小猪都会了),具体可以自行查看源码分析。

3.小结

1.List和Set之间很重要的一个区别是是否允许重复元素的存在,List允许重复元素出现,Set不允许;
2.List和Set都是线程不安全的,通过集合类Collection提供的方法或JUC中提供的方法,能够避免线程不安全问题;
3.每种方案都有利有弊,要根据场景来选择合适的技术,没有银弹。

4.参考文献

1.https://juejin.cn/post/6899330436468899847
2.https://www.cnblogs.com/chengxiao/p/6881974.html
3.https://www.bilibili.com/video/BV1B7411L7tE?p=11&spm_id_from=pageDriver

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐