蚂蚁花呗:阿里巴巴面试:Java 集合知识点(附图文解析)( 八 )


Hashtable 和 ConcurrentHashMap 的区别ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同 。

  • 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现 , JDK1.8 采用的数据结构和HashMap1.8的结构类似 , 数组+链表/红黑二叉树 。 Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式 , 数组是 HashMap 的主体 , 链表则是主要为了解决哈希冲突而存在的;
  • 实现线程安全的方式(重要):
    • 在JDK1.7的时候 , ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment) , 每一把锁只锁容器其中一部分数据 , 多线程访问容器里不同数据段的数据 , 就不会存在锁竞争 , 提高并发访问率 。 (默认分配16个Segment , 比Hashtable效率提高16倍 。 ) 到了 JDK1.8 的时候已经摒弃了Segment的概念 , 而是直接用 Node 数组+链表/红黑树的数据结构来实现 , 并发控制使用 synchronized 和 CAS 来操作 。 (JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap , 虽然在 JDK1.8 中还能看到 Segment 的数据结构 , 但是已经简化了属性 , 只是为了兼容旧版本;
    • Hashtable(同一把锁) :使用 synchronized 来保证线程安全 , 效率非常低下 。 当一个线程访问同步方法时 , 其他线程也访问同步方法 , 可能会进入阻塞或轮询状态 , 如使用 put 添加元素 , 另一个线程不能使用 put 添加元素 , 也不能使用 get , 竞争越激烈效率越低 。
list 可以删除吗 , 遍历的时候可以删除吗 , 为什么
Java快速失败(fail-fast)和安全失败(fail-safe)区别快速失败(fail—fast)在用迭代器遍历一个集合对象时 , 如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改) , 则会抛出ConcurrentModificationException 。
原理:迭代器在遍历时直接访问集合中的内容 , 并且在遍历过程中使用一个 modCount 变量 。 集合在被遍历期间如果内容发生变化 , 就会改变 modCount 的值 。 每当迭代器使用 hashNext()/next() 遍历下一个元素之前 , 都会检测 modCount 变量是否为 expectedmodCount 值 , 是的话就返回遍历;否则抛出异常 , 终止遍历 。
注意:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件 。 如果集合发生变化时修改modCount 值刚好又设置为了 expectedmodCount 值 , 则异常不会抛出 。 因此 , 不能依赖于这个异常是否抛出而进行并发操作的编程 , 这个异常只建议用于检测并发修改的bug 。
场景:java.util包下的集合类都是快速失败的 , 不能在多线程下发生并发修改(迭代过程中被修改) 。
安全失败(fail—safe)采用安全失败机制的集合容器 , 在遍历时不是直接在集合内容上访问的 , 而是先复制原有集合内容 , 在拷贝的集合上进行遍历 。
原理:由于迭代时是对原集合的拷贝进行遍历 , 所以在遍历过程中对原集合所作的修改并不能被迭代器检测到 , 所以不会触发 Concurrent Modification Exception 。
缺点:基于拷贝内容的优点是避免了Concurrent Modification Exception , 但同样地 , 迭代器并不能访问到修改后的内容 , 即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝 , 在遍历期间原集合发生的修改迭代器是不知道的 。
场景:java.util.concurrent包下的容器都是安全失败 , 可以在多线程下并发使用 , 并发修改 。
快速失败和安全失败是对迭代器而言的 。 快速失败:当在迭代一个集合的时候 , 如果有另外一个线程在修改这个集合 , 就会抛出ConcurrentModification异常 , java.util下都是快速失败 。 安全失败:在迭代时候会在集合二层做一个拷贝 , 所以在修改集合上层元素不会影响下层 。 在java.util.concurrent下都是安全失败


推荐阅读