HashTable
HashTable是线程安全的,那么HashTable做了什么操作才实现了HashMap没做到的线程安全呢???
写个简单的demo:
进入synchronizedMap内部看一下:
声明了一个mutex修饰对象的成员,使用互斥锁包围起来保证了内容互斥,串行化访问以此来保证线程安全。
HashTable也是一样的和HashMap实现逻辑没有什么区别,是用同样的去加锁而已:
但是串行化访问效率就会降低,那么既要保证线程安全又要保证效率要怎么办呢?这个时候ConcurrentHashMap就出现了。
ConcurrentHashMap
如果不用ConcurrentHashMap,那么要如何去优化HashTable呢???
通过锁粒度细化,整锁拆解成多个锁进行优化
在JDK5中的ConcurrentHashMap确实这么实现的,通过分段锁的Segment实现的结构是数组加上链表。而Segment可以对应头结点去存放默认的16把锁,区别是比起HashMap给每个头结点配一把锁,从串行化的HashTable简单对比效率提升了16倍。
如果7号锁下的线程去访问数组的话,其他节点占有的线程也可以去访问这些资源不会被阻塞,只有访问相同的Segment才会被阻塞。
但是每个bucket都用不同的锁去管理,性能上来说还是存在一定的缺陷,JDK8中ConcurrentHashMap使用CAS+synchronized使锁变得更加细粒度化,同时又进行了优化。
下图是ConcurrentHashMap的图解:
JDK8中ConcurrentHashMap随着最新的HashMap,使用了数组+链表+红黑树这种结构在效率上进行一定的提升。
下面来看一下ConcurrentHashMap的源码:
从成员变量来看,和HashMap存在相似之处:
最重要的是sizeCtl的成员变量,是JUC下面ConcurrentHashMap十分关键的一个成员变量。
是一个大小控制的标识符,哈希表初始化或者扩容的和时候的一个控制位标示量,负数代表正在进行初始化或者扩容操作,-1代表正在进行初始化,其他的负数代表有n-1个线程正在等待扩容,整合和为0代表还没有被初始化。
被volatile修饰,是多线程可见的:
ConcurrentHashMap是使用CAS+synchronized做到高效保证线程安全的,来看看put方法:
简单描述一下:
- 使用for循环,存在CAS操作要保证不断的去请求操作,之后判断是否为空,是空的话就去初始化;
- 如果不是空,进行哈希寻址找到头结点的存储位置;
- 如果这个位置不存在,使用CAS操作创建出来,添加失败break进入下一个循环;
- 如果发现这个key已经存在,说明ConcurrentHashMap正在进行remove,remove说明正在进行扩容就协助进行扩容,执行helpTransfer操作。
最后一种情况是哈希碰撞:
这个时候会锁住链表或者红黑二叉树的头结点,就是数组元素,操作如下:
此外ConcurrentHashMap还可以构建本地缓存来降低程序的计算量和负责度。。。
总结一下ConcurrentHashMap的put方法的实现逻辑:
- 判断Node[]数组是否进行了初始化,没有进行初始化操作;
- 通过hash定位数组的索引坐标是否有Node节点,如果没有使用CAS进行添加,添加失败进行下一次循环;
- 检查到内部正在扩容,就帮助一起扩容;
- 如果链表头结点是空,用synchronized锁住头结点;
(1)、如果是Node进行链表的添加操作;
(2)、如果是TreeNode进行树添加操作;
(3)、如果是ReservationNode则抛出异常,ReservationNode和ConcurrentHashMap本地缓存相关; - 判断链表长度达到8将链表转换成红黑树结构。
总结一下ConcurrentHashMap在JDK8和JDK5版本的差异:
总体还是使用的锁分离的思想就是比Segment更加细致,首先使用无锁操作CAS插入头结点,失败就循环重试,如果头结点已经存在就尝试获取头结点的同步锁再进行操作。
总结一下HashMap、HashTable、ConcurrentHashMap三者的区别:
- HashMap线程不安全,数据结构是数组+链表+红黑树;
- HashTable线程安全,数据结构是数组+链表;
- ConcurrentHashMap线程安全,使用CAS+同步锁,数据结构是数组+链表+红黑树;
- HashMap的key和value可以为null,其他两个不可以。