美文网首页
java 集合类,ConcurrentHashMap详解

java 集合类,ConcurrentHashMap详解

作者: John13 | 来源:发表于2018-09-07 09:43 被阅读0次

HashMap 1.8 put get过程:

HashMap? ConcurrentHashMap? 相信看完这篇没人能难住你!

  • put:
    1、判断当前桶是否为空,空的就需要初始化(resize 中会判断是否进行初始化)。
    2、根据当前 key 的 hashcode 定位到具体的桶中并判断是否为空,为空表明没有 Hash 冲突就直接在当前位置创建一个新桶即可。
    3、如果当前桶有值( Hash 冲突),那么就要比较当前桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e,在第 8 步的时候会统一进行赋值及返回。
    4、如果当前桶为红黑树,那就要按照红黑树的方式写入数据。
    5、如果是个链表,就需要将当前的 key、value 封装成一个新节点写入到当前桶的后面(形成链表)。
    6、接着判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树。
    7、如果在遍历过程中找到 key 相同时直接退出遍历。
    8、如果 e != null 就相当于存在相同的 key,那就需要将值覆盖。
    9、最后判断是否需要进行扩容。

  • get:
    1、首先将 key hash 之后取得所定位的桶。
    2、如果桶为空则直接返回 null 。
    3、否则判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
    4、如果第一个不匹配,则判断它的下一个是红黑树还是链表。
    5、红黑树就按照树的查找方式返回值。
    6、不然就按照链表的方式遍历匹配返回值。

HashMap为啥线程不安全

为什么HashMap线程不安全 -- 很棒

1、 put的时候,hash冲突会导致数据被覆盖而丢失(一直存在
2、 多线程Resize时可能会形成环形链表,当执行 get 时,当key 正好被分到那个 table[i] 上时,遍历链表就会产生循环。(jdk1.8用红黑树修复

  • JDK8的修复:
    JDK8 将 HashMap 的结构作了修改,将原来的链表部分改为数据少时仍然链表,当超过一定数量后变换为红黑树。
    HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。
    HashMap 多线程下死循环分析及JDK8修复

  • HashMap在1.7和1.8之间的变化:
    1、1.7采用数组+单链表,1.8在单链表超过一定长度后改成红黑树存储
    2、1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行&操作来计算新的索引位置。
    3、1.7插入元素到单链表中采用头插入法,1.8采用的是尾插入法。

ConcurrentHashMap原理

  • JDK1.7分析:

ConcurrentHashMap采用分段锁(Segment+ReentrantLock)的机制,实现并发的更新操作,底层采用数组+链表的存储结构。
其包含两个核心静态内部类 Segment和HashEntry。

  1. Segment继承ReentrantLock用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶。
  2. HashEntry 用来封装映射表的键 / 值对;
  3. 每个桶是由若干个 HashEntry 对象链接起来的链表。

一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组,下面我们通过一个图来演示一下 ConcurrentHashMap 的结构:

  • JDK1.8分析:

1.8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。

image

Segment+ReentrantLock VS CAS+Synchronized
ConcurrentHashMap 1.8为什么要使用CAS+Synchronized取代Segment+ReentrantLock

1.7:

  • 在初始化ConcurrentHashMap的时候,会初始化一个Segment数组,容量为16,而每个Segment呢,都继承了ReentrantLock类,也就是说每个Segment类本身就是一个锁,之后Segment内部又有一个table数组,而每个table数组里的索引数据呢,又对应着一个Node链表。
  • 使用put方法的时候,是对我们的key进行hash拿到一个整型,然后将整型对16取模,拿到对应的Segment,之后调用Segment的put方法,然后上锁,请注意,这里lock()的时候其实是this.lock(),也就是说,每个Segment的锁是分开的。

1.8:
Synchronized上锁的对象,请记住,Synchronized是靠对象的对象头和此对象对应的monitor来保证上锁的,也就是对象头里的重量级锁标志指向了monitor,而monitor呢,内部则保存了一个当前线程,也就是抢到了锁的线程.

那么这里的这个f是什么呢?f一定是链表的头结点,即该元素在Node数组中。所以这里锁住的是hash冲突那条链表。

它是Node链表里的每一个Node,也就是说,Synchronized是将每一个Node对象作为了一个锁,这样做的好处是什么呢?将锁细化了,也就是说,除非两个线程同时操作一个Node,注意,是一个Node而不是一个Node链表哦,那么才会争抢同一把锁。

如果使用ReentrantLock其实也可以将锁细化成这样的,只要让Node类继承ReentrantLock就行了,这样的话调用f.lock()就能做到和Synchronized(f)同样的效果,但为什么不这样做呢?

请大家试想一下,锁已经被细化到这种程度了,那么出现并发争抢的可能性还高吗?还有就是,哪怕出现争抢了,只要线程可以在30到50次自旋里拿到锁,那么Synchronized就不会升级为重量级锁,而等待的线程也就不用被挂起,我们也就少了挂起和唤醒这个上下文切换的过程开销.

但如果是ReentrantLock呢?它则只有在线程没有抢到锁,然后新建Node节点后再尝试一次而已,不会自旋,而是直接被挂起,这样一来,我们就很容易会多出线程上下文开销的代价.当然,你也可以使用tryLock(),但是这样又出现了一个问题,你怎么知道tryLock的时间呢?在时间范围里还好,假如超过了呢?

所以,在锁被细化到如此程度上,使用Synchronized是最好的选择了.这里再补充一句,Synchronized和ReentrantLock他们的开销差距是在释放锁时唤醒线程的数量,Synchronized是唤醒锁池里所有的线程+刚好来访问的线程,而ReentrantLock则是当前线程后进来的第一个线程+刚好来访问的线程.

如果是线程并发量不大的情况下,那么Synchronized因为自旋锁,偏向锁,轻量级锁的原因,不用将等待线程挂起,偏向锁甚至不用自旋,所以在这种情况下要比ReentrantLock高效

集合类

JAVA集合类
Java提高篇(三四)-----fail-fast机制

函数式编程,stream用法,Lambda表达式

Java 8 函数式接口
JDK 8 函数式编程入门
Java 8 中的 Streams API 详解

  • “快速失败”也就是fail-fast
  • 它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
  • 这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对 HashMap 内容(当然不仅仅是 HashMap 才会有,其他例如 ArrayList 也会)的修改都将增加这个值(大家可以再回头看一下其源码,在很多操作中都有 modCount++ 这句),那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。
  • 解决方案
    在上文中也提到,fail-fast 机制,是一种错误检测机制。它只能被用来检测错误,因为 JDK 并不保证 fail-fast 机制一定会发生。若在多线程环境下使用 fail-fast 机制的集合,建议使用“java.util.concurrent 包下的类”去取代“java.util 包下的类”。
  • java集合框架图

Map:

  • LinkedHashMap 几乎和 HashMap 一样:从技术上来说,不同的是它定义了一个 Entry<K,V> header,这个 header 不是放在 Table 里,它是额外独立出来的。LinkedHashMap 通过继承 hashMap 中的 Entry<K,V>,并添加两个属性 Entry<K,V> before,after,和 header 结合起来组成一个双向链表,来实现按插入顺序或访问顺序排序。

  • Hashtable 与 HashMap 的简单比较

  • 1、HashTable 基于 Dictionary 类,而 HashMap 是基于 AbstractMap。Dictionary 是任何可将键映射到相应值的类的抽象父类,而 AbstractMap 是基于 Map 接口的实现,它以最大限度地减少实现此接口所需的工作。
  • 2、HashMap 的 key 和 value 都允许为 null,而 Hashtable 的 key 和 value 都不允许为 null。HashMap 遇到 key 为 null 的时候,调用 putForNullKey 方法进行处理,而对 value 没有处理;Hashtable遇到 null,直接返回 NullPointerException。
  • 3、Hashtable 方法是同步,而HashMap则不是。我们可以看一下源码,Hashtable 中的几乎所有的 public 的方法都是 synchronized 的,而有些方法也是在内部通过 synchronized 代码块来实现。所以有人一般都建议如果是涉及到多线程同步时采用 HashTable,没有涉及就采用 HashMap,但是在 Collections 类中存在一个静态方法:synchronizedMap(),该方法创建了一个线程安全的 Map 对象,并把它作为一个封装的对象来返回

Java HashMap源码分析

深入浅出ConcurrentHashMap1.8

ConcurrentHashMap 的实现原理

主题:ConcurrentHashMap之实现细节

HashMap为什么线程不安全(hash碰撞与扩容导致)

图解集合5:不正确地使用HashMap引发死循环及元素丢失

浅析HashMap与ConcurrentHashMap的线程安全性

java集合类TreeMap和TreeSet

Java集合类及并发集合类

  • map结构图

  • map比较

  • map比较1

List:

Arrays.asList使用指南
使用java.util.List.subList时最好小心点

  • List架构

  • List比较

  • List比较1

Set:

  • HashSet,它是基于 HashMap 实现的,底层采用 HashMap 来保存元素,对于 HashSet 中保存的对象,请注意正确重写其 equals 和 hashCode 方法,以保证放入的对象的唯一性。

  • LinkedHashSet 是 Set 的一个具体实现,其维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。

Vector

Stack

相关文章

网友评论

      本文标题:java 集合类,ConcurrentHashMap详解

      本文链接:https://www.haomeiwen.com/subject/plvqgftx.html