理论上,每个人知道hashmap不是线程安全的,并且它不应该被使用在多线程应用中。但是依然有人提出自己的能在上下文中使用hashmap的理论。一些人说他们仅仅是为了读数据并且map不会被写太多,不辛的是,当遇到一个并发问题时,并没有好的解释去支持。
通常,大多数人并没有懂得围绕在JMM和并发下的基本原理。一个人不能责备他们不知道他们的基本原理
public class MapTestTask implements Runnable {
private Map hashMap;
private Object value = new Object();
public MapTestTask (Map map ){
this.hashMap - map;
}
public void run (){
hashMap.put(Thread.currentThread(),value);
Ojbect retrieved = hashMap.get(Thread.currentThread());
if(retrieved == null) {
// Can it ever Happen
}
}
}
现在问题是当我们跑多线程时,是否能看到retrieved
为空。
如果我们按顺序去看,它是不会发生的。但是当牵扯到并发时,这可能发生;我将给你一个复现这个场景的代码。
这是我的个人建议:不会当涉及到并发时,不要做太多设计。在并发环境中,没有一种理论能经历时间的考验。 粗略而简便的方法是,使用java自带的线程安全集合 在任何涉及到并发的地方。
最后我将给你
java JDK升级到1.8后有些集合类的实现有了变化,其中ConcurrentHashMap进行结构上的大调整。jdk1.6、1.7实现的共同点主要是通过采用分段锁Segment减少热点域来提高并发效率,
重要概念
在正式研究前,我们需要先知道几个重要参数,提前说明其值所代表的意义以便更好的讲解源码实现。
table所有数据都存在table中,table的容量会根据实际情况进行扩容,table[i]存放的数据类型有以下3种:
TreeBin 用于包装红黑树结构的结点类型
ForwardingNode 扩容时存放的结点类型,并发扩容的实现关键之一
Node 普通结点类型,表示链表头结点’
nextTable
扩容时用于存放数据的变量,扩容完成后会置为null。
sizeCtl
以volatile修饰的sizeCtl用于数组初始化与扩容控制,它有以下几个值:
当前未初始化:
= 0 //未指定初始容量
> 0 //由指定的初始容量计算而来,再找最近的2的幂次方。
//比如传入6,计算公式为6+6/2+1=10,最近的2的幂次方为16,所以sizeCtl就为16。
初始化中:
= -1 //table正在初始化
= -N //N是int类型,分为两部分,高15位是指定容量标识,低16位表示
//并行扩容线程数+1,具体在resizeStamp函数介绍。
初始化完成:
=table.length * 0.75 //扩容阈值调为table容量大小的0.75倍
其它的分析相应源码时再细说。
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash //01111111_11111111_11111111_11111111
static final int spread(int h) {
//无符号右移加入高位影响,与HASH_BITS做与操作保留对hash有用的比特位,有让hash>0的意思
return (h ^ (h >>> 16)) & HASH_BITS;
}
ConcurrentHashMap实现原理总结
1.7版本的ReentrantLock+Segment+HashEntry,
1.8版本中synchronized+CAS+HashEntry+红黑树。
1.数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
2.保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
3.锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
4.链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
5.查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。