HashMap的线程安全问题

有过java开发经验的从都知道 ,HashMap不是线程安全的,今天我打算用代码来试验下它的不安全性

 

代码 :

package com.study;

import com.entry.HashMapEntry;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;

import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CountDownLatch;

public class HashMapStudy {

public static void main(String []args){

Map<HashMapEntry,Object> map = new HashMap<>();
//同步计数器
CountDownLatch cd = new CountDownLatch(1);

Thread[] threads = new Thread[20];
for(int i=0;i<20;i++){
final int j = i;
threads[i] = new Thread(() ->{
try{
cd.await();
map.put(new HashMapEntry("name"+j,j),j);
}catch (Exception e){
}
});
threads[i].start();
}

//打印map被修改次数
System.out.println(map.size());
cd.countDown();
try {
//主线程休眠1秒(如果不休眠,下面的打印修改次数会为0,侧面说明线程和主线程是同时进行的)
Thread.sleep(1000);
}catch (Exception e){

}
//再次打印修改次数()
System.out.println(map.size());
Class clazz = map.getClass();
try{
//利用反射查看map中的结构
Class nodeClass = Class.forName("java.util.HashMap$Node");
Field t = null;
Field[] fields = clazz.getDeclaredFields();
Field nodeField = null;

for(Field field:fields){
if(field.getName().equals("table")){
t = field;
}
}
t.setAccessible(true);
Object[] table = (Object[]) t.get(map);
int i = 0;
for(Object o : table){
if(null == o){
continue;
}
//查看链表中的结构
nodeField = nodeClass.getDeclaredField("next");
nodeField.setAccessible(true);
i++;
System.out.println(o);
while( (o = nodeField.get(o)) != null){
i++;
System.out.println(o);
}
}
System.out.println(i);
}catch (Exception e){

System.out.println(e.getMessage());
}
}
}

HashMapEntr对象:
package com.entry;

public class HashMapEntry {

public HashMapEntry(String name,int hash){
this.name = name;
this.hash = hash;
}

private String name;

private int hash;

@Override
public int hashCode(){

return super.hashCode();
}

@Override
public boolean equals(Object obj){

if(null == obj){
return false;
}

if(!(obj instanceof HashMapEntry)){
return false;
}

HashMapEntry that = (HashMapEntry) obj;

return that.getName().equals(this.getName());
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getHash() {
return hash;
}

public void setHash(int hash) {
this.hash = hash;
}
}

 

我用20个线程利用一个同步计数器往map中put数据,结果

 

 从结果来看,map被修改了20次,但是map中的数据只有18个,说明在put的时候有的两个数据因为线程冲突被覆盖掉了,在HashMapEntry中我用的是Object的HashCode方法,这种方法的Hash冲突不是很严重,如果我们自己定义返回一个常数的话,我们得到的结果差异会更大。

 

为什么20个线程同时去put,但是里面的数据会少了呢?

看过HashMap源码的人可能知道 ,HshaMpa是由数组和链表作为底层的存储结构的,当put方法被调用的时候,会计算key的Hash值来确定数据的数组中的位置,如果这个位置没有,那么直接这设值,如果有数据 ,会去对比是不是同一个对象 ,如果是,会替换老的数据,否则,会在这个位置形成一个链表。

 

那么,当我们有多个线程同时调用这个方法的时候 ,由于这个方法没有任何的同步措施,如果两个线程同时同时操作的时候 ,他们要put的数据计算的位置都是同一个的时候,这时候本来应该形成的链表,但是恰好两个线程检测到这个位置没有数据,于是总有一个线程会把另外一个线程的数据覆盖掉,这个时候就会出现上述的现象 ------数据丢了。

 

当我使用synchronize关键字了之后,修改次数和map中的数据都会达到一致。

 

扩展:我们知道当HashMap的冲突很严重的时候先是会形成链表,进而当链表达到一定的长度后会转换成红黑树,而这个临界值是8,如果只是这样认知的话,其实是不准确 的,有兴趣的可以把代码copy下来,debug运行一下 ,(把HashMapEntry中的hashCode方法改成一个常量,然后step调试,就会发现链表长度达到 8 之后不会立即转换成红黑树)

posted @ 2020-08-21 20:16  jie的博客  阅读(1194)  评论(0编辑  收藏  举报