博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
集合中常见的 fail-fast 机制
阅读量:3947 次
发布时间:2019-05-24

本文共 3119 字,大约阅读时间需要 10 分钟。

本篇参考 《码出高效 Java 开发手册》

  • fail-fast 机制是集合世界中比较常见的错误检测机制,通常出现在遍历集合元素的过程中,下面通过校园生活中的一个例子来体会 fail-fast 机制。
  • 上课前,班长开始点名,刚点到一半,这时教师外面两两三三进来几位同学,同学们起哄:点错了!班长重新点名,点到中途,又出去几位同学,同学们又起哄说:点错了!班长又需要重新点名了,这就是 fail-fast 机制。它是一种对集合遍历操作时的错误检测机制,在遍历中途出现意料之外的修改时,通过 unchecked 异常暴力的反馈出来,这种机制经常出现在多线程环境下,当前线程会维护一个计数比较器,叫做 expectedModCount,记录已经修改过的次数,在进入遍历前,会把实时修改次数 modCount赋值给 expectedModCount,如果这两个数据不相等,则抛出异常。java.util 下的所有集合类都是 fail-fast ,而 concurrent 包中的集合类都是 fail-safe。与 fail-fast不同,fail-safe对于刚才点名被频繁打断的情形,相当于班长直接拿出手机快速拍照,然后跟据照片点名,不在关心同学们的进进出出。
  • 下面我们通过 ArrayList.subList()方法进一步阐述fail-fast这种机制,在某种情况下,需要从一个主列表 master 中获取子列表 branch,master 集合元素个数的增加或删除,均会导致子列表的遍历、增加、删除进而产生 fail-fast 异常,代码分析如下:
@Test	public void test01() {
List masterList = new ArrayList(); masterList.add("one"); masterList.add("two"); masterList.add("three"); masterList.add("four"); masterList.add("five"); List branchList = masterList.subList(0, 3); System.out.println(branchList); // 下方三行代码,如果不注释掉,则会导致 branchList 操作出现异常 (`第一处`) masterList.remove(0); masterList.add("ten"); masterList.clear(); // 下方四行全部执行成功 branchList.clear(); branchList.add("six"); branchList.add("seven"); branchList.remove(0); // 正常遍历结束 :只有一个元素:seven for (Object t : branchList) {
System.out.println(t); } // 子列表修改导致主列表也被动修改,输出:[seven, four, five] System.out.println(masterList); }
  • 第一处说明,如果不注释掉,masterList 的任何有关于元素个数的修改操作都会导致 branchList 的 ”增删改查“ 抛出 ConcurrentModificationException 异常,在实际调研中,大部分程序员知道 subList 子列表无法序列化,也知道它的修改会导致主列表的修改,但是并不知道主列表个数的改动会让子列表如此敏感,频频抛出异常,在实际代码中,这样的故障案例属于常见的类型, subList 方法返回的是内部类 SubList 的对象,SubList 类是 ArrayList 的内部类,SubList 的定义如下,并没有实现序列化接口,无法网络传输:
  • private static class SubList<E> extends AbstractList<E> implements RandomAccess {...}
  • 在 foreach 遍历元素时,使用删除方式测试 fail-fast 机制,查看如下代码:
@Test	public void test02() {
List
list = new ArrayList
(); list.add("one"); list.add("two"); list.add("three"); for (String s : list) {
if ("two".equals(s)) {
list.remove(s); } } System.out.println(list); }
  • 编译正确,执行成功!输出 [one,three] ,说好的 ConcurrentModificationException 异常呢?这只是一种巧合,在集合遍历时维护一个初始值为0的游标 cursor,从头到尾的进行扫描,当 cursor 等于 size 时,退出遍历,执行 remove 这个元素后,所有元素往前拷贝,size = size -1 即为2,这时 cursor 也等于2。在执行 hasNext() 时,结果为 false,退出循环体,并没有机会执行到 next() 的第一行代码 checkForComodification(),此方法用来判断 expectedModCount 和 modCount 是否相等,如果不相等,则会抛出 ConcurrentModificationException 异常。
  • 这个案列应该引起对删除元素时的 fail-fast 警觉,我们可以使用 Iterator 机制进行遍历时的删除,如果时多线程并发,还需要在 Iterator 遍历时加锁,如下源码:
Iterator
iterator = list.iterator();while(iterator.hasNext()){
synchronized(对象) {
String item = iterator.next(); if (删除元素的条件) {
iterator.remove(); } }}
  • 或者使用并发容器 CopyOnWriteArrayList 代替 ArrayList。顺便介绍一个 COW 奶牛家族,即 Copy-On-Write。它是并发的一种新思路,实行读写分离,如果要是写操作,则复制一个新集合,在新集合中对元素进行添加或删除,待一切都修改完成后,再将原集合的引用指向新集合,这样做的好处是可与高并发的对 COW 进行读和遍历操作,而不需要加锁,因为当前集合不会添加任何元素,使用 COW 时应该注意两点:
    • 尽量设置合理的容量初始值,它扩容的代价比较大;
    • 使用批量添加或删除方法,如 addAll 或 removeAll 操作,在高并发请求下,可以攒一下要添加或者删除的元素,避免增加一个元素复制整个集合。
  • COW 是 fail-safe 机制的,在并发包中的集合都是由这种机制实现的,fail-safe 是在安全的副本(或者说没有修改操作的正本)上进行遍历,集合修改与副本的遍历是没有任何关系的,但是缺点也很明显,就是读取不到最新的数据。这也是 CAP 理论中 一致性和可用性的矛盾之处。

转载地址:http://nsqwi.baihongyu.com/

你可能感兴趣的文章
JavaScript常用算法(面试)------Sestid
查看>>
Js或者jQuery实现点击图片出现蒙层并将图片放大在蒙层上------Sestid
查看>>
Js,jQuery事件、效果大全------Sestid
查看>>
CSS块元素、内联元素、内联块元素详解------Sestid
查看>>
Js实现跟随鼠标移动的小球------Sestid
查看>>
HTML图像,链接,列表,表格等详细介绍------Sestid
查看>>
Js实现的俄罗斯方块小游戏------Sestid
查看>>
Js实现贪吃蛇小游戏------Sestid
查看>>
jQuery常用方法(持续更新)
查看>>
原生js实现自定义倒计时效果------Sestid
查看>>
原生js实现生成随机验证码=------Sestid
查看>>
js实现购物时选带属性的商品------Sestid
查看>>
点击出现对应界面(第二个界面可以选择显示内容)------Sestid
查看>>
Js实现炫酷仿抖罗盘时钟------Sestid
查看>>
vivo官网鼠标触碰图片拉长------Sestid
查看>>
canvas画布实现的集中效果
查看>>
Js实现点击置顶效果(带动画)
查看>>
Js实现input全选、全不选、反选功能------Sestid
查看>>
纯css实现好看的背景------Sestid
查看>>
为什么我的CSDN上都是开关灯??????Js实现开灯关灯特效
查看>>