【List篇】ArrayList 的线程不安全介绍

ArrayList 为什么线程不安全?

主要原因是ArrayList是非同步的,没有同步机制,并且其底层实现是基于数组,而数组的长度是固定的。当对 ArrayList 进行增删操作时,需要改变数组的长度,这就会导致多个线程可能同时操作同一个数组,从而引发线程安全问题。

具体来说,如果多个线程同时对 ArrayList 进行写操作(add、remove 等),可能会导致以下问题:

  • 数据不一致:多个线程同时修改 ArrayList 的元素,可能会导致数据不一致的情况。例如,一个线程正在修改一个元素,而另一个线程正在读取该元素,这时就会出现数据不一致的情况。

  • 索引越界:如果多个线程同时进行添加或删除元素操作,就可能导致索引越界的情况。例如,一个线程正在删除 ArrayList 中最后一个元素,而另一个线程正在向 ArrayList 中添加元素,这时就可能导致索引越界的情况。

coffeescript 复制代码
/**
 * 模拟ArrayList线程不安全
 */
public class UnsafeArrayList {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        //线程1,添加1000个元素
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                list.add(i);
            }
        });
        //线程2,添加1000个元素
        Thread t2 = new Thread(() -> {
            for (int i = 1000; i < 2000; i++) {
                list.add(i);
            }
        });

        //启动线程
        t1.start();
        t2.start();

        try {
            //等待两个线程执行完毕
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("期望size=2000,实际size=" + list.size());
    }
}

上面的代码中,我们创建了两个线程 t1 和 t2,分别向 ArrayList 中添加了 1000 个元素。但是,由于 ArrayList 不是线程安全的,所以在多线程环境下,两个线程可能会同时访问 ArrayList,导致线程安全问题。

运行上面的代码,结果如下:


输出结果可能不一定是 2000,可能会小于 2000,这是因为线程之间的操作是交错执行的,有可能一个线程正在添加元素时,另一个线程就已经读取了数组的大小并返回了结果。这样就会导致对数组的修改操作在并发情况下会导致线程竞争和不确定性的结果。

  • ConcurrentModificationException场景 : 这个异常发生的原因是在迭代集合时,有一个线程对集合进行了修改,并且另一个线程正在运行迭代器,这时就会发生ConcurrentModificationException异常。

为了模拟这个异常,我们可以使用Java的多线程功能。下面是一个简单的示例代码,可以模拟ConcurrentModificationException异常:

csharp 复制代码
public class ConcurrentModificationDemo {
  public static void main(String[] args) {
      //模拟在多线程场景下,由于ArrayList线程不安全引发的ConcurrentModificationException异常场景
      List<Integer> list = new ArrayList<>();
      list.add(1000);
      list.add(2000);
      list.add(3000);
      
      //执行迭代的逻辑
      Runnable task = () -> {
          Iterator<String> iterator = list.iterator();
          while (iterator.hasNext()) {
              String fruit = iterator.next();
              System.out.println(fruit);
              try {
                  Thread.sleep(1000);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      };

      //线程1执行迭代逻辑
      Thread thread1 = new Thread(task);
      //线程2 插入元素
      Thread thread2 = new Thread(() -> list.add(4000));

      thread1.start();
      thread2.start();
  }
}

在这个示例代码中,我们创建了一个包含三个字符串元素的列表,并使用两个线程来模拟ConcurrentModificationException异常。

一个线程使用列表的迭代器来遍历列表,并在每个元素输出之后休眠1秒。另一个线程在1秒后向列表添加一个新元素。

当第二个线程添加一个新元素时,它会修改列表,而第一个线程仍在使用迭代器遍历列表。因此,当第一个线程尝试访问下一个元素时,就会抛出ConcurrentModificationException异常。

注意,由于多线程环境的不确定性,实际运行结果可能有所不同。

如何解决ArrayList的线程不安全?

1.使用Collections.synchronizedList方法将ArrayList转换成线程安全的List。该方法返回一个线程安全的List,它会在每个方法调用时同步访问

coffeescript 复制代码
List<Integer> syncList = Collections.synchronizedList(new ArrayList<Integer>());

2.使用CopyOnWriteArrayList类来替代ArrayList。CopyOnWriteArrayList是Java并发包中提供的一个线程安全的List,它通过使用写时复制技术(Copy-On-Write)来实现线程安全,每次写操作都会创建一个新的数组,读操作不会加锁,因此性能较高。

coffeescript 复制代码
List<Integer> copyOnWriteList = new CopyOnWriteArrayList<Integer>();

3.使用Locksynchronized关键字来保证多个线程对ArrayList的操作同步。可以通过在访问ArrayList的代码块上添加synchronized关键字,或者使用Java并发包中的Lock类来实现同步。但是需要注意,这种方式可能会出现死锁等问题,并且性能也会受到影响。

coffeescript 复制代码
private static Lock lock = new ReentrantLock();
private static List<Integer> list = new ArrayList<>();

public static void addToArrayList(Integer element) {
    //加锁
    lock.lock();
    try {
        list.add(element);
    } finally {
        //释放锁
        lock.unlock();
    }
}
  1. 使用线程安全的并发容器,如ConcurrentHashMap等。

ArrayList既然线程不安全 为什么还要使用?

虽然ArrayList是线程不安全的,但是它具有以下优点:

  • 简单易用:ArrayList是Java集合框架中最简单且最常用的类之一。
  • 高效性能:相比于线程安全的Vector类,ArrayList在单线程环境下具有更高的性能。
  • 空间利用率高:ArrayList在内存使用上比数组更加灵活,能够优化内存利用率。
  • 可扩展性强:ArrayList自带自动扩容机制,能够自动调整数组大小,支持动态添加元素。

因此,对于单线程环境下的数据存储和访问,使用ArrayList是一种非常方便和高效的选择。

相关推荐
RainbowSea3 分钟前
5. RabbitMQ 消息队列中 Exchanges(交换机) 的详细说明
java·消息队列·rabbitmq
李少兄2 小时前
Unirest:优雅的Java HTTP客户端库
java·开发语言·http
此木|西贝2 小时前
【设计模式】原型模式
java·设计模式·原型模式
可乐加.糖2 小时前
一篇关于Netty相关的梳理总结
java·后端·网络协议·netty·信息与通信
s9123601012 小时前
rust 同时处理多个异步任务
java·数据库·rust
9号达人2 小时前
java9新特性详解与实践
java·后端·面试
cg50172 小时前
Spring Boot 的配置文件
java·linux·spring boot
啊喜拔牙2 小时前
1. hadoop 集群的常用命令
java·大数据·开发语言·python·scala
owde3 小时前
顺序容器 -list双向链表
数据结构·c++·链表·list
第404块砖头3 小时前
分享宝藏之List转Markdown
数据结构·list