fail-safe 和 fail-fast 都是什么鬼?

本是古典 何须时尚 2022-09-10 03:21 262阅读 0赞

点击关注公众号,Java干货**及时送达**728e5e7cfd8766d580b91b5bb867f9b6.png

你真的了解 fail-fastfail-safe 吗?????

简介

java.util 包下的 属于 fail-fast , 快速失败~ ????

java.util.concurrent 包下的 属于 fail-safe安全失败~ ????

简单来说 就是 fail-fast 在迭代时,如果发现 该集合数据 结构被改变 (modCount != expectedModCount),就会 抛出 ConcurrentModificationException

小伙伴们可以参考下 下面的代码简单实验一下~ ????

fail-fast 实验代码

实验对象是 Hashtable,这里采用 jdk1.7 的写法 ~

因为博主还在研究 下文中 ConcurrentHashMap 在7和8中有啥不一样 ????

  1. class E implements Runnable{
  2. Hashtable<String, String> hashtable;
  3. public E(Hashtable<String, String> hashtable) {
  4. this.hashtable = hashtable;
  5. }
  6. private void add(Hashtable<String, String> hashtable){
  7. for (int i = 0; i < 10000000; i++) {
  8. hashtable.put("a",""+i);
  9. }
  10. }
  11. @Override
  12. public void run() {
  13. add(hashtable);
  14. }
  15. }
  16. public class D {
  17. public static void main(String[] args) {
  18. Hashtable<String, String> hashtable = new Hashtable<String, String>();
  19. hashtable.put("1","2");
  20. hashtable.put("2","2");
  21. hashtable.put("3","2");
  22. hashtable.put("4","2");
  23. hashtable.put("15","2");
  24. new Thread(new E(hashtable)).start();
  25. Set<Map.Entry<String, String>> entries = hashtable.entrySet();
  26. Iterator<Map.Entry<String, String>> iterator = entries.iterator();
  27. try {
  28. Thread.sleep(10);
  29. } catch (InterruptedException e) {
  30. e.printStackTrace();
  31. }
  32. while (iterator.hasNext()){
  33. System.out.println(iterator.next());
  34. iterator.remove();
  35. }
  36. }
  37. }

效果如图:

38730c499cc7bb785d3f6da2a006882e.png

触发的原理:

5c6dd6238c2f5b5862636aa3ce74cad3.png

当集合数据结构发生变化时,这两个值是不相等的,所以会抛出该异常~ 。

结论:

虽然 HashTable 是 线程安全的 , 但是它有 fail-fast 机制 ,所以在多线程情况下进行 迭代 也不能去修改它的数据结构!

fail-fast 机制 不允许并发修改!

fail-safe 实验代码

  1. class E implements Runnable{
  2. ConcurrentHashMap<String, String> concurrentHashMap;
  3. public E(ConcurrentHashMap<String, String> concurrentHashMap) {
  4. this.concurrentHashMap = concurrentHashMap;
  5. }
  6. private void add( ConcurrentHashMap<String, String> concurrentHashMap){
  7. for (int i = 0; i < 100000; i++) {
  8. concurrentHashMap.put("a"+i,""+i);
  9. }
  10. }
  11. @Override
  12. public void run() {
  13. add(concurrentHashMap);
  14. }
  15. }
  16. public class D {
  17. public static void main(String[] args) {
  18. ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<String, String>();
  19. concurrentHashMap.put("1","2");
  20. concurrentHashMap.put("2","2");
  21. concurrentHashMap.put("3","2");
  22. concurrentHashMap.put("4","2");
  23. concurrentHashMap.put("15","2");
  24. new Thread(new E(concurrentHashMap)).start();
  25. try {
  26. Thread.sleep(2);
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. Set<Map.Entry<String, String>> entries = concurrentHashMap.entrySet();
  31. for (Map.Entry<String, String> entry : entries) {
  32. System.out.println(entry);
  33. // 这里不用调用 iterator 去 remove
  34. concurrentHashMap.remove(entry.getKey());
  35. }
  36. }
  37. }

效果如图:

1b26d0557ff18ba9f8b0805859009fd9.png

代码运行讲解,线程A 往里加数据,线程B 遍历它的数据,并删除。

可以看到这里并没有报错~,但是它也不能保证遍历到所有的值 (可以理解为无法获取到最新的值)

有没有感受到一丝丝 安全失败的感觉~ ????

哈哈哈 它的特点就是 ???? 允许并发修改,不会抛出 ConcurrentModificationException ,但是无法保证拿到的是最新的值

7cc220e005d5c9ed62466bbccbaa3fc5.png

不知道小伙伴们看完上面的实验代码有没有疑惑

(・∀・(・∀・(・∀・*)

#

为什么可以调用它自身的 remove 呢?

别急~ 我们先来看看使用这个迭代器中发生了什么?

源码走起~

小伙伴们可以看看下面四张图~74e7393259bf3051cc09b0c9157cf097.png

创建迭代器的过程

a29d76e50c07a57c15b6a51e8142789f.png

6349dfb5243ca57cba4a3a19a6c63692.png

aeb1c7a23db7ef8c445607151212be44.png

17bb04332147926bb5f111248470e0d0.png

图一 可以看到会去创造一个 EntryIterator, 而 它又 继承了 HashIterator ,在初始化时,会先调用父类的构造器。

图三 可以发现 HashIterator 在初始化 时,会去调用 advance 方法 (这里就不展开这个 concurrentHashMap结构啦~ ) 这里的重点在最后一张图 , 它调用的是 UNSAFE.getObjectVolatile

它的作用是 强制从主存中获取属性值。

小伙伴们可以自行对比下 HashMap 或者 上面的 HashTable,他们都是直接 拿到代码中定义的这个 Entry[]~。????

a4e678d24a9e18ec41afe37406831e3d.png

不知道小伙伴们 get 得到这个点没有~

哈哈哈 容我唠叨唠叨一下~ ????

4ye 在网上搜这个 fail-fastfail-safe 的区别时,看到下面这张图。

几乎都在说 fail-safe 会复制原来的集合,然后在复制出来的集合上进行操作,然后就说这样是不会抛出 ConcurrentModificationException 异常了。

可是这种说法是 不严谨的~ ???? 它描述的情况应该是针对这个 CopyOnWriteArrayList 或者 CopyOnWriteArraySet 的情况(下面的源码讲到~)

6ed5144a017dffac2a3afbf294d5cc52.png

CopyOnWriteArrayList 源码

#

可以发现这里 snapshot 的指针是始终指向这个原数组的(当你创建迭代器的时候)

7878e2ddfc52f7e109191fcbaa072069.png

当你添加数据时,它会复制原来的数组,并在复制出来的数组上进行修改,

然后再设置进去,可以发现至始至终都没有修改到这个原数组,

所以迭代器中的数据是不受影响的~????

4fc3e3dffdb955469617bcae257e30ba.png

结论

fail-safe 也是得具体情况具体分析的。

  1. 如果是 CopyOnWriteArrayList 或者 CopyOnWriteArraySet ,就属于 复制原来的集合,然后在复制出来的集合上进行操作 的情况 ,所以是不会抛出这个 ConcurrentModificationException 的 。
  2. 如果是这个 concurrentHashMap 的,就比较硬核了~ ???? 它直接操作底层,调用UNSAFE.getObjectVolatile ,直接 强制从主存中获取属性值,也是不会抛出这个 ConcurrentModificationException 的 。
  3. 并发下,无法保证 遍历时拿到的是最新的值~

嘿嘿 现在回答上面那个 为啥可以 remove 的问题啦~

remove 的源码如下

b3fc3bd24aa8c1bf9865874617e5ca22.png

886d9a68da8ccf19f2db403d943530a6.png

重点在红框处, pred 为 null 表示是数组的头部,此时调用 setEntryAt ,这里也是出现了这个 UNSAFE ????

UNSAFE.putOrderedObject 这段代码的意思就是 :

有序的(有延迟的) 强制 更新数据到 主内存。(不能立刻被其他线程发现)

这些 和 Java 的 JMM (Java内存模型)有关!

总结

java.util 包下的属于fail-fast

特点:

不允许并发修改,如果并发修改的话会导致在迭代过程中抛出 ConcurrentModificationException

触发点是 modCount != expectedModCount????

java.util.concurrent 包下的 属于 fail-safe

特点:

允许并发修改,但是 无法保证在迭代过程中获取到最新的值 。 ????

concurrentHashMap 获取和修改数据时 ,是通过 UNSAFE 类 直接从主内存中获取或者更新数据到主内存~ 78530dfde8d0c2572cf6afe23df7a339.png

CopyOnWriteArrayList 或者 CopyOnWriteArraySet ,就直接 复制原来的集合,然后在复制出来的集合上进行操作c9a9cd7f3840ead733e844aaa85f5a0d.png

21eaeb807f69e386313b0593585799c4.gif

d3bc0b45b9b09a173999028aa4527824.png

53231edab30a97b3e91f8010c932f5fd.png

1ce22ca3d3034e93ca2010b1b0d7ae2c.png

5fccb3379cba17be5fea90b966167f10.png

e2feb820d9427f029fc5d4457b68caf3.png

82e3ea572922a1679f4413a017156ed2.png

关注Java技术栈看更多干货

e4a8b5044b41ab19018798f90ce61dfc.png

371bb608a18c66c0ed34427f323f131d.gif

获取 Spring Boot 实战笔记!

发表评论

表情:
评论列表 (有 0 条评论,262人围观)

还没有评论,来说两句吧...

相关阅读