面试官问 : ArrayList 不是线程安全的,为什么 ?(看完这篇,以后反问面试官)

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

前言

金三银四 ?

也许,但是。

近日,又收到金三银四一线作战小队成员反馈的战况 :

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

我不管你从哪里看的面经,但是我不允许你看到我这篇文章之后,还不清楚这个面试问题。

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

本篇内容预告:
 

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

① 结合代码去探一探所谓的不安全 

② 我们弄清楚为什么不安全(结合源码以及我的个人讲述)

③ 不止步于为什么, 我们得知道怎么办(方案以及结合源码分析)

ps:  这篇文章 注定篇幅很长, 我会从非常非常小白0基础的角度去 很啰嗦地去讲一些内容。

距离上一次 这么臭长去讲 list集合相关的问题,还是21年的时候 ,个人认为也是很有学习价值的,大家也可以看看,但是注意就是 ,别看着看着回不来了,也是上万文字+图片+源码分析的文章:

Java 移除List中的元素,这玩意讲究!

开整开整。

正文

 看看它的不安全 以及 为什么不安全


线程不安全 ,看看官腔怎么说:
 

线程不安全,是指不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。

其实小白话就是 :

多线程操作的时候 ,容易出现与我们预想不一致的结果。

就比如说,你做好准备 接我两拳。

本来你以为 我是打完一拳再打一拳。

结果我直接一招双龙出海,两只手一起打你, 你顶得住么?(你根本防不住。)

开始结合代码一探究竟。


代码小栗子 ① :
 

    public static void main(String[] args) {

        int threadNum = 1;

        List<String> resultList = new ArrayList<>();
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> resultList.add(UUID.randomUUID().toString())).start();
        }
        System.out.println("我们最终得到的resultList大小:"+resultList.size());
    }

 代码简析:
arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

大家猜想结果是多少 ? 

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

是 0 , 为什么不是 1 ? 为什么会出现 0 ? 不是往里面ADD 了一个元素么 ? 

  

如果说你对这个 0  的结果很意外的话, 兄弟,你完了。

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

(吓你的,你本来要完了,还好你今天遇到了我)。

对这个 0  的结果很意外,代表你对线程方面的基础知识,可能还没了解。

简析:


因为for 里面 开了一个新的线程 new Thread , 这个线程 负责往 list 里面 add 一个数据。
但是 我们的打印 list.size 是 主线程 , 也就是说,如果 在 新的线程 new Thread 没执行完add 方法, 主线程就执行打印的代码,

那么就是 0啊 。

所以就是说,我们 主线程 等一等,让 for循环里面的新的线程 new Thread 先插入数据。

    public static void main(String[] args) throws InterruptedException {

        int threadNum = 1;

        List<String> resultList = new ArrayList<>();
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> resultList.add(UUID.randomUUID().toString())).start();
        }
        sleep(1000);
        System.out.println("我们最终得到的resultList大小:"+resultList.size());
    }

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

可以看到结果是1了 :

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

接下来我们把线程数改成10(另外主线程等5秒,给足够的时间让这个10个线程好好竞争一下) ,我们来看看 所谓的不安全 的ArrayList 能出现什么 ‘不安全’

    public static void main(String[] args) throws InterruptedException {

        int threadNum = 10;

        List<String> resultList = new ArrayList<>();
        for (int i = 0; i < threadNum; i++) {
            new Thread(()->{
                resultList.add(UUID.randomUUID().toString().substring(0,8));
                System.out.println(resultList);
            }).start();
        }
        sleep(5000);
        System.out.println("我们最终得到的resultList大小:"+resultList.size());
    }

情况①:

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

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

 显然这是不符合我们文章主题的,我们要看的是不安全。

 文章来源地址https://www.toymoban.com/news/detail-784170.html

情况②:

有竞争,但是线程们 很友好,所以也没出什么幺蛾子(仅仅对于往list塞数据这个动作来说)

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

 

情况③:

10个线程 显然还是 太少了, 而且我电脑机子又好, 终于出现 ‘不安全’情况了 ,非常难得。

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

 

多线程操作 ArrayList 导致出现 add赋值 出现 null  情景分析 :

为什么会出现,先看看源码 ,

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

Object[] elementData  : 保存所有元素值的 数组
size :  elementData中存储的元素个数

 

 再看看 add 函数的 源码 :

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

 

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

 ensureExplicitCapacity ()函数:


将当前的新元素加到列表后面,判断列表的 elementData 数组的大小是否满足。

如果 size + 1 的这个需求长度大于 elementData 这个数组的长度,那么就要对这个数组进行扩容。

elementData[size++] = e :

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

大家看出来问题没?

这两步没有和在一块操作。

也就说如果出现这个扩容的触发 和后面 赋值 并发情况 ,那么就有好戏看了。

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

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

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程


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

 这里又必须给大家推荐一篇好文章了:

(没错也是我写的,但是看到这,你别去看这篇,跟着我现在的思路继续分析 这个null值出现的情景,实在很感兴趣,自己一会再看)
Java ArrayList new出来,默认的容量到底是0还是10 ?

 看看我们的截图, 第一个数据是 null 。


有趣。

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

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

然后 我们是多线程操作场景, for循环第二次,触发new第二个线程B来了,线程B去add的时候,

因为线程A第一次扩容可能并没完成,所以导致 线程B 扩容所拿到list的elementDate是旧的,并不是线程A第一次扩容后对象, 线程B 拿到的 size还是 0 ,所以线程B 也认为自己是第一次add ,也需要扩容。

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

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

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

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

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

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

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

然后做什么?

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

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


这里的好玩点是什么?

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


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

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

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

    transient Object[] elementData;

   //这是大家共用的 size 
    private int size;
}


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

 

null 就是这么来的 !  能看到这的人,友情提示,你已经阅读了3500字。当然还没完事。

情况④:

java.util.ConcurrentModificationException 并发冲突

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

直接定位报错函数:

 arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

 这个其实 之前分析过:

modCount是修改记录数,expectedModCount是期望修改记录数;
初始化的时候 expectedModCount=modCount ;

ArrayList的add函数、remove函数 操作都有对modCount++操作,当expectedModCount和modCount值不相等, 那就会报并发错误了(其实这个不是仅仅是多线程的问题,是这个ArrayList 代码next函数的问题,更多细节可以有空看看 Java 移除List中的元素,这玩意讲究!)。 

那么到这 我们大概知道 这个 ArrayList的不安全 问题了, 说白了就是  2行代码没上锁操作。

怎么办? 怎么安全起来?

最简单的方式, 也是面经上经常看到的  使用 Vector :

List<String> resultList = new Vector<>();

看看vector怎么保证安全的:
arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

 

其次 是 使用 Collections里面的synchronizedList :
 

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

 看看synchronizedList 怎么保证安全的:

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

 

还有可以使用 CopyOnWriteArrayList :

 List<String> resultList  = new CopyOnWriteArrayList();

  看看CopyOnWriteArrayList 怎么保证安全的:

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

 ps:
CopyOnWriteArrayList 的set 也是上锁
arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

 但是get 没有, 也就是说,get可能在多线程场景使用,拿到的是旧数据是可能的(也就是当前能读到的list里面的数据)
arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

 

那么就CopyOnWriteArrayList的 set\add\get 函数,你能预料到它的不好点么?

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

arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程arraylist线程安全吗,跟我一起玩转 SpringBoot,java,arraylist,线程不安全,并发错误,线程

 所以存在 内存占用以及耗时问题,当数组元素越来越多的时候。

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

好了,该篇就到这吧。

 

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

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

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

相关文章

  • 得物面试:Redis用哈希槽,而不是一致性哈希,为什么?

    在40岁老架构师 尼恩的 读者交流群 (50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格,遇到很多很重要的面试题: Redis为何用哈希槽而不用一致性哈希? 最近有小伙伴在面试网易,又遇到了相关的面试题。

    2024年02月21日
    浏览(58)
  • java八股文面试[多线程]——为什么要用线程池、线程池参数

     速记7个: 核心、最大 存活2 队列 工厂 拒绝 线程池处理流程: 线程池底层工作原理: 线程复用原理:   知识来源: 【并发与线程】为什么使用线程池,参数解释_哔哩哔哩_bilibili 【并发与线程】线程池处理流程_哔哩哔哩_bilibili 【并发与线程】线程池的底层工作原理_哔哩

    2024年02月11日
    浏览(51)
  • ConcurrentHashMap为什么是线程安全的?

    1、ConcurrentHashMap的原理和结构 我们都知道Hash表的结构是数组加链表,就是一个数组中,每一个元素都是一个链表,有时候也把会形象的把数组中的每个元素称为一个“桶”。在插入元素的时候,首先通过对传入的键(key),进行一个哈希函数的处理,来确定元素应该存放于

    2024年02月07日
    浏览(62)
  • 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)
  • 面试官问我:线程锁导致的kafka客户端超时,如何解决?

    本文分享自华为云社区《线程锁导致的kafka客户端超时问题》,作者: 张俭 。 有一个环境的kafka client发送数据有部分超时,拓扑图也非常简单 我们先对客户端的环境及JVM情况进行了排查,从JVM所在的虚拟机到kafka server的网络正常,垃圾回收(GC)时间也在预期范围内,没有

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

    ​ 比如: 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)
  • 为什么DNS使用UDP而不是TCP详解!

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

    2024年02月02日
    浏览(48)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包