ArrayList为什么线程不安全 线程不安全体现在哪些方面 源码角度分析其具体原因-阿里云开发者社区

开发者社区> 豌豆校长> 正文

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,然后再将结果写入主内存。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
CopyOnWriteArrayList源码阅读笔记
CopyOnWriteArrayList源码阅读笔记
8 0
错误代码 missing-signature 错误原因: 缺少签名参数-自查方案
[b]报错截图[/b] [attachment=2253][b]错误原因:[/b] 1.php版本低于5.5 2.把配置私钥的位置,填写成了支付宝公钥或者商户公钥导致。 [color=#ff0000]更多密钥填写错误查看:[url]https://openclub.
1208 0
Servlet中的线程安全
        Servlet 之所有效率比CGI高,是因为CGI每一个请求都会FORK出一个进程来处理相应的请求,而Servlet则是用线程来处理的,但随之而来就有一个问题:多线程的安全问题!       在Servlet中,类的成员变量是共享的,所有的请求都共用成员变量,为了保证线程安全,我们应该尽量做到如下几点:      1.用方法变量,即局部变量来保存请求中的专有数据,因为每个线程都会有自己的一份局部变量,这样就不会相互影响了。
731 0
LinkedList源码阅读笔记
LinkedList源码阅读笔记
5 0
ArrayList源码分析(基于JDK1.6)
不积跬步,无以至千里;不积小流,无以成江海。从基础做起,一点点积累,加油!     《Java集合类》中讲述了ArrayList的基础使用,本文将深入剖析ArrayList的内部结构及实现原理,以便更好的、更高效的使用它。
481 0
ArrayList removeRange方法分析
《ArrayList原码分析》一文中提到了“为什么removeRange(int fromIndex,int toIndex)是protected的?”     先给出removeRange(int fromIndex,int toIndex)方法的源码(这段代码是干什么的就不再解释了,源...
637 0
线程同步:条件变量的使用细节分析
如同互斥量和读写锁一样,条件变量也需要初始化和回收#includeint pthread_cond_init(pthread_cond_t *restrict cond, pthread_condattr_t *restrict attr);int pthread_cond_destroy(pthread_cond_t *cond);互斥量和读写锁解决了多线程访问共享变量产生的竞争问题,那么条件变量的作用何在呢。
707 0
ArrayList为什么线程不安全 线程不安全体现在哪些方面 源码角度分析其具体原因
我们都知道ArrayList是线程不安全的,那么它不安全在哪里?又会出现什么并发问题呢?
254 0
线程安全
什么是线程安全,个人在线程中使用共享资源时,能够保证共享资源在任何时候都是原子的、一致的,这样的线程就是线程安全的线程 首先来介绍一下共享资源的类型(这是我自己分类的,为了后文好解释),共享资源从其类型可以分为三类(下文讲到变量一律指类变量或实例变量,不再特别指出): 1.
861 0
+关注
1
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
《2021云上架构与运维峰会演讲合集》
立即下载
《零基础CSS入门教程》
立即下载
《零基础HTML入门教程》
立即下载