ArrayList为什么不是线程安全的,如何保证线程安全?

这篇具有很好参考价值的文章主要介绍了ArrayList为什么不是线程安全的,如何保证线程安全?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一下详细分析原因

官方曰,线程安全就是多线程访问时,采⽤了加锁机制,当⼀个线程访问该类的某个数据时,进⾏保护,其他线程不能进⾏访问直到该线程读取完,其他线程才可使⽤。不会出现数据不⼀致或者数据污染。线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。说白了,线程安全就是多个线程修改同一个变量的时候,修改的结果需要和单线程修改的结果相同。如果修改的结果和预期不符,那就是线程不安全。

代码例子:

ArrayList为什么不是线程安全的,如何保证线程安全?

结果为0,为什么不是1?

解释:因为for循环里新增了一个新的线程,来负责向list里add一个元素。但是我们打印的list.size 是主线程。如果在新的线程 new Thread 没执行完add 方法, 主线程就执行打印的代码,那么就是 0啊 。所以就是说,主线程等一等,让 for循环里面的新的线程 new Thread 先插入数据。

ArrayList为什么不是线程安全的,如何保证线程安全?

可以看到结果与期望值1是一致的。

情况①:

正常运行的情况,可以看到 10个线程 不争不抢 :

ArrayList为什么不是线程安全的,如何保证线程安全?

情况②:

多个线程抢占资源,有竞争,(仅仅对于往list塞数据这个动作来说)

ArrayList为什么不是线程安全的,如何保证线程安全?

情况③:

出现 ‘不安全’情况了 ,多线程操作 ArrayList 导致出现 add赋值 出现 null

ArrayList为什么不是线程安全的,如何保证线程安全?

出现 null 情景分析 ,先看看源码:

ArrayList为什么不是线程安全的,如何保证线程安全?

Object[] elementData : 保存所有元素值的 数组

size : elementData中存储的元素个数

再看看 add 函数的源码 :

ArrayList为什么不是线程安全的,如何保证线程安全?
ArrayList为什么不是线程安全的,如何保证线程安全?

ensureExplicitCapacity 函数:

将当前的新元素加到列表后面,判断列表的 elementData 数组的大小是否满足。如果 size + 1 的这个需求长度大于 elementData 这个数组的长度,那么就要对这个数组进行扩容。

elementData[size++] = e

e是传入的值, 把这个值赋值在 elementData数组的 size++ 位置 。

很显然,这两步没有和在一块操作。

也就说如果出现这个扩容的触发和后面赋值并发情况 ,那么就有不安全问题产生。

ArrayList是基于数组实现,数组大小一旦确定就无法更改。

ArrayList的扩容: 将旧数组容器的元素拷贝到新大小的数组中(Arrays.copyOf函数)。

ArrayList为什么不是线程安全的,如何保证线程安全?

通过new ArrayList<>()实例的对象初始化的大小是0,所以第一次插入就肯定会触发扩容。如下代码:

ArrayList为什么不是线程安全的,如何保证线程安全?

那什么时候DEFAULT_CAPACITY = 10 默认值起作用呢?其实官方有注释:

ArrayList为什么不是线程安全的,如何保证线程安全?

添加第一个元素时,任何elementData==DEFAULTCAPACITY_EMPTY_ELEMENTDATA的空ArrayList都将扩展为DEFAULT_CAPACITY。

第一个数据是 null (其实应该称为 执行扩容操作,并发导致出现null值 )分析 :

第一个线程A 插入数据时属于首次add ,发现需要扩容 , 线程A 去扩容去了。

然后 我们是多线程操作场景, for循环第二次,触发new第二个线程B来了,线程B去add的时候,因为线程A第一次扩容可能并没完成,所以导致线程B 扩容所拿到list的elementDate是旧的,并不是线程A第一次扩容后对象, 线程B 拿到的 size还是 0 ,所以线程B 也认为自己是第一次add ,也需要扩容。

想一下 A 、B 线程的并发 一起进入扩容场景:

那么线程A 是第一次add的时候,他知道他要去扩容,他自己扩容完,自己整了个list的新elementDate ,然后 就开始赋值 elementDate[size++] = A的UUID值。

在线程A这个操作的过程中,线程 B 在做什么?

线程 B一开始 不巧也是以为要扩容,他拿着一个旧的 list的elementDate 也整了一个新的数组 ,

然后把 整个 list的 elementDate 引用指向 B线程自己弄出来的对象

this.elementData = B新构建的对象(这对象全部值为null);

然后做什么?

ArrayList为什么不是线程安全的,如何保证线程安全?

然后 线程B 开始执行 elementDate[size++] = B的UUID值。

线程A 的值赋值在他创建出来的 elementDate 里面,然后触发 size++ 。

但是线程 B 呢, 把 this.elementData 指向了自己的新弄出来的, 所以 A 的值无情被抛弃,但是线程 B 开始赋值的时候,

看看这个size在源码里的情况:

size是大家共用的,size 被线程A 加1了 ,所以就出现 线程 B 赋值的时候执行 elementDate[size++] = B的UUID值,出来的结果是:

[null , B的UUID值]

情况④:

java.util.ConcurrentModificationException 并发冲突

ArrayList为什么不是线程安全的,如何保证线程安全?

直接定位报错函数:

ArrayList为什么不是线程安全的,如何保证线程安全?

modCount是修改记录数,expectedModCount是期望修改记录数;

初始化的时候 expectedModCount=modCount ;

ArrayList的add函数、remove函数 操作都有对modCount++操作,当expectedModCount和modCount值不相等, 那就会报并发错误了

怎么办? 怎么安全起来?

1.使用 Vector :

List<String> resultList = newVector<>();

看看vector怎么保证安全的,add 方法synchronized加锁实现

ArrayList为什么不是线程安全的,如何保证线程安全?

主意:Vector是一个线程安全的列表,底层采用数组实现。其线程安全的实现方式非常粗暴:Vector大部分方法和ArrayList都是相同的,只是加上了synchronized关键字,这种方式严重影响效率,因此,不再推荐使用Vector了。JAVA官方文档中这样描述:如果不需要线程安全性,推荐使用ArrayList替代Vector。

2.使用 Collections里面的synchronizedList:

List<String> resultList =Collections.synchronizedList(new ArrayList<>());

synchronizedList同步块保证安全的:

ArrayList为什么不是线程安全的,如何保证线程安全?

但是迭代器未加锁,需要手动实现同步:

ArrayList为什么不是线程安全的,如何保证线程安全?

所以使用Collections.synchronizedList注意两个地方:

1.迭代操作必须加锁,可以使用synchronized关键字修饰;

2.synchronized持有的监视器对象必须是synchronized (list),即包装后的list,使用其他对象如synchronized (new Object())会使add,remove等方法与迭代方法使用的锁不一致,无法实现完全的线程安全性。

3.使用 CopyOnWriteArrayList :

List<String> resultList = new CopyOnWriteArrayList();

CopyOnWriteArrayList 使用ReentrantLock保证安全的:

ArrayList为什么不是线程安全的,如何保证线程安全?

CopyOnWriteArrayList 的set 也是上锁:

ArrayList为什么不是线程安全的,如何保证线程安全?

1.set add remove 都选择使用了Arrays.copyOf复制操作

2.get 多线程过程读取数据不是实时,那就可能出现 数据不一致问题,但是最终数据是一致的(读多写少就很合适)。

以上分析借鉴https://blog.csdn.net/qq_35387940/article/details/129611551,非常感谢!

CopyOnWriteArrayList是java.util.concurrent包下面的一个实现线程安全的List,顾名思义,CopyOnWriteArrayList在进行写操作(add,remove,set等)时会进行Copy操作,可以推测出在进行写操作时CopyOnWriteArrayList性能应该不会很高。

可以看到CopyOnWriteArrayList底层实现为Object[] array数组。

每次添加元素时都会进行Arrays.copyOf操作,代价非常昂贵。

读的时候是不需要加锁的,直接获取。删除和增加是需要加锁的。

有两点必须讲一下。我认为CopyOnWriteArrayList这个并发组件,其实反映的是两个十分重要的分布式理念:

(1)读写分离

我们读取CopyOnWriteArrayList的时候读取的是CopyOnWriteArrayList中的Object[] array,但是修改的时候,操作的是一个新的Object[] array,读和写操作的不是同一个对象,这就是读写分离。这种技术数据库用的非常多,在高并发下为了缓解数据库的压力,即使做了缓存也要对数据库做读写分离,读的时候使用读库,写的时候使用写库,然后读库、写库之间进行一定的同步,这样就避免同一个库上读、写的IO操作太多。

(2)最终一致

对CopyOnWriteArrayList来说,线程1读取集合里面的数据,未必是最新的数据。因为线程2、线程3、线程4四个线程都修改了CopyOnWriteArrayList里面的数据,但是线程1拿到的还是最老的那个Object[] array,新添加进去的数据并没有,所以线程1读取的内容未必准确。不过这些数据虽然对于线程1是不一致的,但是对于之后的线程一定是一致的,它们拿到的Object[] array一定是三个线程都操作完毕之后的Object array[],这就是最终一致。最终一致对于分布式系统也非常重要,它通过容忍一定时间的数据不一致,提升整个分布式系统的可用性与分区容错性。当然,最终一致并不是任何场景都适用的,像火车站售票这种系统用户对于数据的实时性要求非常非常高,就必须做成强一致性的。

性能对比

通过前面的分析可知

Vector对所有操作进行了synchronized关键字修饰,性能应该比较差

CopyOnWriteArrayList在写操作时需要进行copy操作,读性能较好,写性能较差

Collections.synchronizedList性能较均衡,但是迭代操作并未加锁,所以需要时需要额外注意

测试结果:

ArrayList为什么不是线程安全的,如何保证线程安全?

总结:

  • CopyOnWriteArrayList的写操作与Vector的遍历操作性能消耗尤其严重,不推荐使用。

  • CopyOnWriteArrayList适用于读操作远远多于写操作的场景。

  • Vector读写性能可以和Collections.synchronizedList比肩,但Collections.synchronizedList不仅可以包装ArrayList,也可以包装其他List,扩展性和兼容性更好。

三个方法对比借鉴 原文链接:https://blog.csdn.net/lkxsnow/article/details/104130143,非常感谢!文章来源地址https://www.toymoban.com/news/detail-468367.html

到了这里,关于ArrayList为什么不是线程安全的,如何保证线程安全?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • SimpleDateFormat为什么是线程不安全的?

    大家好,我是哪吒。 在日常开发中,Date工具类使用频率相对较高,大家通常都会这样写: 这很简单啊,有什么争议吗? 你应该听过“时区”这个名词,大家也都知道,相同时刻不同时区的时间是不一样的。 因此在使用时间时,一定要给出时区信息。 对于当前的上海时区和

    2024年02月20日
    浏览(53)
  • 再谈StringBuilder为什么线程不安全以及带来的问题

    比较有意思的是,学习锁消除的过程中,有人讲到StringBuffer在方法内构建,不会被其他方法引用时,StringBuffer的锁会被消除, 于是,顺便看了一下同源的StringBuidler为什么线程不安全,以及为什么多线程不安全,和带来的问题, 有了这篇文章,分享出来,帮助读者轻松应对知

    2024年02月11日
    浏览(44)
  • Java基础:为什么hashmap是线程不安全的?

    HashMap 是线程不安全的主要原因是它的内部结构和操作不是线程安全的。下面是一些导致 HashMap 线程不安全的因素: 非同步操作:HashMap 的操作不是线程同步的,也就是说,在多线程环境下同时对 HashMap 进行读写操作可能会导致数据不一致的问题。 非原子操作:HashMap 的操作

    2024年02月10日
    浏览(40)
  • 为什么聊天机器人界面不是未来

    ​ 比如: 0 按时上下班,用固定时间长度获取价值 1 创业,用非线性时间,获取真实价值 0-1 之间有无限多种状态 shadow ChatBot目前的交互界面有非常多值得被改进的体验机会。最近看到一篇非常有启发性的文章,分享给大家。 核心观点来自于文章: https://wattenberger.com/though

    2024年02月03日
    浏览(51)
  • 为什么字节大量用GO而不是Java?

    见字 如面,我是军哥。 我看很多程序员对字节编程语言选型很好奇,为此我还特地问了在字节的两位4-1的技术大佬朋友,然后加上自己的思考,总结了一下就以下 2 个原因: 1、 选型上没有历史包袱 字节的早期的程序员大多来自于百度、360,本身就是 php / c++ 的背景,一开

    2024年02月08日
    浏览(61)
  • 为什么说ChatGPT还不是搜索引擎的对手

    1950年,英国科学家图灵在一篇论文中预言,人类有可能创造出具有真正智能的机器。 著名的「图灵测试」就此诞生:如果一台机器能够与人类展开对话,而不被辨别出其机器身份,那么称这台机器具有智能。 也是从那时开始,人类世界开始了对人工智能长达半个多世纪的探

    2024年02月11日
    浏览(56)
  • 为什么DNS使用UDP而不是TCP详解!

    DNS(Domain Name System)使用UDP(User Datagram Protocol)而不是TCP(Transmission Control Protocol)的主要原因是出于性能和效率的考虑。下面详细解释为什么DNS选择使用UDP协议: 小型请求和快速响应:DNS查询通常是小型请求,仅需要几个字节的数据传输。UDP是无连接的协议,它不需要在通

    2024年02月02日
    浏览(48)
  • 为什么说HTTPS比HTTP安全? HTTPS是如何保证安全的?

    在上篇文章中,我们了解到 HTTP 在通信过程中,存在以下问题: 通信使用明文(不加密),内容可能被窃听 不验证通信方的身份,因此有可能遭遇伪装 而 HTTPS 的出现正是解决这些问题, HTTPS 是建立在 SSL 之上,其安全性由 SSL 来保证 在采用 SSL 后, HTTP 就拥有了 HTTPS 的加密

    2024年03月19日
    浏览(56)
  • MySQL为什么使用B+树,而不是B树?

    在MySQL中,B+树被广泛应用于索引结构,因为它支持高效的范围查询和区间扫描,并且有助于减少磁盘I/O操作,从而提高查询效率。为什么MySQL使用B+树而不是B树?主要有以下几个原因: 1、B+树可以更好地利用磁盘预读特性 在数据库中,数据通常都存储在磁盘上。而磁盘的读

    2024年02月13日
    浏览(44)
  • 笔记:TCP握手为什么是3次而不是2次?

    这个问题比较常见,这里简单总结一下。 一、两次握手建立连接:流程说明: 1)客户端发送SYN。 2)服务端收到SYN请求后,服务端回复SYN+ACK,然后进入已连接状态。 3)客户端收到SYN+ACK回复后,进入已连接状态。 二、两次握手建立连接:存在的问题 若客户端发送SYN后,没

    2023年04月13日
    浏览(57)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包