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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM