ArrayList为什么线程不安全 线程不安全体现在哪些方面 源码角度分析其具体原因

简介: 我们都知道ArrayList是线程不安全的,那么它不安全在哪里?又会出现什么并发问题呢?

一、ArrayList源码摘录

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    /**
     * 列表元素集合数组
     * 说明ArrayList基于数组存储数据
     */
    transient Object[] elementData; 

    /**
     * 列表大小,elementData中存储的元素个数
     */
    private int size;
}

add() 方法

/**
 * Appends the specified element to the end of this list.
 * 将指定的元素追加到列表的末尾。
 * add() 方法做了如下操作:
 *     1.检查容量是否足够,如不够将进行扩容,并自增 modCount
 *     2.将指定的元素追加到列表的末尾
 *
 * @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!!
    //将e存在index为size的位置(即最后一位的下一位置),size++
    //我们都知道,++操作不是原子指令,多线程情况下将发生并发问题
    elementData[size++] = e;
    return true;
}

二、测试用例

@Test
public void listThreadUnsafe() throws InterruptedException {
    List<String> list = new ArrayList<>();
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                list.add("t1-" + i);
            }
        }
    });
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                list.add("t2-" + i);
            }
        }
    });
    t1.start();
    t2.start();

    Thread.sleep(2000);
    int size = list.size();
    System.out.println("size = " + size);
    for (int i = 0; i < size; i++) {
        System.out.println("索引为" + i + "的元素为:" + list.get(i));
    }
}

本用例多跑几次,将出现下面几种并发问题。

三、ArrayList线程不安全的表现

add()实际执行的过程为:

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

1. 并发环境下进行add操作时可能会导致elementData数组越界

问题现场如下:
有两个线程:t1,t2。有ArrayList size=9(即其中有9个元素)。elementData.length=10
t1进入add()方法,这时获取到size值为9,调用ensureCapacityInternal()方法判断容量是否需要扩容
t2也进入add()方法,这时获取到size值也为9,也调用ensureCapacityInternal()方法判断容量是否需要扩容
t1发现自己的需求为size+1=10,容量足够,无需扩容
t1发现自己的需求为也size+1=10,容量足够,无需扩容
t1开始设置元素操作,elementData[size++] = e,成功,此时size变为10
t2也开始进行设置元素操作,它尝试设置elementData[10] = e,而elementData没有进行过扩容,它的下标最大为9。于是此时会报出一个数组越界的异常:ArrayIndexOutOfBoundsException

用例体现为:
测试结果

2. 一个线程的值覆盖另一个线程添加的值

这个问题要分多钟情况了

2.1 情况1 size大小符合预期,但是中间有null值存在

流程描述如下:

问题现场如下:
有两个线程:t1,t2。有ArrayList size=5(即其中有5个元素)。elementData.length=10
t1进入add()方法,这时获取到size值为5,调用ensureCapacityInternal()方法判断容量是否需要扩容
t2也进入add()方法,这时获取到size值也为5,也调用ensureCapacityInternal()方法判断容量是否需要扩容
t1发现自己的需求为size+1=6,容量足够,无需扩容
t1发现自己的需求为也size+1=6,容量足够,无需扩容
t1开始设置元素操作,elementData[size] = e,成功,
t2也开始设置元素操作,elementData[size] = e,成功,注意此时t1的size+1还没执行
t1 size = size + 1 = 6,并写入主存
t2 size = size + 1 = 7
这样,size符合预期,但是t2设置的值被覆盖,而且索引为6的位置将永远为null,因为size已经为7,下次add()也会从7开始。除非手动set值。

用例体现如下:
测试结果

我们发现,t2的“t2-0”元素被覆盖。

2.2 情况2 size大小比预期的小

情况分析:

问题现场如下:
有两个线程:t1,t2。有ArrayList size=5(即其中有5个元素)。elementData.length=10
t1进入add()方法,这时获取到size值为5,调用ensureCapacityInternal()方法判断容量是否需要扩容
t2也进入add()方法,这时获取到size值也为5,也调用ensureCapacityInternal()方法判断容量是否需要扩容
t1发现自己的需求为size+1=6,容量足够,无需扩容
t1发现自己的需求为也size+1=6,容量足够,无需扩容
t1开始设置元素操作,elementData[size] = e,成功,
t2也开始设置元素操作,elementData[size] = e,成功,注意此时t1的size+1还没执行
t1 size = size + 1 = 6,暂未写入主存
t2 size = size + 1 此时因为t1操作完size还未写入主存,所以size依然为5,+1后仍为6
t1将size=6 写入主存
t2将size=6 写入主存
这样,size=6 比预期结果小了。

用例体现:

在这里插入图片描述


总结

上面介绍的情况都有其出现的概率,并不是每次都出现,只是在临界状态下出现错误。但是,作为程序的编写者,即使有千万分之一的概率,我们也要尽量去避免它,这是程序员的基本素养。

Tips

关于写入主存。
基本现在的CPU都是多核心的,每个核心有各自的高速缓存,计算任务需要在高速缓存中进行,对于缓存的访问速度 L1 > L2 > L3 > 内存。L1、L2为各核心独有,L3为多个核心共享。
我们的程序运行在主内存,但计算需要在CPU中完成。
当执行计算任务时,比如 size+1 操作,CPU先将 size 的值读进CPU缓存,在CPU缓存中计算 +1,然后再将结果写入主内存。

相关文章
|
2月前
|
设计模式 消息中间件 安全
【JUC】(3)常见的设计模式概念分析与多把锁使用场景!!理解线程状态转换条件!带你深入JUC!!文章全程笔记干货!!
JUC专栏第三篇,带你继续深入JUC! 本篇文章涵盖内容:保护性暂停、生产者与消费者、Park&unPark、线程转换条件、多把锁情况分析、可重入锁、顺序控制 笔记共享!!文章全程干货!
331 1
|
3月前
|
数据采集 存储 弹性计算
高并发Java爬虫的瓶颈分析与动态线程优化方案
高并发Java爬虫的瓶颈分析与动态线程优化方案
|
7月前
|
安全 Java 调度
Netty源码—3.Reactor线程模型二
本文主要介绍了NioEventLoop的执行总体框架、Reactor线程执行一次事件轮询、Reactor线程处理产生IO事件的Channel、Reactor线程处理任务队列之添加任务、Reactor线程处理任务队列之执行任务、NioEventLoop总结。
|
7月前
|
安全 Java
Netty源码—2.Reactor线程模型一
本文主要介绍了关于NioEventLoop的问题整理、理解Reactor线程模型主要分三部分、NioEventLoop的创建和NioEventLoop的启动。
|
存储 NoSQL Redis
Redis 新版本引入多线程的利弊分析
【10月更文挑战第16天】Redis 新版本引入多线程是一个具有挑战性和机遇的改变。虽然多线程带来了一些潜在的问题和挑战,但也为 Redis 提供了进一步提升性能和扩展能力的可能性。在实际应用中,我们需要根据具体的需求和场景,综合评估多线程的利弊,谨慎地选择和使用 Redis 的新版本。同时,Redis 开发者也需要不断努力,优化和完善多线程机制,以提供更加稳定、高效和可靠的 Redis 服务。
301 1
|
8月前
|
Java 中间件 调度
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
本文涉及InheritableThreadLocal和TTL,从源码的角度,分别分析它们是怎么实现父子线程传递的。建议先了解ThreadLocal。
313 4
【源码】【Java并发】从InheritableThreadLocal和TTL源码的角度来看父子线程传递
线程CPU异常定位分析
【10月更文挑战第3天】 开发过程中会出现一些CPU异常升高的问题,想要定位到具体的位置就需要一系列的分析,记录一些分析手段。
311 0
|
11月前
|
并行计算 安全 Java
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
在Python开发中,GIL(全局解释器锁)一直备受关注。本文基于CPython解释器,探讨GIL的技术本质及其对程序性能的影响。GIL确保同一时刻只有一个线程执行代码,以保护内存管理的安全性,但也限制了多线程并行计算的效率。文章分析了GIL的必要性、局限性,并介绍了多进程、异步编程等替代方案。尽管Python 3.13计划移除GIL,但该特性至少要到2028年才会默认禁用,因此理解GIL仍至关重要。
905 16
Python GIL(全局解释器锁)机制对多线程性能影响的深度分析
|
9月前
|
Java 调度
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
499 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
|
安全 Java 程序员
ArrayList vs Vector:一场线程安全与性能优化的世纪之争!
在 Java 面试中,ArrayList 和 Vector 是高频考点,但很多人容易混淆。本文通过10分钟深入解析它们的区别,帮助你快速掌握性能、线程安全性、扩容机制等核心知识,让你轻松应对面试题目,提升自信!
322 18

热门文章

最新文章