如何解决ArrayList线程不安全

大家都知道ArrayList是线程不安全的,那我们如何写一个demo来证明ArrayList是线程不安全的呢,并且我们有哪些方式可以解决ArrayList线程不安全呢?

首先,我们创建30个线程去往ArrayList中添加元素:

public class ArrayListDemo {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        //此处只是案例demo,真实使用线程池不建议用这种方式创建
        ExecutorService threadPool = Executors.newFixedThreadPool(100);

        for (int i = 1; i <= 100; i++) {
            int finalI = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                list.add(finalI);
            });
        }
        threadPool.shutdown();
    } }

运行程序:
在这里插入图片描述

程序报了数组下标越界异常异常ArrayIndexOutOfBoundsException,我们也可以通过看下ArrayList底层中add()方法,是没有加锁的操作,当多个线程共享一份资源时,可能发生线程问题;
ArrayList的add()方法是没有加锁
在这里插入图片描述

那我们如何解决ArrayList线程不安全的问题呢

一、使用Vector
Vector是线程安全的,我们可以看下Vector底层的方法是同步的(Synchronized修饰),从而可以解决ArrayList线程不安全的问题;

/**
 * Appends the specified element to the end of this Vector.
 *
 * @param e element to be appended to this Vector
 * @return {@code true} (as specified by {@link Collection#add})
 * @since 1.2
 */
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

二、使用Collections.synchronizedList()来解决ArrayList线程不安全的问题
将上述案例中创建list方法改成如下即可

 List<Integer> list = Collections.synchronizedList(new ArrayList<>());

Collections.synchronizedList()方法中会这个根据传入的List是否实现RandomAccess这个接口来返回的SynchronizedRandomAccessList还是SynchronizedList.

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

我们可以看下SynchronizedList中的方法也是同步的,使用了Synchronized修饰;
在这里插入图片描述
上面两种方法确实是可以解决ArrayList线程不安全的问题,但是两者都是将所有的方法都加锁,那会导致效率低下,只能一个线程操作完,下一个线程获取到锁才能操作;

我们来认识一下:CopyOnWriteArrayList
CopyOnWriteArrayList是如何保证线程安全的呢?
CopyOnWriteArrayList是一个线程安全的ArrayList,其实现原理是读写分离,其对写操作使用ReentrantLock来上锁,对读操作则不加锁;CopyOnWriteArrayList在写操作的时候,会将list中的数组拷贝一份副本,然后对其副本进行操作(如果此时其他线程需要读的事,那么其他线程读取的是原先的没有修改的数组,如果其他写操作的线程要进行写操作,需要等待正在写的线程操作完成,释放ReentrantLock后,去获取锁才能进行写操作),写操作完成后,会讲list中数组的地址引用指向修改后的新数组地址。

  /**
     * Appends the specified element to the end of this list.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#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();
        }
    }

我们可以看到,如果使用场景的写操作十分频繁的话,建议还是不要实现CopyOnWriteArrayList,因为其添加的时候会造成数组的不断扩容和复制,十分消耗性能,会消耗内存,如果原数组的数据比较多的情况下,可能会导致young gc或者full gc;并且其不能使用在实时读的场景,在写操作过程中是要花费时间的,读取的时候可能还是旧数据;
CopyOnWriteArrayList 合适读多写少的场景, 如果我们在使用的时候没法保证CopyOnWriteArrayList 到底要放多少数据的话,我们还是要谨慎使用,如果数据稍微有点多,每次写操作都重新拷贝数组,其代价实在太高昂了。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值