本文首发于公众号,关注文末公众号,阅读体验更佳。html
这是我的第10篇原创文章java
全文共计7362个字,46张图。分析的较为详尽,并进行了相关知识点的扩展,因此篇幅较长,建议转发朋友圈或者本身收藏起来,慢慢阅读。面试
一.题是什么题?数据库
二.阿里Java开发规范。apache
2.1 正例代码。
2.2 反例代码。
三.层层揭秘,为何发生异常了呢?复制代码
3.1 第一层:异常信息解读。
3.2 第二层:抛出异常的条件解读。
3.3 第三层:什么是modCount?它是干啥的?何时发生变化?
3.4 第四层:什么是expectedModCount?它是干啥的?何时发生变化?
3.5 第五层:组装线索,直达真相。
四.这题的坑在哪?复制代码
4.1 回头再看。
4.2 还有一个骚操做。
五.线程安全版的ArrayList。复制代码
六.总结一下。数组
七.回答另一个面试题。缓存
八.扩展阅读。安全
7.1 fail-fast机制和safe-fast机制。
7.2 Java语法糖。
7.3 阿里Java开发手册。
九.最后说一句。复制代码
我第一次遇到这个题的时候,是在一个微信群里,阿里著名的"Java劝退师"小马哥抛出了这样的一个问题:微信
而后你们纷纷给出了本身的看法(注:删除了部分聊天记录):并发
后面在另外的群里聊天的时候(注:删除了部分聊天记录),我也抛出了这样的问题:
总结一下图片中的各类回答:
1.什么也不会发生,remove以后,list中的数据会被清空。
2.remove的方法调用错误,入参应该是index(数组下标)。
3.并发操做的时候会出现异常。
4.会发生ConcurrentModifyException。
你的答案又是什么呢?
在这里,我先不说正确的答案是什么,也先不评价这些回答是对是错,咱们一块儿去探索真相,寻找答案。
有人看到题的第一眼(没有认真读题),就想起了阿里java开发手册(先入为主),里面是这样说的:
正是由于大多数人都知道而且读过这个规范(毕竟是业界权威)。因此呼声最高的答案是【会发生ConcurrentModifyException】。由于他们知道阿里java开发手册里面是强制要求:不要在foreach循环里面进行元素的remove/add操做。remove元素请使用Iterator方式,若是并发操做,须要对Iterator对象加锁。
可是不能由于他是权威,咱们就全盘接受吧?
因此咱们眼见为实,先把手册里面提到的【正例代码】跑一下,以下:
细心的读者可能发现了:咦,这个代码的22行为啥颜色不同呢?
我帮你看看。
替换以后的代码是这样的:
从上面咱们能够获得一个结论.......
等等,到这一步你就想获得结论了?你不对【一行代码为何就替换了七行代码】好奇吗?
看到真相的时候,有时候再往前一步就是本质了。
源码之下无秘密,我再送你一张图,JDK1.8中Collection.removeIf的源码:
好了,已经到源码级别了,从这里咱们验证了,阿里java开发手册里面的正例是对的,并且我还想给他加上一句:若是你的JDK版本是1.8以上,没有并发访问的状况下,可使用Collection.removeIf(Predicate filter)方法。使代码更加优雅。
接下来咱们看看【反例代码】的运行结果:
从执行结果来看,和咱们预期的结果是一致。看着没有问题呀?
可是你别忘了,下面还有一句话啊:
咱们执行试一试:
什么状况?真的是"出乎意料"啊!
把删除元素的条件从【公众号】修改成【why技术】就发生了异常:java.util.ConcurrentModificationException
咱们如今明白为何阿里强制要求不要在foreach循环里面进行元素的remove/add操做,由于会发生异常了。
可是开发手册里面并无告诉你,为何会发生异常。须要咱们本身层层深刻,积极探索。
因此这一小节咱们就一块儿探索,为何会发生异常。咱们再解析一下程序的运行结果,以下:
正如上图里面异常信息的体现,异常是在代码的第21行触发的。而代码的第21行,是一个foreach循环。foreach循环是Java的语法糖,咱们能够从编译后的class文件中看出,以下图所示:
请注意图中的第26行代码:list.remove(item) (这句话很关键!!!)很关键,很重要,后面会讲到。
这也解释了,异常信息里面的这一个问题:
好了,到这一步,咱们把异常信息都解读完毕了。
我再看看真实抛出异常的那一个方法:
很简单,很清晰的四行代码。抛出异常的条件是:modCount !=expectedModCount
因此,咱们须要解开的下两层面纱就是下面两大点:
第一:什么是modCount?它是干啥的?何时发生变化?
第二:什么是expectedModCount?它是干啥的?何时发生变化?
先来第一个:什么是modCount?
modCount上的注释很长,我只截取了最后一段。在这一段中,提到了两个关键点。
1.modCount这个字段位于java.util.AbstractList抽象类中。
2.modCount的注释中提到了"fail-fast"机制。
3.若是子类但愿提供"fail-fast"机制,须要在add(int,E)方法和remove(int)方法中对这个字段进行处理。
4.从第三点咱们知道了,在提供了"fail-fast"机制的容器中(好比ArrayList),除了文中示例的remove(Obj)方法会致使ConcurrentModificationException异常,add及其相关方法也会致使异常。
知道了什么是modCount。那modCount是干啥的呢?
在提供了"fail-fast"机制的集合中,modCount的做用是记录了该集合在使用过程当中被修改的次数。
证据就在源码里面,以下:
这是java.util.ArrayList#add(int, E)方法的源码截图:
这是java.util.ArrayList#remove(int)方法的源码截图:
注:这里不讨论手动设置为null是否对GC有帮助,我我的认为,在这里有这一行代码并无坏处。在实际开发过程当中,通常不须要考虑到这点。
同时,上面的源码截图也回答了这一层的最后一个问题:它何时被修改?
拿ArrayList来讲,当调用add相关和remove相关方法时,会触发modCount++操做,从而被修改。
好了,经过上面的分析,咱们知道了什么是modCount和modCount是干啥的。准备进入第四层。
接下来:什么是expectedModCount?
expectedModCount是ArrayList中一个名叫Itr内部类的成员变量。
第二问:expectedModCount它是干啥的:
它表明的含义是在这个迭代器中,预期的修改次数
第三问:expectedModCount何时发生变化?
状况一:从上图中也能够看出当Itr初始化的时候,会对expectedModCount字段赋初始值,其值等于modCount。
状况二:以下图所示,调用Itr的remove方法后会再次把modCount的值赋给expectedModCount。
换句话说就是:调用迭代器的remove会维护expectedModCount=modCount。(这句话很关键!!!)
好了分析到了这里,咱们知道了下面这个六连击:
1.什么是modCount?
2.modCount是干啥的?
3.modCount何时发生变化?
4.什么是expectedModCount?
5.expectedModCount是干啥的?
6.expectedModCount何时发生变化?
为何发生了异常呢?
若是说前四层是线索的话,真相其实已经隐藏在线索里面了。我带你梳理一下:
【第一层:异常信息解读】中说到:
【第二层:抛出异常的条件解读】中说到:
【第三层:什么是modCount?它是干啥的?何时发生变化?】中说到:
【第四层:什么是expectedModCount?它是干啥的?何时发生变化?】中说到:
为何发生了异常呢?我想你大概已经有了一个答案了,我再去Debug一下,为了方便演示,咱们去掉语法糖,程序修改以下:
并确认一下这个循环体会执行三次,以下:
第一次循环
第一次循环取出的【公众号】,不知足条件if("why技术".equals(item)),不会触发list.remove(Obj)方法。
第二次循环
如图所示,第二次循环取到了“why技术”。知足条件if("why技术".equals(item)),会触发list.remove(Obj)方法,以下所示:
第三次循环
总结一下在foreach循环里面进行元素的remove/add操做抛出异常的真相:
由于foreach循环是Java的语法糖,通过编译后还原成了迭代器。
可是从通过编译后的代码的第26行能够看出,remove方法的调方是list,而不是迭代器。
通过前面的源码分析咱们知道,因为ArrayList的"fail-fast"机制,调用remove方法会触发【modCount++】操做,对expectedModCount没有任何操做。只有调用迭代器的remove方法,才会维护expectedModCount=modCount。
因此调用了list的remove方法后,再调用Itr的next方法时,致使了expectedModCount!=modCount,抛出异常。
前面讲了阿里开发手册。讲了在foreach循环里面进行元素的remove/add为何会发生异常。有了这些铺垫以后。
咱们再回过头来看小马哥出的这个题:
我靠,这乍一看,foreach循环里面调用list.remove(obj)。咱们刚刚分析过,会抛出ConcurrentModificationException异常。
你要这样答,你就进了小马哥的坑了。
这个题的坑在这三个点里面。小马哥并无说这个list是ArrayList吧?若是你没有认真审题,先入为主的默认了这个list就是ArrayList。第一步就错了。
这是真正的高手,借力打力。借阿里开发手册的力,让你第一步就走错。
请看下面这张图:
当使用CopyOnWriteArrayList的时候,程序正常执行。
既然咱们知道为何会抛出异常,也知道怎么不抛出异常,List原本就是一个接口,那咱们是否是能够实现这个接口,弄一个自定义的List呢?
好比下面的这个WhyTechnologyList,就是我本身的List,狸猫换太子,这操做,够"骚"啊。
只有掌握了原理,咱们想怎么玩就怎么玩。
CopyOnWriteArrayList是什么?咱们看一下源码注释上面是怎么说的:
相对于ArrayList而言,CopyOnWriteArrayList集合是线程安全的容器。在遍历的时候,因为它操做是数组的"快照","快照"不会发生变化。因此它不须要额外加锁,也不会抛出ConcurrentModificationException异常。
咱们主要看一下,示例程序中用到的三个方法,add(E e)、next()、remove(Obj)
先看add(E e)方法:
咱们看一下它的next()方法:
再看一下它的remove(Obj)方法:
next、remove都是操做的快照,并无看到ArrayList里面的modCount和expectedModCount。因此它没有抛出ConcurrentModificationException
以前看小马哥说的这句话的时候还不太明白集合和一致性之间的关系(老问题,仍是先入为主,一说到一致性首先想到的是缓存和数据库之间的一致性)。
可是当我阅读源码,从add方法能够看出CopyOnWriteArrayList并不保证数据的实时一致性。只能保证最终一致性。
同时咱们从源码中能够看出CopyOnWriteArrayList增删改数据的时候须要搞一个"快照",这一点是比较耗内存的,使用过程当中须要注意。
咱们再回到最开始的地方,看看你们的回答:
1.什么也不会发生,remove以后,list中的数据会被清空。
2.remove的方法调用错误,入参应该是index(数组下标)。
3.并发操做的时候会出现异常。
4.会发生ConcurrentModifyException。
如今,你知道这些回答的问题在哪里了吧?这一部分的总结也很简单,上一个对比图就行了:
如今面试官常常问的一个问题,你读过源码吗?
咦,巧了。你看了这篇文章,就至关于了读了ArrayList和CopyOnWriteArrayList的部分源码。
那你就能够这样回答啦:我以前看阿里Java开发手册的时候看到一条规则是不要在foreach循环里面进行元素的remove/add操做。remove元素请使用Iterator方式,若是并发操做,须要对Iterator对象加锁。我对这条规则很感兴趣,因此我对其进行了深刻的研究,阅读了ArrayList和CopyOnWriteArrayList的部分源码。
若是碰巧面试官也读过这块源码,这个问题,大家能够相谈甚欢。若是面试官没有读过这块源码,你能够给他讲的明明白白。
固然,还有一个前提是:我但愿你读完这篇文章后,若是是第一次知道这个知识点,那你能够本身实际操做一下。
看懂了是一回事,本身再实际操做一下,是另一回事。
文中屡次提到了"fail-fast"机制(快速失败),与其对应的还有"fail-safe"机制(失败安全)。
这种机制是一种思想,它不只仅是体如今Java的集合中。在咱们经常使用的rpc框架Dubbo中,在集群容错时也有相关的实现。
Dubbo 主要提供了这样几种容错方式:
Failover Cluster - 失败自动切换
Failfast Cluster - 快速失败
Failsafe Cluster - 失败安全
Failback Cluster - 失败自动恢复
Forking Cluster - 并行调用多个服务提供者
若是对这两种机制感兴趣的朋友能够查阅相关资料,进行了解。若是想要了解Dubbo的集群容错机制,能够看官方文档,地址以下:http://dubbo.apache.org/zh-cn/docs/sourcecodeguide/cluster.html
文中说到foreach循环的时候提到了Java的语法糖。若是对这一块有兴趣的读者,能够在网上查阅相关资料,也能够看看《深刻理解Java虚拟机》的第10.3节,有专门的介绍。
书中说到:
总而言之,语法糖能够看作是编译器实现的一些“小把戏”,这些“小把戏”可能会使得效率“大提高”,但咱们也应该去了解这些“小把戏”背后的真实世界,那样才能利用好它们,而不是被它们所迷惑。
关注公众号并回复关键字【Java】。便可得到此书的电子版。
阿里的孤尽大佬做为主要做者写的这本《阿里Java开发手册》,能够说是呕心沥血推出的业界权威,很是值得阅读。读完此书,你不只可以得到不少干货,甚至你还能读出一点技术情怀在里面。
对于技术情怀,孤尽大佬是这样的说的:
热爱、思考、卓越。热爱是一种源动力,而思考是一个过程,而卓越是一个结果。若是给这三个词加一个定语,使技术情怀更加立体、清晰地被解读,那就是奉献式的热爱,主动式的思考,极致式的卓越。
关注公众号并回复关键字【Java】。便可得到此书的电子版。
这篇文章写以前我一直在纠结,由于感受这个知识点其实我已经掌握了,那我还有写的必要吗?我在写的这个过程当中还能收获一些东西吗?
可是在写的过程当中,我翻阅了大量的源码,虽然以前已经看过,可是没有这样一行一行仔细的去分析。以前只是一个大概的模糊的影像,如今具象化清晰了起来,在这个过程当中,我仍是学到了不少不少。
其实想到写什么内容并不难,难的是你对内容的把控。关于技术性的语言,我是反复推敲,查阅大量文章来进行证伪,总之慎言慎言再慎言,毕竟作技术,我认为是一件很是严谨的事情,我经常想象本身就是在故宫修文物的工匠,在工匠精神的认知上,目前我可能和他们还差的有点远,可是我时常以工匠精神要求本身。就像我以前表达的:对于技术文章(由于我偶尔也会荒腔走板的聊一聊生活,写一写书评,影评),我尽可能保证周推,全力保证质量。
文中提到的两本书《深刻理解Java虚拟机》和《阿里Java开发手册》是两本很是优秀,值得反复阅读的工具书,能够关注我后,在后台发送java,便可得到电子书。
才疏学浅,不免会有纰漏,若是你发现了错误的地方,还请你留言给我指出来,我对其加以修改。
若是你以为文章还不错,你的点赞、留言、转发、分享、赞扬就是对我最大的鼓励。
另外,若是小马哥本尊能读到这个文章,读到这段话,我想在这里表达对他的敬意。同时也想催更一下:小马哥,每日一问很久没更新啦,很是怀恋那种被"坑"的明明白白的感受!
以上。
谢谢您的阅读,感谢您的关注。
欢迎关注公众号【why技术】。在这里我会分享一些技术相关的东西,主攻java方向,用匠心敲代码,对每一行代码负责。偶尔也会荒腔走板的聊一聊生活,写一写书评,影评。愿你我共同进步。