之前在程序中遇到快速失败的问题,在网上找解释时发现网上的问题总结都比较片面,故打算自己总结一个,也可以供以后参考。

                                                    --宇的季节

首先什么是快速失败?

快速失败是为了提示程序员在多线程的情况下不要用线程不安全的集合(bug)的一种机制。

当然在单线程的情况下有时也会出现ConcurrentModificationException异常,下面我们就根据ArrayList来探索快速失败的内部实现方法。

示例一:

public static void main(String[] args) {

List<String> list = new ArrayList<String>();

//为集合添加100个数

for (int i = 0; i < 100; i++)

list.add(i+"");

//获取集合的迭代器

Iterator<String> iter = list.iterator();

//我们在集合中删除一个数据,注意我们是直接用集合操作而不是迭代器

list.remove("2");

//再使用迭代器获取数据

iter.next();

}

执行结果:

果不其然,报了ConcurrentModificationException的错误。

为什么会报错呢?下面我们就来看看ArrayList的源码(为了方便阅读,把没用的都省略了)

public class ArrayList<E> ,,,,,

{

....

//modCount:集合的修改次数

protected transient int modCount = 0;

//当出现增删操作时,便会modCount++;

public E remove(int index) {

        ....

        modCount++;

        ......

    }

....

//通过此方法获取迭代器

public Iterator<E> iterator() {

return new Itr();

}

....

//迭代器是ArrayList的一个内部类

private class Itr implements Iterator<E> {

        ...

//每次获取一个迭代器,就新建一个对象,将expectedModCount赋值为modCount

        int expectedModCount = modCount;

       .....

        @SuppressWarnings("unchecked")

        public E next() {

//检查expectedModCount和modCount是否相等

            checkForComodification();

            .....

        }

//这个就是检查的方法

final void checkForComodification() {

            if (modCount != expectedModCount)

                throw new ConcurrentModificationException();

        }  

.....

  }

我们已经了解了ArrayList的大概结构,那么示例一的报错也就不难解释了:

假设创建一个迭代器时modCount=10,那么expectedModCount=10(注意,modCount是ArrayList的参数,而expectedModCount是迭代器的参数,从创建迭代器之后,他们就没联系了)

当执行list.remove("2");操作时,modCount++=11;而此时expectedModCount=10;当我们再执行iter.next()时,很明显两个数不一致,抛出异常。

结论:当我们在获取一个快速失败集合(只要不是concurrent包下的集合都是快速失败的)的迭代器时候,之后在操作迭代器的中间不能对集合本身再进行增删操作,否则会抛异常。

但是,快速失败机制的意义并不在此,上面已经说到了,快速失败是为了警告程序员不能在多线程情况下使用线程不安全的集合。

示例2:

public class test {
private static List<String> list = new ArrayList<String>();
public static void main(String[] args) {
for (int i = 0; i < 100; i++)
list.add(i+"");
System.out.println("线程开始了");
new threadOne().start();
new threadTwo().start();
}
//线程1每隔10ms遍历一次
private static class threadOne extends Thread{
public void run(){
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
System.out.println("线程1遍历到了:"+iter.next());
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
//线程1每隔10ms删除一个
private static class threadTwo extends Thread{
public void run(){
Iterator<String> iter = list.iterator();
while(iter.hasNext()){
System.out.println("线程2删除了:"+iter.next());
iter.remove();
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}

执行结果:

在示例2中,我们并没有在使用迭代器中间直接对集合进行增删。但是依然报错。

原因就是因为线程的异步性!

这次只看迭代器的remove()操作即可:

可以看到,两个值的改变被分成了两条程序语句,那么就会出现这种情况:

线程1删除时执行到语句1,modCount+1。

由于语句2还没执行,expectedModCount不会改变

这时处理器跳转到线程2执行。

线程2执行next()时,对比modCount和expectedModCount。

发现两值不一致,抛出ConcurrentModificationException

终于把快速失败搞明白了!那么什么是安全失败呢?

这个就简单了,在使用安全失败的迭代器时,会copy一个集合,之后的操作都是对这个copy的“副本”进行操作。

那么就算是多线程,操作的也是创建迭代器时copy的单独的副本。

所以不会造成冲突,ConcurrentModificationException也就不复存在啦~

(简单易懂)Java的快速失败(fail-fast)与安全失败,源码分析+详细讲解的更多相关文章

  1. Java显式锁学习总结之六:Condition源码分析

    概述 先来回顾一下java中的等待/通知机制 我们有时会遇到这样的场景:线程A执行到某个点的时候,因为某个条件condition不满足,需要线程A暂停:等到线程B修改了条件condition,使con ...

  2. Java显式锁学习总结之五:ReentrantReadWriteLock源码分析

    概述 我们在介绍AbstractQueuedSynchronizer的时候介绍过,AQS支持独占式同步状态获取/释放.共享式同步状态获取/释放两种模式,对应的典型应用分别是ReentrantLock和 ...

  3. Java并发包中Semaphore的工作原理、源码分析及使用示例

    1. 信号量Semaphore的介绍 我们以一个停车场运作为例来说明信号量的作用.假设停车场只有三个车位,一开始三个车位都是空的.这时如果同时来了三辆车,看门人允许其中它们进入进入,然后放下车拦.以后 ...

  4. Java显式锁学习总结之四:ReentrantLock源码分析

    概述 ReentrantLock,即重入锁,是一个和synchronized关键字等价的,支持线程重入的互斥锁.只是在synchronized已有功能基础上添加了一些扩展功能. 除了支持可中断获取锁. ...

  5. 转!!Java学习之自动装箱和自动拆箱源码分析

    自动装箱(boxing)和自动拆箱(unboxing)   首先了解下Java的四类八种基本数据类型   基本类型 占用空间(Byte) 表示范围 包装器类型 boolean 1/8 true|fal ...

  6. Java学习之自动装箱和自动拆箱源码分析

    自动装箱(boxing)和自动拆箱(unboxing) 首先了解下Java的四类八种基本数据类型   基本类型 占用空间(Byte) 表示范围 包装器类型 boolean 1/8 true|false ...

  7. 【Java】CAS的乐观锁实现之AtomicInteger源码分析

    1. 悲观锁与乐观锁 我们都知道,cpu是时分复用的,也就是把cpu的时间片,分配给不同的thread/process轮流执行,时间片与时间片之间,需要进行cpu切换,也就是会发生进程的切换.切换涉及 ...

  8. Java - "JUC线程池" 线程状态与拒绝策略源码分析

    Java多线程系列--“JUC线程池”04之 线程池原理(三) 本章介绍线程池的生命周期.在"Java多线程系列--“基础篇”01之 基本概念"中,我们介绍过,线程有5种状态:新建 ...

  9. Java 自动装箱、拆箱机制及部分源码分析

    Integer i = 10; //装箱,反编译后发现调用Integer.valueOf(int i) int t = i; //拆箱,反编译后发现调用i.intValue() public clas ...

随机推荐

  1. USB的前世今生

    在人类的历史长河中,很少有一种技术或者传输标准能像USB那样跟我们的生活息息相关,甚至到了没有不行的地步.USB对于今天的人们来说,就好像是空气,是水,是我们每天必需但是又熟视无睹的东西,没有多少人知 ...

  2. 使用iframe父页面调用子页面和子页面调用父页面的元素与方法

    在实际的项目开发中,iframe框架经常使用,主要用于引入其他的页面.下面主要介绍一下使用iframe引入其他页面后,父页面如何调用子页面的方法和元素以及子页面如何调用父页面的方法和元素. 1.父页面 ...

  3. 简单谈谈js中的MVC

    MVC是什么? MVC是一种架构模式,它将应用抽象为3个部分:模型(数据).视图.控制器(分发器). 本文将用一个经典的例子todoList来展开(代码在最后). 一个事件发生的过程(通信单向流动): ...

  4. SpringMVC源码情操陶冶-AbstractHandlerExceptionResolver

    springmvc支持服务端在处理业务逻辑过程中出现异常的时候可以配置相应的ModelAndView对象返回给客户端,本文介绍springmvc默认的几种HandlerExceptionResolve ...

  5. 【nodejs】nodejs 的linux安装(转)

    (一) 编译好的文件 简单说就是解压后,在bin文件夹中已经存在node以及npm,如果你进入到对应文件的中执行命令行一点问题都没有,不过不是全局的,所以将这个设置为全局就好了. ./node -v ...

  6. Redis-位图法实现简单统计

    比如一个网站有1亿个用户, 现在要统计一周内连续登录的用户 方法: 可以用一个字节8个位表示7个人, 首位不算固定为0, 若某人周一登录则置为1, 没登录则为0 127.0.0.1:6379> ...

  7. Shopex如何清理缓存

    一.进入后台,点击 右上角 的"关于" 二.点击:缓存系统: 三.点击"清空缓存" 四.清除成功!

  8. zabbix分布式监控的部署

    zabbix是一个分布式监视,管理系统,基于server-clinet架构,可用于监视各种网络服务,服务器和网络机器等状态. server端基于C语言,web管理端Frontend则是基于PHPA制作 ...

  9. swift3.0 顶部状态栏隐藏

    横屏进入手机状态栏是隐藏的,但是调用了相册选去图片后顶部状态栏又显示了出来,网上查阅按照以下方式: override var prefersStatusBarHidden: Bool{ return ...

  10. linux下U盘挂载

    linux下挂载U盘 一.Linux挂载U盘:1.插入u盘到计算机,如果目前只插入了一个u盘而且你的硬盘不是scsi的硬盘接口的话,那它的硬件名称为:sda1,可以用"fdisk -l&qu ...