Before JDK1.8版本
Hashtable
我们都知道HashMap是线程不安全的。Hashtable是线程安全的。看过Hashtable源码的我们都知道Hashtable的线程安全是采用在每个方法来添加了synchronized关键字来修饰,即Hashtable是针对整个table的锁定,这样就导致HashTable容器在竞争激烈的并发环境下表现出效率低下。翻阅Hashtable的源码,确实在put和get方法之前都添加了synchronized来完成同步操作,这样无论是在put还是get的时候只能同时进行一种操作,毫无疑问这种效率是极其低下的。
锁分离技术
基于Hashtable的缺点,人们就开始思考,假如容器里有多把锁,每一把锁用于锁容器其中一部分数据,那么当多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率呢??这就是我们的“锁分离”技术,这也是ConcurrentHashMap实现的基础。
ConcurrentHashMap使用的就是锁分段技术,ConcurrentHashMap由多个Segment组成(Segment下包含很多Node,也就是我们的键值对了),每个Segment都有把锁来实现线程安全,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。
ConcurrentHashMap JDK1.8
前言
- JDK1.8的ConcurrentHashMap中Segment虽保留,但已经简化属性,仅仅是为了兼容旧版本。
- ConcurrentHashMap的底层与Java1.8的HashMap有相通之处,底层依然由“数组”+链表+红黑树来实现的,底层结构存放的是TreeBin对象,而不是TreeNode对象。
- ConcurrentHashMap实现中借用了较多的CAS算法,unsafe.compareAndSwapInt(this, valueOffset, expect, update); CAS(Compare And Swap)。
ConcurrentHashMap既然借助了CAS来实现非阻塞的无锁实现线程安全,那么是不是就没有用锁了呢?在操作hash值相同的链表的头结点还是会synchronized上锁,这样才能保证线程安全。因为修改链表头结点会涉及到一系列的操作,这些操作必须要保证原子性。
重要属性
|
|
Node/TreeNode/TreeBin
- Node类
Node是最核心的内部类,它包装了key-value键值对,所有插入ConcurrentHashMap的数据都包装在这里面。它与HashMap中的定义很相似,但是但是有一些差别就是value和next设置成了volatile变量,不允许调用setValue方法直接改变Node的value域,它增加了find方法辅助map.get()方法。
|
|
- TreeNode类
树节点类,另外一个核心的数据结构。当链表长度过长的时候,会转换为TreeNode。但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把每个Node结点包装成TreeNode,然后将这些TreeNode放在一个TreeBin对象中,由TreeBin完成对红黑树的包装。
|
|
- TreeBin类
这个类并不负责包装用户的key、value信息,而是包装的很多TreeNode节点。它代替了TreeNode的根节点,也就是说在实际的ConcurrentHashMap“数组”中,存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。另外这个类还带有了读写锁。
下图是整个TreeBin类的结构图:
TreeBin类中除了TreeBin构造方法,还有rotateLeft、rotateRight和balanceInsertion等,这些概念我们在数据结构中应该接触过,另外,还有lockRoot、unlockRoot方法,是为了树在restructuring的时候,进行写锁保护(write lock)。
下面主要来看看TreeBin构造方法:
Unsafe与CAS
在ConcurrentHashMap中,随处可以看到U, 大量使用了U.compareAndSwapXXX的方法,这个方法是利用一个CAS算法实现无锁化的修改值的操作,他可以大大降低锁带来的性能消耗。这个算法的基本思想就是不断地去比较当前内存中的变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。CAS的详细可以参见CAS原子操作.
下面是基于CAS的对指定位置数据的修改方法,分别是调用的U的三个原子操作:
initTable
对于ConcurrentHashMap来说,调用它的构造方法仅仅是设置了一些参数而已。而整个table的初始化是在向ConcurrentHashMap中插入元素的时候发生的。如调用put、computeIfAbsent、compute、merge等方法的时候,调用时机是检查table==null。
put
前面的所有的介绍其实都为这个方法做铺垫。ConcurrentHashMap最常用的就是put和get两个方法,不允许key或value为null的情况放入,这一点跟HashMap相反。整体流程就是对于每一个放入的值,首先利用spread方法对key的hashcode进行一次hash计算,由此来确定这个值在table中的位置。
- 如果这个位置是空的,那么直接放入,而且不需要加锁操作。
- 如果这个位置不是空的,存在结点,先要加锁,也说明发生了hash碰撞,首先判断这个节点的类型。
- 如果是链表结点Node(fh>0),则得到的结点就是hash值相同的节点组成的链表的头节点。需要依次向后遍历确定这个新加入的值所在位置。如果遇到hash值与key值都与新加入节点是一致的情况,则只需要更新value值即可。否则依次向后遍历,直到链表尾插入这个结点。如果加入这个节点以后链表长度大于8,就把这个链表转换成红黑树。
- 如果这个节点的类型已经是树节点的话,直接调用树节点的插入方法进行插入新的值。
源码如下:
下面是treeifyBin方法,这个方法用于将过长的链表转换为TreeBin对象。但是他并不是直接转换,而是进行一次容量判断,如果容量没有达到转换的要求,直接进行扩容操作并返回;如果满足条件才链表的结构抓换为TreeBin ,这与HashMap不同的是,它并没有把TreeNode直接放入红黑树,而是利用了TreeBin这个小容器来封装所有的TreeNode.
get
get方法比较简单,给定一个key来确定value的时候,必须满足两个条件 key相同 hash值相同,对于节点可能在链表或树上的情况,需要分别去查找.
参考
ConcurrentHashMap源码分析(JDK8版本)
《Java源码分析》:ConcurrentHashMap JDK1.8
-EOF-