【集合】Vector与CopyOnWriteArrayList

发布时间:2023年12月27日

前言:

? ? ? ? 此篇博客着重于:在多线程并发执行读、写操作的场景下Vector集合CopyOnWriteArrayList集合是否能保证线程安全?它们是通过什么方式保证线程安全的?

Vector:

? ? ? ? (1)add(E e)方法实现

public synchronized boolean add(E e) {
        //modCount:修改表结构的次数(增、删、改等操作都算修改了表结构)
        modCount++;
        //确保数组容量没有达到上限,达到上限则扩容
        ensureCapacityHelper(elementCount + 1);
        //将元素添加到Object数组
        elementData[elementCount++] = e;
        //返回指向结果
        return true;
    }

private void ensureCapacityHelper(int minCapacity) {
        // overflow-conscious code
        // 如果动态数组容量达到了上限
        if (minCapacity - elementData.length > 0)
            //扩容
            grow(minCapacity);
    }

private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

? ? ? ? (2)get(int index)方法实现

public synchronized E get(int index) {
        //如果索引越界,则直接抛出异常
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
        
        //否则通过索引返回对应元素
        return elementData(index);
    }


E elementData(int index) {
        //通过索引获取Object数组中的元素
        //将Object对象强转成泛型E返回
        return (E) elementData[index];
    }

? ? ? ? 总结

? ? ? ? 无论是add(E e)方法,还是get(int index)方法,方法声明上都有synchronized关键字,这意味着每次读、写操作都会对当前Vector对象上锁,保证同一时间并发的多个读、写线程是串行执行的,以此来确保多线程并发读、写时的线程安全。

? ? ? ? 图解

为什么并发读、写场景下,不上锁会有线程安全问题呢?

以get(int index)(读操作)、remove(int index)(写操作)这两个方法为切入点分析

? ? ? ? get(int index)方法可以分为两步1、判断索引是否越界;2、返回索引对应的元素。

? ? ? ? remove(int index)方法也可以分为两步1、从数组中移除指定数据;2、更新数组元素数量。

public synchronized E remove(int index) {
        modCount++;
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);
        E oldValue = elementData(index);

        int numMoved = elementCount - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--elementCount] = null; // Let gc do its work

        return oldValue;
    }

????????此时有一个get线程和一个remove线程同时来操作Vector对象,操作动态数组的最后一个元素,由于没有加锁,任何执行顺序都是有可能的。假设有如下图所示的执行顺序

? ? ? ? 我们预期的结果是get线程会报数组越界异常,但结果却是返回了一个null值,与我们想要的结果不符。写线程修改了Vector集合的结构后,我们期望读线程能感知到表结构的改变,所以线程安全问题实质上是数据一致性问题。

CopyOnWriteArrayList:

? ? ? ? 我们分析了Vector集合的add、get方法,知道了Vector集合多线程并发场景下,保证线程安全的原理:读、写操作都会对当前Vector集合对象上synchronized锁。但其实多线程并发执行读操作的场景是不会有线程安全问题的,这时候我们就希望有一个集合类,它能将读、写操作分离,只让写线程串行执行,而读线程可以并行执行,这个集合类就是:CopyOnWriteArrayList

? ? ? ? 如何实现读、写分离?

????????让我们来看看add(E e)方法实现

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        //获取锁对象
        lock.lock();
        try {
            //获取存储元素的Object数组
            Object[] elements = getArray();
            //获取Object数组长度
            int len = elements.length;
            //拷贝旧Object数组得到一个新的Object数组
            //新数组长度比旧数组长度多1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            //将元素插入新的Object数组
            newElements[len] = e;
            //使用新数组覆盖旧数组
            setArray(newElements);
            //返回
            return true;
        } finally {
            //finally块释放锁,避免死锁
            lock.unlock();
        }
    }

? ? ? ? get(int index)方法实现

public E get(int index) {
        
        return get(getArray(), index);
    }


private E get(Object[] a, int index) {
        return (E) a[index];
    }



//获取当前Object数组
final Object[] getArray() {
        //array:成员变量,是集合类底层存储元素的Object数组
        return array;
    }

总结:? ? ? ?

? ? ? ? ?1、多个写线程并发访问CopyOnWriteArrayList集合对象时,只有一个写线程能获取到ReentrantLock锁,所有写线程串行执行。

? ? ? ? 2、add插入逻辑:获取旧数组及旧数组长度;基于旧数组拷贝出一个新数组,新数组长度为旧数组长度+1;将元素插入到新数组中,并用新数组覆盖掉旧数组。

? ? ? ? 3、get方法逻辑:无需上锁,使用getArray()方法获取当前的Object数组,并通过索引查找对应元素即可。

? ? ? ? 基于CopyOnWriteArrayList的特点我们不难发现,这种集合对象只适用于读多写少的场景,如果写线程远多于读线程,写线程串行执行的同时还要执行耗时的拷贝操作,性能较低。

文章来源:https://blog.csdn.net/Annancqxxx/article/details/135226054
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。