为什么说ArrayList是线程不安全的?

为什么说ArrayList是线程不安全的?一 概述对于 ArrayList 相信大家并不陌生 这个类是我们平时接触得最多的一个列表集合类 面试时相信面试官首先就会问到关于它的知识 一个经常被问到的问题就是 ArrayList 是否是线程安全的 答案当然很简单 无论是背来的还是自己看过源码 我们都知道它是线程不安全的 那么它为什么是线程不安全的呢 它线程不安全的具体体现又是怎样的呢 我们从源码的角度来看下 二 源码分析首先看

一.概述

对于ArrayList,相信大家并不陌生。这个类是我们平时接触得最多的一个列表集合类。

面试时相信面试官首先就会问到关于它的知识。一个经常被问到的问题就是:ArrayList是否是线程安全的?

答案当然很简单,无论是背来的还是自己看过源码,我们都知道它是线程不安全的。那么它为什么是线程不安全的呢?它线程不安全的具体体现又是怎样的呢?我们从源码的角度来看下。

二.源码分析

首先看看这个类所拥有的部分属性字段:

public class ArrayList<E> extends AbstractList<E> implements List 
  
    , RandomAccess, Cloneable, java.io.Serializable { 
   / * 列表元素集合数组 * 如果新建ArrayList对象时没有指定大小,那么会将EMPTY_ELEMENTDATA赋值给elementData, * 并在第一次添加元素时,将列表容量设置为DEFAULT_CAPACITY */ transient Object[] elementData; 
   / * 列表大小,elementData中存储的元素个数 */ 
   private int size; } 
  

所以通过这两个字段我们可以看出,ArrayList的实现主要就是用了一个Object的数组,用来保存所有的元素,以及一个size变量用来保存当前数组中已经添加了多少元素。

接着我们看下最重要的add操作时的源代码:

public boolean add(E e) { / * 添加一个元素时,做了如下两步操作 * 1.判断列表的capacity容量是否足够,是否需要扩容 * 2.真正将元素放在列表的元素数组里面 */ ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } 

ensureCapacityInternal()这个方法的详细代码我们可以暂时不看,它的作用就是判断如果将当前的新元素加到列表后面,列表的elementData数组的大小是否满足,如果size + 1的这个需求长度大于了elementData这个数组的长度,那么就要对这个数组进行扩容。

由此看到add元素时,实际做了两个大的步骤:

  1. 判断elementData数组容量是否满足需求
  2. 在elementData对应位置上设置值

这样也就出现了第一个导致线程不安全的隐患,在多个线程进行add操作时可能会导致elementData数组越界。具体逻辑如下:

  1. 列表大小为9,即size=9
  2. 线程A开始进入add方法,这时它获取到size的值为9,调用ensureCapacityInternal方法进行容量判断。
  3. 线程B此时也进入add方法,它获取到size的值也为9,也开始调用ensureCapacityInternal方法。
  4. 线程A发现需求大小为10,而elementData的大小就为10,可以容纳。于是它不再扩容,返回。
  5. 线程B也发现需求大小为10,也可以容纳,返回。
  6. 线程A开始进行设置值操作, elementData[size++] = e 操作。此时size变为10。
  7. 线程B也开始进行设置值操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常ArrayIndexOutOfBoundsException.

另外第二步 elementData[size++] = e 设置值的操作同样会导致线程不安全。从这儿可以看出,这步操作也不是一个原子操作,它由如下两步操作构成:

  1. elementData[size] = e;
  2. size = size + 1;

在单线程执行这两条代码时没有任何问题,但是当多线程环境下执行时,可能就会发生一个线程的值覆盖另一个线程添加的值,具体逻辑如下:

  1. 列表大小为0,即size=0
  2. 线程A开始添加一个元素,值为A。此时它执行第一条操作,将A放在了elementData下标为0的位置上。
  3. 接着线程B刚好也要开始添加一个值为B的元素,且走到了第一步操作。此时线程B获取到size的值依然为0,于是它将B也放在了elementData下标为0的位置上。
  4. 线程A开始将size的值增加为1
  5. 线程B开始将size的值增加为2

这样线程AB执行完毕后,理想中情况为size为2,elementData下标0的位置为A,下标1的位置为B。而实际情况变成了size为2,elementData下标为0的位置变成了B,下标1的位置上什么都没有。并且后续除非使用set方法修改此位置的值,否则将一直为null,因为size为2,添加元素时会从下标为2的位置上开始。

接下来我们用个小例子验证一下。

三.案例复现

我们用如下的代码可以进行安全性的校验:

public static void main(String[] args) throws InterruptedException { final List 
  
    list = 
   new ArrayList 
   
     (); 
    // 线程A将0-1000添加到list 
    new Thread( 
    new Runnable() { 
    public 
    void 
    run() { 
    for ( 
    int i = 
    0; i < 
    1000 ; i++) { list.add(i); 
    try { Thread.sleep( 
    1); } 
    catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); 
    // 线程B将1000-2000添加到列表 
    new Thread( 
    new Runnable() { 
    public 
    void 
    run() { 
    for ( 
    int i = 
    1000; i < 
    2000 ; i++) { list.add(i); 
    try { Thread.sleep( 
    1); } 
    catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); Thread.sleep( 
    1000); 
    // 打印所有结果 
    for ( 
    int i = 
    0; i < list.size(); i++) { System. 
    out.println( 
    "第" + (i + 
    1) + 
    "个元素为:" + list. 
    get(i)); } } 
    
  

最后的输出结果中,有如下的部分:

7个元素为:38个元素为:10039个元素为:410个元素为:100411个元素为:null12个元素为:100513个元素为:6 

可以看到第11个元素的值为null,这也就是我们上面所说的情况。

多测试几次的话,数组越界的异常也可以复现出来。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/216520.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月18日 上午11:40
下一篇 2026年3月18日 上午11:40


相关推荐

  • mysql的left join和inner join的效率对比,以及如何优化

    mysql的left join和inner join的效率对比,以及如何优化一 前言最近在写代码的时候 遇到了需要多表连接的一个问题 初始 sql 类似于 select fromaleftjoi x b xleftjoincon y b yleftjoindon z c z nbsp nbsp nbsp nbsp nbsp nbsp 这样的多个 leftjoin 组合 一方面是心里有点不舒服 总觉得这种

    2026年3月16日
    2
  • 电口与光口的区别

    电口与光口的区别电口与光口的区别光口光纤接口是用来连接光纤线缆的物理接口 其原理是利用了光从光密介质进入光疏介质从而发生了全反射 通常有 SC ST FC 等几种类型 它们由日本 NTT 公司开发 FC 是 FerruleConne 的缩写 其外部加强方式是采用金属套 紧固方式为螺丝扣 ST 接口通常用于 10Base F SC 接口通常用于 100Base FX 电口电口是相对光口来讲的

    2026年3月26日
    2
  • 天涯共此双11——天猫升级港澳台“购物天堂”

    天涯共此双11——天猫升级港澳台“购物天堂”香港北区上水60多年的老字号正和隆酱油没想过会出名。这家专注服务街坊的小店不在乎“酱香巷子深”,门店一半是透明及地的塑胶门帘,一半是一块块拼接起来的黄色纸板箱。来的都是熟客,所谓收银台就是个铅桶,顾客要付钱就把铅桶拉下来,付钱、找零,再把铅桶放上去。这是父辈们持续了半个多世纪的生意。到了店主女儿这里,事情开始改变。她赶时髦,在店里放了有支付宝二维码的蓝白…

    2022年10月5日
    4
  • android中适配器的作用,适配器模式 在Android中的简单理解「建议收藏」

    android中适配器的作用,适配器模式 在Android中的简单理解「建议收藏」Android在Android上提到适配器模式就会想到最常用的ListView和BaseAdapter在这个功能的使用中,类似于适配器模式的对象适配器例如在ListView中想用一个getView()方法,但是不同的数据,不同的需求,会有不同的getView()结果,所以getView()不能写死了,那么可能就想到了用适配器模式所以ListView里面包含了一个ListAdapter的成员变量,实…

    2022年5月31日
    48
  • 虚拟存储技术和交换技术的区别是什么_虚拟存储器技术

    虚拟存储技术和交换技术的区别是什么_虚拟存储器技术虚拟存储技术和交换技术很像,乍一看都是换入换出,把暂时不需要用的数据换出内存,将需要用到的数据换入内存,从而实现逻辑上内存的扩充。二者之间的区别是,虚拟存储技术是在一个作业运行的过程中,将作业的数据进行换入换出。王道老师举得例子就是玩儿游戏。这儿换一个游戏,比如玩儿DOTA,停留在场景A的时候,场景B的数据不需要用到,所以不放在内存,转换到场景B的时候再把场景B的数据放入内存。而交换技术是内存紧张时,换出某些进程,腾出内存空间,换入其他进程。换而言之,交换技术是在不同的进程(作业)间的,虚拟存储技术是在一个

    2026年4月13日
    4
  • patch文件介绍和生成方法

    patch文件介绍和生成方法Git 打补丁 patch 和 diff 的使用 详细 gitdiff 和 diff 产生的文件简介 gitpatch 制作相关简介 gitformat patch 用法

    2026年3月19日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号