写点什么

你真的了解 fail-fast 和 fail-safe 吗

用户头像
4ye
关注
发布于: 2 小时前

小伙伴们早上好呀~😝



本想着继续讲讲这个 ConcurrentHashMap 的,突然挖掘到源码中的这些特点,应该对你面试时很有用!欢迎指正!😋




简介


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


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


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


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

fail-fast 实验代码


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


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


class E implements Runnable{
Hashtable<String, String> hashtable;
public E(Hashtable<String, String> hashtable) { this.hashtable = hashtable; }
private void add(Hashtable<String, String> hashtable){ for (int i = 0; i < 10000000; i++) { hashtable.put("a",""+i); } }
@Override public void run() { add(hashtable); }}
public class D {

public static void main(String[] args) { Hashtable<String, String> hashtable = new Hashtable<String, String>(); hashtable.put("1","2"); hashtable.put("2","2"); hashtable.put("3","2"); hashtable.put("4","2"); hashtable.put("15","2");

new Thread(new E(hashtable)).start();
Set<Map.Entry<String, String>> entries = hashtable.entrySet(); Iterator<Map.Entry<String, String>> iterator = entries.iterator(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } while (iterator.hasNext()){ System.out.println(iterator.next()); iterator.remove(); } }}
复制代码

效果如图

触发的原理:


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

结论:

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

fail-safe 实验代码

class E implements Runnable{
ConcurrentHashMap<String, String> concurrentHashMap;
public E(ConcurrentHashMap<String, String> concurrentHashMap) { this.concurrentHashMap = concurrentHashMap; }
private void add( ConcurrentHashMap<String, String> concurrentHashMap){ for (int i = 0; i < 100000; i++) { concurrentHashMap.put("a"+i,""+i); } }
@Override public void run() { add(concurrentHashMap); }}public class D {


public static void main(String[] args) {
ConcurrentHashMap<String, String> concurrentHashMap = new ConcurrentHashMap<String, String>();
concurrentHashMap.put("1","2"); concurrentHashMap.put("2","2"); concurrentHashMap.put("3","2"); concurrentHashMap.put("4","2"); concurrentHashMap.put("15","2");
new Thread(new E(concurrentHashMap)).start(); try { Thread.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }
Set<Map.Entry<String, String>> entries = concurrentHashMap.entrySet();

for (Map.Entry<String, String> entry : entries) { System.out.println(entry);// 这里不用调用 iterator 去 remove concurrentHashMap.remove(entry.getKey()); }
}}
复制代码

效果如图:


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


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


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


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



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


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

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

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


源码走起~


小伙伴们可以看看下面四张图~


创建迭代器的过程






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


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


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


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



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


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


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


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


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


CopyOnWriteArrayList 源码

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



当你添加数据时,它会复制原来的数组,并在复制出来的数组上进行修改,然后再设置进去,可以发现至始至终都没有修改到这个原数组,所以迭代器中的数据是不受影响的~😝


结论

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


  1. 如果是 CopyOnWriteArrayList 或者 CopyOnWriteArraySet ,就属于 复制原来的集合,然后在复制出来的集合上进行操作 的情况 ,所以是不会抛出这个 ConcurrentModificationException 的 。

  2. 如果是这个 concurrentHashMap 的,就比较硬核了~ 😄 它直接操作底层,调用UNSAFE.getObjectVolatile ,直接 强制从主存中获取属性值,也是不会抛出这个 ConcurrentModificationException 的 。

  3. 并发下,无法保证 遍历时拿到的是最新的值~




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

remove 的源码如下




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


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


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


这些和 Java 的 JMM (Java 内存模型)有关! 埋个坑🕳,后面写并发的时候更~ 😝

总结

java.util 包下的属于fail-fast ,它的特点是 不允许并发修改,如果并发修改的话会导致在迭代过程中抛出 ConcurrentModificationException ,触发点是 modCount != expectedModCount😝


java.util.concurrent 包下的 属于 fail-safe ,它的特点是 允许并发修改,但是 无法保证在迭代过程中获取到最新的值 。 😋而且 concurrentHashMap 修改数据时 是直接通过 UNSAFE 类 直接从主内存中获取数据,或者直接更新数据到主内存~CopyOnWriteArrayList 或者 CopyOnWriteArraySet ,就直接 复制原来的集合,然后在复制出来的集合上进行操作 ,是不同的操作~

最后

欢迎小伙伴们来一起探讨问题~

如果你觉得本篇文章还不错的话,那拜托再点点赞支持一下呀😝

让我们开始这一场意外的相遇吧!~

欢迎留言!谢谢支持!ヾ(≧▽≦*)o 冲冲冲!!

我是 4ye 咱们下期应该……很快再见!! 😆

如果文章对您有所帮助,欢迎关注公众号 J a v a 4 y e 😆

发布于: 2 小时前阅读数: 3
用户头像

4ye

关注

公众号:J a v a 4 y e 2021.07.19 加入

还未添加个人简介

评论

发布
暂无评论
你真的了解 fail-fast 和 fail-safe 吗