有過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 之后不會立即轉換成紅黑樹)