金三银四 ?
也许,但是。
近日,又收到金三银四一线作战小队成员反馈的战况 :

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

本篇内容预告:
ArrayList 是线程不安全的, 为什么 ?
① 结合代码去探一探所谓的不安全
② 我们弄清楚为什么不安全(结合源码以及我的个人讲述)
③ 不止步于为什么, 我们得知道怎么办(方案以及结合源码分析)
ps: 这篇文章 注定篇幅很长, 我会从非常非常小白0基础的角度去 很啰嗦地去讲一些内容。
距离上一次 这么臭长去讲 list集合相关的问题,还是21年的时候 ,个人认为也是很有学习价值的,大家也可以看看,但是注意就是 ,别看着看着回不来了,也是上万文字+图片+源码分析的文章:
开整开整。
线程不安全 ,看看官腔怎么说:
线程不安全,是指不提供加锁机制保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
其实小白话就是 :
多线程操作的时候 ,容易出现与我们预想不一致的结果。
就比如说,你做好准备 接我两拳。
本来你以为 我是打完一拳再打一拳。
结果我直接一招双龙出海,两只手一起打你, 你顶得住么?(你根本防不住。)
开始结合代码一探究竟。
代码小栗子 ① :
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 多线程过程读取数据不是实时,那就可能出现 数据不一致问题,但是最终数据是一致的(读多写少就很合适)。
好了,该篇就到这吧。
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
为什么4.1%2返回0.0999999999999996?但是4.2%2==0.2。 最佳答案 参见此处:WhatEveryProgrammerShouldKnowAboutFloating-PointArithmetic实数是无限的。计算机使用的位数有限(今天是32位、64位)。因此计算机进行的浮点运算不能代表所有的实数。0.1是这些数字之一。请注意,这不是与Ruby相关的问题,而是与所有编程语言相关的问题,因为它来自计算机表示实数的方式。 关于ruby-为什么4.1%2使用Ruby返
我正在编写一个小脚本来定位aws存储桶中的特定文件,并创建一个临时验证的url以发送给同事。(理想情况下,这将创建类似于在控制台上右键单击存储桶中的文件并复制链接地址的结果)。我研究过回形针,它似乎不符合这个标准,但我可能只是不知道它的全部功能。我尝试了以下方法:defauthenticated_url(file_name,bucket)AWS::S3::S3Object.url_for(file_name,bucket,:secure=>true,:expires=>20*60)end产生这种类型的结果:...-1.amazonaws.com/file_path/file.zip.A
它不等于主线程的binding,这个toplevel作用域是什么?此作用域与主线程中的binding有何不同?>ruby-e'putsTOPLEVEL_BINDING===binding'false 最佳答案 事实是,TOPLEVEL_BINDING始终引用Binding的预定义全局实例,而Kernel#binding创建的新实例>Binding每次封装当前执行上下文。在顶层,它们都包含相同的绑定(bind),但它们不是同一个对象,您无法使用==或===测试它们的绑定(bind)相等性。putsTOPLEVEL_BINDINGput
我可以得到Infinity和NaNn=9.0/0#=>Infinityn.class#=>Floatm=0/0.0#=>NaNm.class#=>Float但是当我想直接访问Infinity或NaN时:Infinity#=>uninitializedconstantInfinity(NameError)NaN#=>uninitializedconstantNaN(NameError)什么是Infinity和NaN?它们是对象、关键字还是其他东西? 最佳答案 您看到打印为Infinity和NaN的只是Float类的两个特殊实例的字符串
如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象
关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?