List并发线程安全问题

时间:2025-02-14 14:43:45

一、发现并发问题

1.1 测试代码
public class Client {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                list.add(1);
            }
        },"A").start();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                list.add(1);
            }
        },"B").start();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                list.add(1);
            }
        },"C").start();

        try {
            TimeUnit.SECONDS.sleep(3);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("list.size() = " + list.size());
    }
}
1.2 问题一:ArrayIndexOutOfBoundsException

我们先来看看ArrayList.add()的源码

/**
 * Appends the specified element to the end of this list.
 *
 * @param e element to be appended to this list
 * @return <tt>true</tt> (as specified by {@link Collection#add})
 */
public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

在多线程环境中时,多个线程同时进入add()方法,同时检查容量,例如当前容量为5,而已占用4。三个线程同时检查,都发现还有容量,则都同时添加元素。由此导致ArrayIndexOutOfBoundsException。

1.3 问题二:实际插入元素个数小于预期插入元素个数

从运行结果可以看出,最终list.size()只有11680 <= 30000。我们希望能够插入30000个元素,可是实际上只插入了<= 30000个元素。还是从源码入手:

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

试想一下,如果多个线程同时向size位插入元素,且都没有来得及size++,那么导致的结果就是多个元素被插入在了同一个位置,相互抵消。

二、解决并发问题

2.1 使用Vector

早期,IT前人为了解决List在并发时出现的问题,引入了Vector实现类。Vetor的实现方式与ArrayList大同小异,它的底层也是一个数组,在添加时自增长。我们先来看看Vector.add()的源码

/**
 * 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;
}

与ArrayList不同的是,它的add()方法带有synchronized关键字。这表明当线程调用该方法时,会自动占用锁,直到这个线程的任务完成,期间不会放弃该锁。而且当线程占有该锁时,别的线程无法进入Vetor类调用带有synchronized关键字的方法。这很好的避免了多线程竞争的现象,从而保证了并发安全

我们现在将ArrayList换成Vetor再试试:

public class Client {
    public static void main(String[] args) {
        Vector<Integer> list = new Vector<>();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                list.add(1);
            }
        },"A").start();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                list.add(1);
            }
        },"B").start();

        new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                list.add(1);
            }
        },"C").start();

        try {
            TimeUnit.SECONDS.sleep(3);
        }catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("list.size() = " + list.size());  // 30000
    }
}