相信很多人都知道jdk7及其以前版本的hashmap在并发场景下使用时存在死循环(注意是死循环,不是死锁)的问题,问题出在扩容时对链表逆序的问题,下面是出问题的相关源码:
代码语言:javascript复制 /**
* Transfers all entries from current table to newTable.
*/
void transfer(Entry[] newTable, boolean rehash) {
int newCapacity = newTable.length;
for (Entry<K,V> e : table) {
while(null != e) {
Entry<K,V> next = e.next;
if (rehash) {
e.hash = null == e.key ? 0 : hash(e.key);
}
int i = indexFor(e.hash, newCapacity); //所在散列桶序号
e.next = newTable[i]; //这边做了逆序处理,在多线程使用时会导致死循环
newTable[i] = e;
e = next;
}
}
}
jdk8对这一问题做了相关补救工作,不会出现死循环
代码语言:javascript复制 final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//oldCap代表有没有数据插入,table创建采用lazy模式,只有使用时才创建
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) { //已经有数据,证明不是初始化
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)//老的threshold大于16时,新的threshold才进行double处理
newThr = oldThr << 1; // double threshold
}//oldCap == 0,初始化,创建hashmap时没有设置capacity
else if (oldThr > 0) // initial capacity was placed in threshold
//oldThr > 0 && oldCap == 0
newCap = oldThr;
else { // zero initial threshold signifies using defaults
//oldThr == 0 && oldCap == 0,初始化
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {//
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {//插入数据时扩容
for (int j = 0; j < oldCap; j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {//该散列桶已有数据,需要将该散列桶中数据重新映射
oldTab[j] = null;
if (e.next == null)//只有一个数据
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)//该散列桶中数据红黑树存储
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order 保证原来的顺序,否则在多线程误用时会导致死循环
//有多个数据,而且是链表形式存储
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
/**
* 该hash在老哈希桶数组位置为(oldCap-1) & hash,在扩容后哈希桶下标为(2*oldCap-1) & hash
* 2*oldCap-1相当于在oldCap-1的基础上左侧高位再加一个1,所以插入数据hash值在oldCap-1高一位是0还是1将决定
* 数据是否需要迁移,如果是1,则迁移到 j oldCap位置,如果是0,则保持原位置
*/
//与JDK7不同的是JDK8在扩容时原数据没有进行链表倒置操作,因为这个在并发编程时会导致
if ((e.hash & oldCap) == 0) {//在原来的散列桶
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {//不在原来的散列桶
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
jdk8及其以上版本在多线程下使用虽然没有死循环问题,但是仍然不是安全的,存在数据丢失以及异常的问题,数据丢失比如在插入时,多个线程同时在一个节点上增加新的节点,多个线程都会将自己新增的节点与某个节点A绑定关系,这样就会导致其他线程与节点A的关联关系丢失,类似mysql的update如果不做处理也存在更新丢失问题。异常发生在TreeNode与Node节点强行转换的地方,比如TreeNode类的moveRootToFront方法,测试代码可以参考下面:
代码语言:javascript复制 /**
* JDK8的hashmap虽然没有jdk7及以前版本存在的死循环问题,但仍然存在丢失数据问题
*/
@Test
public void test(){
Map<String,String> map = new HashMap<>();
Thread[] ts = new Thread[50];
for(int i=0;i<50;i ){
int k = i;
Thread t = new Thread(()->{
for(int j=0;j<500;j ){
map.put(String.format("1001-=",k,j),"h");
}
});
t.start();
ts[i] = t;
}
for(int i=0;i<50;i ){
try {
ts[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(map.keySet().size());
Map<String,String> map1 = new HashMap<>();
for(int i=0;i<50;i ){
int k = i;
Thread t = new Thread(()->{
for(int j=0;j<500;j ){
map1.put(String.format("1001-=",k,j),"h");
}
});
t.start();
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
for(int i=0;i<50;i ){
try {
ts[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(map1.keySet().size());
}
所以要在并发场景下使用map,可以使用ConcurrentHashMap