【JavaSE】fail-fast与fail-safe源码分析
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
文章目录
1. fail-fast与fail-safe概述
快速失败(fail-fast)
快速失败是Java集合的一种错误检测机制。
- 出现场景线程A在使用迭代器遍历一个集合对象的时候线程B对集合对象的内存进行了操作增加、删除、修改这时候会抛出
Concurrent Modification Exception
- 原理就拿
ArrayList
来说ArrayList
继承了一个抽象类AbstractList
这个抽象了中有一个成员变量protected transient int modCount = 0;
这个变量是记录集合被修改的次数的。集合使用迭代器进行遍历的时候每当迭代器使用hashNext()/next()
之前会先检测modCount
变量是否为expectedmodCount
值是的话就遍历否则抛出异常。 - 注意这里抛出异常的判断条件为
modCount
变量是否为expectedmodCount
值如果集合发生变化时modCount
值刚好又设置为了expectedmodCount
值。因此不能依赖于这个异常是否抛出而进行并发操作的编程这个异常只建议用于检测并发修改的bug。 - 场景
java.util
包下的集合类都是快速失败的不能在多线程下发生并发修改迭代过程中被修改比如ArrayList
类。
安全失败fail—safe
采用安全失败的集合容器在遍历时不是直接在集合内容上访问的而是先复制原有集合内容在拷贝的集合上进行遍历。
- 原理由于迭代时是对原集合的拷贝进行遍历所以在遍历过程中对原集合所作的修改并不能被迭代器检测到所以不会触发
Concurrent Modification Exception
。 - 缺点基于拷贝内容的优点是避免了
Concurrent Modification Exception
但同样地迭代器并不能访问到修改后的内容也就是说迭代器遍历的是开始遍历那一刻拿到的集合拷贝在遍历期间原集合发生的修改迭代器是不知道的。 - 场景
java.util.concurrent
包下的容器都是安全失败可以在多线程下并发使用并发修改比如CopyOnWriteArrayList
类。
2. fail-fast源码分析
测试代码使用IDEA进行debug
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
for (Integer integer : list) {
System.out.println(integer);
}
}
这里的增强for循环实际上就是使用迭代器进行迭代的。迭代器记录了当前集合的修改次数。
int expectedModCount = modCount;
在for循环之前list
集合中添加了四个元素所以在list
中的modCount
的值为4。
使用IDEA的debug工具在程序执行过程中往list
集合中添加一个元素——5模拟并发。
成功往集合中添加第五个元素。
这时候当下一次增强for
循环执行的时候也就是使用迭代器的hasNext
方法
public boolean hasNext() {
return cursor != size;
}
然后执行迭代器的next
方法
public E next() {
//检验集合是否被修改
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
首先会调用checkForComodification()
检验集合是否被修改如果修改那么集合中的mod
不会与expectedModCount
相等。
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
mod
与expectedModCount
不相等则会抛出ConcurrentModificationException
异常程序中断。
3. fail-safe源码分析
使用CopyOnWriteArrayList
进行测试。
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
for (Integer integer : list) {
System.out.println(integer);
}
}
先来简单阅读一下CopyOnWriteArrayList
的源码
//底层存放数据的数组
private transient volatile Object[] array;
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//扩容
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
add
方法它底层实现是加了ReentrantLock
锁。
首先获取底层数组的长度。然后进行扩容。接着将需要添加进集合的元素放置在扩容后的数组的末端。
再将新数组newElements
赋值给底层数组array
。
接下来在增强for
循环打断点。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qrm6VV5A-1675313411939)(https://xingqiu-tuchuang-1256524210.cos.ap-shanghai.myqcloud.com/9944/image-20230202123227242.png)]
在准备开始增强for
循环的时候会调用iterator
创建一个COWIterator
迭代器。
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
并且将底层存放数据的数组array
作为参数。
//由后续调用next返回的元素的索引。
private int cursor;
//快照存储的是迭代器进行迭代的时候集合的数据
private final Object[] snapshot;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
然后调用hasNext
方法
public boolean hasNext() {
return cursor < snapshot.length;
}
接着调用next
方法将快照的数组依次输出。
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
假如这时候线程B向集合中添加一个元素。但是迭代器中的snapshot
并没有变化。
因为能snapshot
在COWIterator
初始化的时候已经固定了。即使接下来array
发生改变。snapshot
也依然不变。
所以迭代器输出的数据是迭代器创建那一刻之前的数据迭代过程中无论集合原数据变成什么都是输出旧数据。
4. 总结
ArrayList
是fail-fast
的经典代表遍历的同时不能修改否则会抛出异常。CopyOnWriteArrayList
是fail-safe
的经典代表遍历的同时可以修改原理是读写分离。