面试官问 : 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());
    }

 代码简析:

大家猜想结果是多少 ? 

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

  

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

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

对这个 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());
    }

可以看到结果是1了 :

接下来我们把线程数改成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个线程 不争不抢 :

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

 

情况②:

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

 

情况③:

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

 

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

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

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

 

 再看看 add 函数的 源码 :

 

 ensureExplicitCapacity ()函数:


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

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

elementData[size++] = e :

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

大家看出来问题没?

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

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

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

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


而 通过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 ,也需要扩容。

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

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

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

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

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

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

然后做什么?

然后 线程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 并发冲突

直接定位报错函数:

 

 这个其实 之前分析过:

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

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

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

怎么办? 怎么安全起来?

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

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

看看vector怎么保证安全的:

 

其次 是 使用 Collections里面的synchronizedList :
 

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

 看看synchronizedList 怎么保证安全的:

 

还有可以使用 CopyOnWriteArrayList :

 List<String> resultList  = new CopyOnWriteArrayList();

  看看CopyOnWriteArrayList 怎么保证安全的:

 ps:
CopyOnWriteArrayList 的set 也是上锁

 但是get 没有, 也就是说,get可能在多线程场景使用,拿到的是旧数据是可能的(也就是当前能读到的list里面的数据)

 

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

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

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

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

好了,该篇就到这吧。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/916.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【基础算法】单链表的OJ练习(5) # 环形链表 # 环形链表II # 对环形链表II的解法给出证明(面试常问到)

文章目录前言环形链表环形链表 II写在最后前言 本章的OJ练习相对于OJ练习(4)较为简单。不过&#xff0c;本章的OJ最重要的是要我们证明为何可以这么做。这也是面试中常出现的。 对于OJ练习(4)&#xff1a;-> 传送门 <-&#xff0c;分割链表以一种类似于归并的思想解得&a…

ChatGPT-4 终于来了(文末附免费体验地址)

大家好&#xff0c;我是小钱学长。 ChatGPT4.0 重磅来袭&#xff0c;今天一打开plus页面出现的就是这个GPT-4的体验界面&#xff01;现在就带大家一起看看GPT4.0​。 进入之后是这样的 看到最下面有一行话&#xff0c;目前应该是4个小时限制100条消息。 GPT-4有什么优势&…

手把手学会DFS (递归入门)

目录 算法介绍 递归实现指数型枚举 递归实现排列型枚举 递归实现组合型枚举 算法介绍 &#x1f9e9;DFS 即 Depth First Search &#xff0c;中文又叫深度优先搜索&#xff0c;是一种沿着树的深度对其进行遍历&#xff0c;直到尽头之后再进行回溯&#xff0c;再走其他路线的…

springboot复习(黑马)

学习目标基于SpringBoot框架的程序开发步骤熟练使用SpringBoot配置信息修改服务器配置基于SpringBoot的完成SSM整合项目开发一、SpringBoot简介1. 入门案例问题导入SpringMVC的HelloWord程序大家还记得吗&#xff1f;SpringBoot是由Pivotal团队提供的全新框架&#xff0c;其设计…

GPT-4技术报告

摘要 链接&#xff1a;https://cdn.openai.com/papers/gpt-4.pdf 我们汇报了GPT-4的发展&#xff0c;这是一个大规模的多模态模型&#xff0c;可以接受图像和文本输入并产生文本输出。虽然在许多现实场景中&#xff0c;GPT-4的能力不如人类&#xff0c;但它在各种专业和学术基…

数智链接,新一代校园招聘解决方案

疫情3年市场巨变&#xff0c;00后新生代初登上求职舞台&#xff0c;中和作用下&#xff0c;牛客发现新生代求职发生明显变化&#xff0c;企业校招也要随之而变&#xff0c;并率先提出以种草、精准、专业为特点的新一代校园招聘解决方案。01.学生求职变了&#xff01;安全感、非…

奇异值分解(SVD)原理与在降维中的应用

奇异值分解(SVD)原理与在降维中的应用 奇异值分解(Singular Value Decomposition&#xff0c;以下简称SVD)是在机器学习领域广泛应用的算法&#xff0c;它不光可以用于降维算法中的特征分解&#xff0c;还可以用于推荐系统&#xff0c;以及自然语言处理等领域。是很多机器学习算…

GPT-4来袭:开启人工智能新时代

文章目录介绍GPT4 模型演示示例示例 1示例 2示例 3示例 4示例 5最后Reference介绍 2023年3月15日&#xff0c;OpenAI公司正式发布了先进的自然语言处理模型GPT-4&#xff0c;前不久发布的GPT-3.5模型只能理解文字的语言模型&#xff0c;而新发布的GPT4则是多模态模型&#xff…

【java】了解常见集合类

了解常见集合类 一、集合类框架 1、集合类框架结构图 首先我们要对集合类结构有一个大体的认识&#xff0c;所有集合都继承于迭代器&#xff0c;分为单列集合和映射集合&#xff0c;单列集合分为有序可重复和有序不可重复&#xff0c;大概结构如下图所示 2、主要集合类的介…

你真的知道如何系统高效地学习数据结构与算法吗?

文章目录前言&#xff1a;什么是数据结构&#xff1f;什么是算法&#xff1f;学习这个算法需要什么基础&#xff1f;学习的重点在什么地方&#xff1f;一些可以让你事半功倍的学习技巧1.边学边练&#xff0c;适度刷题2.多问、多思考、多互动3.打怪升级学习法4.知识需要沉淀&…

文心一言---中国版的“ChatGPT”狂飙的机会或许要出现了

⭐️我叫忆_恒心&#xff0c;一名喜欢书写博客的在读研究生&#x1f468;‍&#x1f393;。 如果觉得本文能帮到您&#xff0c;麻烦点个赞&#x1f44d;呗&#xff01; 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧&#xff0c;喜欢的小伙伴给个三…

linux 基础

1.Shell 命令的格式如下&#xff1a;command -options [argument]command: Shell 命令名称。options&#xff1a; 选项&#xff0c;同一种命令可能有不同的选项&#xff0c;不同的选项其实现的功能不同。argument&#xff1a; Shell 命令是可以带参数的&#xff0c;也可以不带参…

【计算机二级Python】综合题目

计算机二级python真题 文章目录计算机二级python真题一、简单应用——明星投票二、综合应用题《评奖学金 两问》一、简单应用——明星投票 描述使用字典和列表型变量完成最有人气的明星的投票数据分析。投票信息由附件里的文件vote.txt给出,一行只有一个明星姓名的投票才是有效…

【BLE 5.3无线MCU CH582】1、初识CH582开发板(开箱)

1、认识板子 优点&#xff1a; &#xff08;1&#xff09;引脚全部引出&#xff1b; &#xff08;2&#xff09;USB下载程序&#xff1b; &#xff08;3&#xff09;TYPE-C接口好评&#xff1b; &#xff08;4&#xff09;板载连个两个USB口&#xff0c;都可以供电&#xff1b;…

前端性能优化之HTTP缓存

前端缓存 前端缓存可分为两大类&#xff1a;HTTP 缓存和浏览器缓存。 我们今天重点是 HTTP 缓存&#xff0c;下面这张图是前端缓存的一个大致知识点&#xff1a; HTTP 缓存 首先解决困扰绕人们的老大难问题&#xff1a; 一、什么是HTTP缓存&#xff1f; HTTP 缓存会存储与请…

六个实用技巧让你轻松写出优雅的链表代码

文章目录&#x1f4d5;前言&#xff1a;如何轻松写出正确的链表代码&#xff1f;&#x1f4d6;技巧一&#xff1a;理解指针或引用的含义&#x1f4d6;技巧二&#xff1a;警惕指针丢失和内存泄漏&#x1f4d6;技巧三&#xff1a;利用哨兵简化实现难度&#x1f4d6;技巧四&#x…

HTTP 协议

文章目录1. 前言2. HTTP 协议3. fiddler 的安装与认识4. HTTP 协议报文格式4.1 请求4.2 响应5. 构造 HTTP 请求5.1 基于 form 表单构造 HTTP 请求5.2 基于 ajax 构造 HTTP 请求6. postman7. HTTPS7.1 加密7.2 HTTPS 的工作过程1. 前言 前面几篇文章 &#xff0c; 说了关于 前端…

C++继承[万字详解]

目录 一.继承的介绍 1.1、继承的概念 1.2、继承的定义 1.2.1、定义格式 1.2.2、继承关系和访问限定符 1.2.3、继承基类成员后&#xff0c;在子类中成员访问方式的变化 二.基类和派生类对象赋值转化 三.继承中的作用域 四.派生类的默认成员函数 ★派生类的构造函数 派…

有关pytorch的一些总结

Tensor 含义 张量&#xff08;Tensor&#xff09;&#xff1a;是一个多维数组&#xff0c;它是标量、向量、矩阵的高维拓展。 创建 非随机创建 1.用数组创建 将数组转化为tensor np.ones([a,b]) 全为1 #首先导入PyTorch import torch#数组创建 import numpy as np anp.arr…

4.类的基本概念

目录 4.1 类的概述 类是一种活动的数据结构 4.2 程序和类&#xff1a;一个快速实例 4.3 声明类 ​4.4 类成员 4.4.1 字段 1.显示和隐式字段初始化 2. 声明多个字段 4.4.2 方法 4.5 创建变量和类的实例 4.6 为数据分配内存 合并这两个步骤 4.7 实例成员 4.8 访问修饰…