Skip to content

Commit f41c8b0

Browse files
authored
Merge pull request #2537 from shark-ctrl/shark-chili
[docs update] 说说fail-fast和fail-safe是什么
2 parents 4722969 + 6f224ae commit f41c8b0

File tree

1 file changed

+105
-0
lines changed

1 file changed

+105
-0
lines changed

docs/java/collection/java-collection-questions-01.md

+105
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,111 @@ public interface RandomAccess {
255255

256256
详见笔主的这篇文章: [ArrayList 扩容机制分析](https://javaguide.cn/java/collection/arraylist-source-code.html#_3-1-%E5%85%88%E4%BB%8E-arraylist-%E7%9A%84%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E8%AF%B4%E8%B5%B7)
257257

258+
### 说说集合中的fail-fast和fail-safe是什么
259+
260+
关于`fail-fast`引用`medium`中一篇文章关于`fail-fast``fail-safe`的说法:
261+
262+
> Fail-fast systems are designed to immediately stop functioning upon encountering an unexpected condition. This immediate failure helps to catch errors early, making debugging more straightforward.
263+
264+
265+
快速失败的思想即针对可能发生的异常进行提前表明故障并停止运行,通过尽早的发现和停止错误,降低故障系统级联的风险。
266+
267+
`java.util`包下的大部分集合是不支持线程安全的,为了能够提前发现并发操作导致线程安全风险,提出通过维护一个`modCount`记录修改的次数,迭代期间通过比对预期修改次数`expectedModCount``modCount`是否一致来判断是否存在并发操作,从而实现快速失败,由此保证在避免在异常时执行非必要的复杂代码。
268+
269+
对应的我们给出下面这样一段在示例,我们首先插入`100`个操作元素,一个线程迭代元素,一个线程删除元素,最终输出结果如愿抛出`ConcurrentModificationException`
270+
271+
```java
272+
ArrayList<Integer> list = new ArrayList<>();
273+
CountDownLatch countDownLatch = new CountDownLatch(2);
274+
//添加几个元素
275+
for (int i = 0; i < 100; i++) {
276+
list.add(i);
277+
}
278+
279+
Thread t1 = new Thread(() -> {
280+
//迭代元素
281+
for (Integer i : list) {
282+
i++;
283+
}
284+
countDownLatch.countDown();
285+
});
286+
287+
288+
Thread t2 = new Thread(() -> {
289+
System.out.println("删除元素1");
290+
list.remove(1);
291+
countDownLatch.countDown();
292+
});
293+
294+
t1.start();
295+
t2.start();
296+
countDownLatch.await();
297+
```
298+
299+
300+
301+
302+
303+
304+
我们在初始化时插入了`100`个元素,此时对应的修改`modCount`次数为`100`,随后线程2在线程1迭代期间进行元素删除操作,此时对应的`modCount`就变为`101`
305+
线程1在随后`foreach`第2轮循环发现`modCount``101`,与预期的`expectedModCount(值为100因为初始化插入了元素100个)`不等,判定为并发操作异常,于是便快速失败,抛出`ConcurrentModificationException`
306+
307+
![](https://qiniuyun.sharkchili.com/202411172341886.png)
308+
309+
310+
对此我们也给出`for`循环底层迭代器获取下一个元素时的`next`方法,可以看到其内部的`checkForComodification`具有针对修改次数比对的逻辑:
311+
312+
```java
313+
public E next() {
314+
//检查是否存在并发修改
315+
checkForComodification();
316+
//......
317+
//返回下一个元素
318+
return (E) elementData[lastRet = i];
319+
}
320+
321+
final void checkForComodification() {
322+
//当前循环遍历次数和预期修改次数不一致时,就会抛出ConcurrentModificationException
323+
if (modCount != expectedModCount)
324+
throw new ConcurrentModificationException();
325+
}
326+
327+
```
328+
329+
330+
`fail-safe`也就是安全失败的含义,它旨在即使面对意外情况也能恢复并继续运行,这使得它特别适用于不确定或者不稳定的环境:
331+
332+
333+
> Fail-safe systems take a different approach, aiming to recover and continue even in the face of unexpected conditions. This makes them particularly suited for uncertain or volatile environments.
334+
335+
该思想常运用于并发容器,最经典的实现就是`CopyOnWriteArrayList`的实现,通过写时复制的思想保证在进行修改操作时复制出一份快照,基于这份快照完成添加或者删除操作后,将`CopyOnWriteArrayList`底层的数组引用指向这个新的数组空间,由此避免迭代时被并发修改所干扰所导致并发操作安全问题,当然这种做法也存缺点即进行遍历操作时无法获得实时结果:
336+
![](https://qiniuyun.sharkchili.com/202411172352477.png)
337+
338+
339+
340+
对应我们也给出`CopyOnWriteArrayList`实现`fail-safe`的核心代码,可以看到它的实现就是通过`getArray`获取数组引用然后通过`Arrays.copyOf`得到一个数组的快照,基于这个快照完成添加操作后,修改底层`array`变量指向的引用地址由此完成写时复制:
341+
342+
```java
343+
public boolean add(E e) {
344+
final ReentrantLock lock = this.lock;
345+
lock.lock();
346+
try {
347+
//获取原有数组
348+
Object[] elements = getArray();
349+
int len = elements.length;
350+
//基于原有数组复制出一份内存快照
351+
Object[] newElements = Arrays.copyOf(elements, len + 1);
352+
//进行添加操作
353+
newElements[len] = e;
354+
//array指向新的数组
355+
setArray(newElements);
356+
return true;
357+
} finally {
358+
lock.unlock();
359+
}
360+
}
361+
```
362+
258363
## Set
259364

260365
### Comparable 和 Comparator 的区别

0 commit comments

Comments
 (0)