为什么 ArrayList 是线程不安全却在开发中被广泛使用?

99 阅读6分钟

沉默是金,总会发光

大家好,我是沉默

在 Java 开发中,ArrayList 无疑是最常用的集合类之一。尽管它线程不安全,但在实际项目中,我们几乎无时无刻不在用它。

为什么?它的线程不安全问题到底有多严重?为什么开发者依然偏爱它?

今天,我们将从 技术原理业务场景最佳实践 三个层面,深度解析这个看似简单的技术问题背后的深层逻辑。

**-**01-

ArrayList 线程不安全的技术原理

1 线程安全问题的核心根源:缺乏同步机制

ArrayList 是基于动态数组实现的。它的核心方法(如 add()remove())并没有加上 synchronized 关键字,这意味着,当多个线程同时操作同一个 ArrayList 实例时,数据一致性就会面临严重威胁。

代码示例:多线程环境下的元素添加

public class ArrayListThreadIssueDemo {
    private static List<String> list = new ArrayList<>();
    public static void main(String[] args) {
        // 创建100个线程,每个线程向list中添加1000个元素
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    list.add("element");  // 多线程无同步保护
                }
            }).start();
        }
        try {
            Thread.sleep(1000);  // 等待线程执行完毕
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("预期大小:100000,实际大小:" + list.size());
    }
}

问题:最终的 list.size() 可能小于预期的 100,000,原因是多线程并发操作没有同步,导致元素丢失或数据不一致。

2 扩容机制引发的线程安全问题

ArrayList 的元素数量超过当前容量时,它会自动触发扩容。在扩容过程中,ArrayList 会进行数组复制和底层数组引用更新。如果两个线程同时触发扩容,可能导致某个线程的扩容结果被覆盖,进而丢失数据。

扩容机制的风险

private void grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);  // 新容量是原容量的1.5倍
    elementData = Arrays.copyOf(elementData, newCapacity);  // 复制元素到新数组
}

多线程环境下,扩容可能出现的问题是:若两个线程同时执行扩容操作,可能导致其中一个线程的修改结果被覆盖,丢失数据。

3 迭代器与 modCount 机制的冲突

ArrayList 使用 modCount 来记录集合的修改次数。在多线程环境下,如果一个线程在迭代过程中修改了集合,就会抛出 ConcurrentModificationException

示例:迭代器与 modCount 的冲突

private class Itr implements Iterator<E> {
    int expectedModCount = modCount;  // 记录修改次数
    public boolean hasNext() {
        return cursor != size;
    }
    public E next() {
        checkForComodification();  // 检查修改次数是否一致
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)  // 若修改次数变化,抛出异常
            throw new ConcurrentModificationException();
    }
}

**-**02-

为何在实际开发中广泛使用 ArrayList

1 单线程环境:99%的业务场景

大多数情况下,ArrayList 是在单线程环境中使用的,比如:

  • Web 请求处理:Controller 层在单个线程内处理数据(如将查询结果放入 ArrayList)。

  • 方法内部逻辑:在某个方法内创建并操作 ArrayList,方法执行完即销毁,不涉及多线程共享。

这种情况下,ArrayList 的线程不安全特性根本不会暴露,性能优势自然得到了发挥。

2 性能优势:与线程安全集合的差距

与线程安全的 Vector 相比,ArrayList 在单线程环境下的性能要高得多。无锁设计避免了同步带来的性能损耗。

  • 操作效率ArrayList 在单线程下,元素的添加操作是 O(1)(平均),而 Vector 因为加了锁,性能明显下降。

  • 遍历效率:使用普通的 for 循环遍历 ArrayList 性能远高于 VectorEnumeration 迭代器。

3 多线程环境下的成熟解决方案

当确实需要在多线程环境中使用集合时,Java 提供了多种线程安全的方案,无需强制使用 Vector

  • 同步包装器:通过 Collections.synchronizedList()ArrayList 包装成线程安全的集合。
List<String> safeList = Collections.synchronizedList(new ArrayList<>());
synchronized (safeList) {  // 手动同步
    safeList.add("element");
}
  • 写时复制容器CopyOnWriteArrayList 适用于读多写少的场景,写操作时会复制数组,保证线程安全。
List<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("write");
for (String s : cowList) {  // 读操作直接访问原数组
    process(s);
}
  • 自定义锁范围:通过手动控制锁的粒度,平衡安全性和性能。
private static final Object LOCK = new Object();
public static void safeAdd(String element) {
    synchronized (LOCK) {  // 在操作期间加锁
        list.add(element);
    }
}

**-**03-

核心代码中的安全与效率平衡

1 复现线程安全问题的完整代码

public class ArrayListThreadUnsafeDemo {
    private static List<String> list = new ArrayList<>();
    private static final int THREAD_COUNT = 100;  // 线程数
    private static final int ELEMENT_PER_THREAD = 1000;  // 每线程添加的元素数
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(THREAD_COUNT);
        for (int i = 0; i < THREAD_COUNT; i++) {
            new Thread(() -> {
                try {
                    for (int j = 0; j < ELEMENT_PER_THREAD; j++) {
                        list.add(UUID.randomUUID().toString());  // 模拟多线程并发写
                    }
                } finally {
                    latch.countDown();
                }
            }).start();
        }
        latch.await();  // 等待线程结束
        System.out.println("预期元素总数:" + (THREAD_COUNT * ELEMENT_PER_THREAD));
        System.out.println("实际元素总数:" + list.size());  // 多线程导致元素丢失
    }
}

2 安全使用 ArrayList 的最佳实践

同步包装器

List<String> safeList = Collections.synchronizedList(new ArrayList<>());
public static void addElement(String element) {
    synchronized (safeList) {  // 手动对列表加锁
        safeList.add(element);
    }
}

CopyOnWriteArrayList

List<String> cowList = new CopyOnWriteArrayList<>();
cowList.add("write once");
for (String s : cowList) {
    System.out.println(s);  // 高效且线程安全的读取
}

**-**04-

总结

ArrayList 的广泛使用,不仅仅是因为它性能优越,还因为它在单线程环境下的稳定性和简单性。对于大多数业务场景,没有必要使用线程安全的集合,而是可以在适当场景下使用 ArrayList 以获得更高的性能。

然而,当涉及到多线程时,Java 提供了多种成熟的工具,如同步包装器、写时复制容器等,帮助开发者根据业务需求灵活选择最适合的方案。

通过理解 ArrayList 的线程安全问题,我们能够更加理性地选择合适的工具和解决方案,而不是盲目避开它。这也体现了软件开发中的一个原则没有完美的工具,只有最适合的解决方案

热门文章

一套能保命的高并发实战指南

架构师必备:用 AI 快速生成架构图

**-**05-

粉丝福利

我这里创建一个程序员成长&副业交流群,和一群志同道合的小伙伴,一起聚焦自身发展,可以聊:技术成长与职业规划,分享路线图、面试经验和效率工具,探讨多种副业变现路径,从写作课程到私活接单,主题活动、打卡挑战和项目组队,让志同道合的伙伴互帮互助、共同进步。如果你对这个特别的群,感兴趣的,可以加一下,微信通过后会拉你入群,但是任何人在群里打任何广告,都会被我T掉。