java數據結構-Map集合(1)

基本介紹和使用

Key-value 鍵值對形式的容器

在java.util包中

創建容器方式:

1.無參數

2.有參數//設置默認容量

基本使用方法:

HashMapo<Integer,String>map=new HashMap();

E v = put(key,value)//增

putAll(map);//加map中全部的數據

get(key)//取得元素返回值是value

E v = remove(key)//
boolean = remove(key,value)
replace(oldkey,newValue)//改
put(oldKey,newValue)
size()//獲取有效元素個數

//map集合的遍歷
//先獲取全部的key
Set keys = map.keySet();//返回值就是Set可以⽀持泛型
//獲取⼀個迭代器對象裝滿了key的值
Iteratorit=keys.itetrator();
while(it.hasNext()){
Ineteger Key =it.next():
String value=map.get(key);
}

特點
HashMap
key⽆序⽆重複
value⽆序可重複
如果重複 將原來的元素覆蓋
適合查找某⼀個元素 唯⼀存在

內部數據結構

java中兩大基本數據結構,數組加鏈表,數組(ArrayList底層)遍歷非常快,插入和刪除困難,鏈表(Linklist底層):遍歷滿,插入和刪除容易,HashMap是將兩者結合

數組每個地方都存在一個key-value的實例,java7中叫Entry java8叫Node
在這裏插入圖片描述
本身所有的位置都爲null,在put插入的時候會根據key的運用hash算法去計算一個index值(源碼分析,hash如何確定)

進行hash算法計算的hashmap內部的hash()函數

把key-value 的實例放到index 對應的數組位置中
但是
數組長度有限,hash值有可能是相同的,多個index 相同的 key-value實例,會在同一個node形成鏈表:
在這裏插入圖片描述
每一個節點都會保存自身的hash、key、value、和下一個節點:單個entry的結構圖
在這裏插入圖片描述
隨着數據量的增加,同一個節點上的鏈表可能掛載的元素變得很多,鏈表變長,查詢的性能變慢,在java8後爲了解決這一問題,採用了
數組+ 鏈表/紅黑樹的形式:當鏈表長還是<8時候仍然是鏈表形式,當>8時會變成紅黑樹,當紅黑樹內部元素個數小於6會轉爲鏈表
如圖:
在這裏插入圖片描述

總結:java7之前是數組加鏈表,java8之後是數組+ 鏈表(<8)/紅黑樹

1.判斷數組是否爲空,爲空則進行初始化;//怎麼初始化呢

2.如果不爲空,計算k的hash值,通過(n-1)&hash計算應當存放在數組中的下標index

3.查看table[index]是否存在數據,沒有數據舊構造一個Node節點存放在table[index]中

4.存在數據,說明發生了hash衝突,繼續判斷key是否相等,相等,用新的value替換原數據(onlyIfAbsent爲false);

5.如果不相等,判斷當前節點類型是不是樹型節點,如果是樹型節點,創建樹型節點插入紅黑樹中

6.如果不是樹型節點,創建普通Node加入鏈表中,判斷鏈表長度是否大於8,大於的話鏈表轉換爲紅黑樹

7.插入完成後判斷當前節點數是否大於閾值,如果大於開始擴容爲原數組的兩倍

HashMap 默認容量機制:

容量的定義:
HashMap 有兩個“容量” 一個是size 是Map 所有元素的個數(我裝“了”多少元素)(也表示數組的容量)
一個是capacity是Map的容量(我“能裝多少元素”)
例子:

try {
            Map<String,String> map =new HashMap<>(16);
            map.put("key","value");
            System.out.println("size"+map.size());
            Class clazz=map.getClass();//因爲capactiy 的得到方法爲私有,這裏通過反射去得到並執行私有的方法
            Method getCapcity =   clazz.getDeclaredMethod("capacity");
            getCapcity.setAccessible(true);
            System.out.println("capacity"+getCapcity.invoke(map));

        } catch (Exception e) {
            e.printStackTrace();
        }

HashMap的構造中如果沒有指定容量,會得到一個默認容量爲16(數組長度爲16),

初始化大小是16的原因:

static final int DEFAULT_INITIAL_CAPACITY=1 <<4

1.一般的初始容量最好是2的冪,這是爲了方便位運算,位運算遠比平常的算術運算高很多,
2.選2的4次方 16 是爲了服務將key映射到index 的算法, index=HashCode (key) & (Length-1)
例子:
一個key的hash後的二進制爲
10111011000010110100
Length-1=15 15的二級製爲1111
index = 10111011000010110100 & 1111 得到的值爲0100轉十進制爲4
所有的key都需要拿到對應的hash,但爲了保證鏈表不要其中某個長的太突出,需要儘可能得到一個均勻分佈的hash值
Length爲2的冪時,Length-1 的值是所有二進制位全爲1,這時index的結果等同於HashCode後幾位的值
?只要輸入的HashCode本身分佈均勻,Hash算法的結果就是 分佈均勻的

具體容量16唄作爲一個經驗值採用了:讓這個初始值不能太小(頻繁擴容影響效率)不能太大(浪費空間)

指定容量初始化

hashmap根據用戶傳入的初始化容量,理工無符號右位移和按位或運算等方式計算出第一個大於該數的2的冪

負載因子是0.75 如果自己傳入初始大小k,會對k進行數據處理,處理成爲大於k的2的整數次方

如果用戶傳17,那麼hashmap初始化大小爲32;

實例如下:

 try{
            HashMap map=new HashMap(17);
            Class clazz=map.getClass();//因爲capacity的得到方法爲私有,
            Method getCapcity=clazz.getDeclaredMethod("capacity");
            getCapcity.setAccessible(true);
            System.out.println("size:"+map.size());
            System.out.println("capacity:"+getCapcity.invoke(map));
        }catch (Exception e ){
			           e.printStackTrace();
        }

在這裏插入圖片描述

//當傳入值非2的n次方是,容量計算的實現代碼
static final int tableSizeFor(int cap){
    int n =cap-1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
//初始二進制數依次右位移1,2,4,6,8,16位,然後與自己異或(“|”)
    //最終把高位第一個爲1的數後面全變爲1
    //111111 +1 =100000=2^6??
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    //三位運算符做一個極限值的判斷把之前計算得到的數值+1
    
}

hashmap在沒有給定默認值時,第一次put時候,會進行一次初始化這很耗費性能,一般開發中我們儘可能的爲map給予初始值(這裏阿里開發手冊也提到)

如何確定比較適合的默認值呢

要知道map不是滿了才擴容,他的擴容臨界點默認是0.75*capacity

這時候可能有個問題是:如果我設置的默認值,到默認值前就到了擴容臨界點了怎麼辦

要知道擴容的性能代價不是一般的大

比如設置 new HashMap(7) 7是我想要的默認值,認爲這個容器裝7個就夠了

但是容器幫你將capaity擴容到8 ,但是這時候的容器擴容臨界點爲8*0.75=6;

在元素數量還沒到你預定的默認值就會開始進行‘“漫長”的擴容

遇到這種預想默認值大於臨界值的情況應該怎麼辦:

可以通過一個簡單的算法

expectsize/0.75F + 1.0F

7/0.75+1=10 的默認值 會被設置成16 這就大大減少了擴容的機率

將預想值進行一個小小的處理就能一定程度上解決擴容的機率//只是會犧牲些內存

//注:這個F是因爲小數默認是double類型 加個F後綴提醒程序這事個Float類型的再次給初學者提醒下

算法在map源碼中有實現開發時候,通過Map創建一個HashMap實現這個容量改變:

Map<String,String>map=Maps.newHashMapWithExpectedSize(7);

源碼碼如下

public static <K,V> HashMap<K,V> newHashMapWithExpectedSize(int expectedSize){
    return new HashMap(capacity(expectedSize));
}
static int capacity(int expectedSize){
    if(expectedSize < 3){
        CollectPreconditions.checkNonnegative(expectedSize,"expectedSize");
        return expectedSize +1;
    }else{
        return expectedSize < 1073741824 ? (int)((float)expectedSize / 0.75F + 1.0F) : 2147483647;
    }
}

當向map中put一個元素會通過hash方法確定吧這個元素放到數組的什麼地方

Map類集合中hash函數設計:(定位)

哈希:
定義:是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數,翻譯做“散列”,把任意長度的輸入,通過散列算法,變換成固定長度的輸出,輸出的就是散列值,這種轉換時一種壓縮映射,散列值的空間通常遠小於輸入的空間,
Tips:不同的輸入可能會散列出相同的輸出,:散列值不同 輸入值肯定不同,散列值相同,輸入值不一定相同
理解圖如下
在這裏插入圖片描述
不同的輸入值根據同一個散列函數計算的值相同的現象叫做碰撞
常見的Hash函數:

直接定址法:直接以關鍵字k或者k加上某個常數(k+c)作爲hash地址
數字分析法:提取關鍵字中取值比較均勻的數字作爲哈希地址
除留餘數法:用關鍵字k除以某個不大於哈希表長度m和數p,將所得餘數作爲哈希表地址
分段疊加法:按照哈希表地址位數將關鍵字分層位數相等的幾部分,其中最後一部分可以比較短,然後將這幾部分相加,捨棄最高進位後的結果就是該關鍵字的哈希地址
平方取中法:如果關鍵字各個部分分佈都不均勻的話,可以先求出它的平方值,然後按照需求取中間的幾位作爲哈希地址。
僞隨機數法:採用一個僞隨機數當作哈希函數。

衡量一個hash函數好壞的重要指標就是發生碰撞的概率和解決方案,但任何hash函數基本無法徹底避免碰撞

常見的解決碰撞的方法有以下幾種

  1. 開放地址法:一旦發生衝突,就去尋找下一個空的散列地址,只要散列表足夠大,空的散列地址總能找到,並將記錄存入。
  2. 鏈地址法:將哈希表的每個單元作爲鏈表的頭結點,所有哈希地址爲i的元素構成一個同義詞鏈表。即發生衝突時就把該關鍵字鏈在以該單元爲頭結點的鏈表的尾部。
  3. 再哈希法:當哈希地址發生衝突用其他的函數計算另一個哈希函數地址,直到衝突不再產生爲止
  4. 建立公共溢出區:將哈希表分爲基本表和溢出表兩部分,發生衝突的元素都放入溢出表中

hash方法源碼解析

hash方法:

hashmap中定義了一個hash(Object k)方法 被內部負責增刪的方法調用,內部充當定位的一個角色

hash基本原理:調用Object 的hashCode()方法計算key的hash值返回一個整數, (int hash(Object k))

然後根據容器的容量進行取模球的最後index值( indexFor(int h ,int length))

jdk1.7中的hash方法

final int hash(Object k){
    int h=hashSeed;
    if(0!= h && k instanceof String){//String 有它自己變成hash的一套方式
        retrun sun.misc.Hashing.stringHash32((String)k)
    }
    
    h ^= k.hashCode();
    //解決hash衝突
    h ^=(h>>>20)^(h>>>12);
    return h^(>>>7)^(h>>>4);
}

static int  indexFor(int h,int length){
    return h&(length-1);
}

indexFor 取模 解析:

方法的目的是爲了取模:數學計算中叫做取餘數 h%length

方法考慮到效率問題將數學運算變成了位運算:

//位運算直接對內存數據進行操作,不用變成十進制,比一般轉成二進制再轉回來的數學運算快很多

​ h&(length-1)=h%length

位運算 實現取模運算一樣的結果的原理:
//前文說到length無論怎麼初始化它都是2^n

h & (2^n -1)=h2^n

設n爲3 2^3爲8 二進制爲1000. 2^3-1=7的二進制位0111

h & 1000 相當於取二進制後三位數 結果爲 0111

2^n-1 形成一個 低位掩碼,“與”操作的結果就是散列值h的值高位全部歸0 ,只保留低位值

如下

 01001010(h)
&00000111(lenght-1)
==================
 00000010
 

所以如果有人問爲什麼hashmap的容量必須是2的次方,主要的原因就是爲了方便內部的位運算尤其是取模時候的位運算

位運算作用:1.性能

2.解決了index爲負數的問題 :2^n-1 首先值是正數,二進制第一位是0,與hash碼取模位運算時第一位是0

//位運算問題?

但是這樣只取後面幾位進行取模,依然很有可能產生重複

hash衝突問題解決(擾動函數機制)

衝突問題是什麼:多個不同的鍵,按位與運算後得到的結果相同

java7處理hash衝突:通過讓自己的高位取和低位取做異或,混合原始哈希碼的高位和低位,加大低位的隨機性

h^=k.hashCode();
h^=(h>>>20^(h>>>12));
return h^(h>>>7) ^ (h>>>4);//擾動計算

這段代碼是爲了對key的hashcode 進行擾動計算,防止不同hashCode=的高位不同但低位相同導致hash衝突,簡單點講,就是爲了把高位的特徵和低位的特徵組合起來,降低哈希衝突的概率,也就是說,儘量做到任何以爲的變化都能對最終得到的結果產生影響

新java8的hash函數

在java8之前使用單項鍊表來存儲相同index的元素,這樣有可能會讓Hashmap的get方法從0(1)降低到0(n)

java8 使用平衡樹來替代鏈表 存儲衝突的元素讓性能從0(n)提高到0(logn)

//0(n) 0(1)

// 如果惡意程序知道我們用的是Hash算法,則在純鏈表情況下,它能夠發送大量請求導致哈希碰撞,然後不停訪問這些key導致HashMap忙於進行線性查找,最終陷入癱瘓,即形成了拒絕服務攻擊(DoS)。

java 8hash函數做了優化,只做了一次右位移異或混合,而不是四次

static final int hash(Object key) {
   int h;
   return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

HashMap擴容和插入機制:

內部數組容量是有限的 ,多次插入後,到達一定的數量就會進行擴容,也就是resize
redsize 擴容的時機
兩個因素:
Capacity:HashMap 當前長度
LoadFactor (DEAFAULT_LOAD_FACTOR) 負載因子默認爲0.75,負載因子確認的是擴容的時機,當容量到某點時候就擴容
擴容點=當前容量*負載因子

默認情況當size 大於capacity*0.75 就會觸發機制

擴容程序:
1.擴容:創建一個新的Entry 數組容量是原來的兩本
2.ReHash: 遍歷原Entry 數組,把所有的Entry重新Hash,算一遍hash值,根據hash進行index放置
賦值要Hash而不是直接複製的原因
長度擴大以後,Hash的規則也隨之改變:
Hash公式–>index =HashCode (key) & (Length-1)
所以擴容後可能的結構樣子是這樣的:
在這裏插入圖片描述

新節點的插入方式:
java8之前是頭插法,就是說新來的值會取代原有的值,原有的值就順推到鏈表中去//這時候的原因是新的元素被查找的可能性更大
java8之後是尾插法:爲什麼換成尾插法

java8前的頭插法:
如果想容量爲2的put 兩個值,負載因子是0.75 ,因爲2*0.75=1,當即將插入第二個就會擴容
當想要在容量爲2的容器李用不同的線程插入A、B、C,三個線程可能是在resize之前就執行了插入

鏈表的指向爲A->B->C
在這裏插入圖片描述
這時候在進行擴容
在java8前 resize 的賦值方式,
1.使用了單鏈表的頭插入方式,同一位置新元素總會被放在鏈表的頭部位置
2.在舊數組同一條Entry鏈上的元素,會同重新計算索引位置
但是在頭插法的過程中可能會出現原本後面的值突然到了頭部,原本的頭部跑到了後面,而它的指向並沒有變
如圖
在這裏插入圖片描述
這時候就會出現環形鏈表
取值就會出現Infinit Loop

java8的尾插法:
使用了尾插法後,即使是擴容後也能保證鏈表元素原本的順序,就能解決鏈表成環的問題

HaspMap數據插入流程:
在這裏插入圖片描述

Hashmap在1.7的resize中的transfer函數//這個transfer在做什麼?--------------

void transfer(Entry [] newTable,boolean rehash){
    int newCapacity=newTable.length;
    for(Entry<K,V> e:table){//遍歷每一個key-value Entry節點
        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);//計算新表的位置
            //根據hash值和容量取模
            //注意這段代碼,如果多線程環境線程在這裏掛起
            //能看出這是頭插法:相鄰兩個元素之間通過建立中間變量進行賦值實現兩者調轉來實現元素像下移動,過程如下圖
            e.next=newTable[i];//e要插入到鏈表的頭部,先用e.next指向新的hash表的第一個元素
            newTable[i]=e;//新的表的頭指針依然指向e沒轉移前端第一個元素,所以要將新hash表的頭指針指向e
            e=next;//轉移e節點開始下一步循環
        }
    }
}

總結:
java7 多線程操作Hashmap 因爲頭插法導致 了鏈表前後倒置 形成環鏈表形成死循環
java8 同樣多線程環境,因爲尾插法導致順序擴容前後一致,不會形成環鏈表的死循環
但是線程安全並不侷限於此:讀某個值時候,因爲hashmap沒有加線程所,並不能保證,開始讀值時到讀值後,兩個時間點,值沒有被更改過,就算改了插入方法依然保證不了線程安全

HashMap 的去重機制:
HashMap 的key 是不允許的重複的,
去重原理就是在add之前看看這個key對象是否與內部有重複,而HashMap內部通過重寫了Object這個所謂的祖先類(所有對象默認繼承Object)的equals方法和hashCode,用來比較對象是否相同
在Object 的原來equals方法中equals僅僅通過== ,對於值對象判斷值,對於引用比較地址

 public boolean equals(Object obj) {
        return (this == obj);
    }

HashMap 重寫的equals相對來講比較複雜,畢竟也有可能出現地址不一樣但內容一樣的這種情況
Tip:這個方法是重寫了AbstractMap的方法(它繼承了AbstractMap)

  public final boolean equals(Object o) {
            if (o == this)//如果地址直接一樣,直接返回true結束方法
                return true;
            if (o instanceof Map.Entry) {//如果它屬於節點對象
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&//比較entry對象的key 和value 與o 的key和value
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }   

但是,hashmap index 的確定是根據hashCode,同一條鏈表的對象如果hash值相同,就很難具體的得到鏈表上某個具體的對象
這時候需要重寫hashCode方法避免給每個對象賦予只屬於它的hash碼作爲標識

擴容機制:

/**
* 初始化或加倍擴容的表大小如果爲空
*/
final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        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)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            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;
                            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;
    }

Hashmap在java7與java8的區別

HashMap 中java7 和java8的線程安全問題

hashmap java7中在多線程中產生的問題:

我們開多個線程進行put操作

public class Text{
       public static void main(String []args){
       	 	HashMapThread thread0 = new HashMapThread();
       	    HashMapThread thread1 = new HashMapThread();
       	 	HashMapThread thread2 = new HashMapThread();
        	HashMapThread thread3 = new HashMapThread();
        	HashMapThread thread4 = new HashMapThread();
        	thread0.start();
			thread1.start();
        	thread2.start();
       		thread3.start();
         	thread4.start();
       }
       class MapThread extends Thread{
       		private static AtomicInteger atomicInteger=new AtomicInteger();
       		//一個線程安全的Interger包裝類
       		private static Map<Integer,Integer> map =new HashMap();
       }   
       public void run(){
           while(atomicInteger.get()<1000000){
               map.put(atomicInteger.get(),ai.get());
               atomicInteger.incrementAndGet();//原子性自增
              
           }
       }
}

以上代碼運行可能出現兩個問題:

1.死循環

2.數組越界出現:ArrayIndexOutOfBoundsException

問題出現原因:

多線程環境put導致get死循環

原因是多線程進行put操作時候觸發了HashMap 擴容(resize函數),出現鏈表兩個節點形成閉環導致死循環,put操作流程:

死循環發生在HashMap的擴容函數中,根源在transfer函數中:

多線程中,線程在transfer函數中的:e.next=newTable[i]中被掛起

void transfer(Entry [] newTable,boolean rehash){
    int newCapacity=newTable.length;
    for(Entry<K,V> e:table){//遍歷每一個key-value Entry節點
        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);//計算新表的位置
            //根據hash值和容量取模
            //注意這段代碼,如果多線程環境線程在這裏掛起
            //能看出這是頭插法:相鄰兩個元素之間通過建立中間變量進行賦值實現兩者調轉來實現元素像下移動,過程如下圖
            e.next=newTable[i];//e要插入到鏈表的頭部,先用e.next指向新的hash表的第一個元素
            newTable[i]=e;//新的表的頭指針依然指向e沒轉移前端第一個元素,所以要將新hash表的頭指針指向e
            e=next;//轉移e節點開始下一步循環
        }
    }
}

上面這段代碼流程分析

假設size=2,key爲 3,7,5

在這裏插入圖片描述

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-jf8aiPT7-1593424689218)(image\微信截圖_20200629162558.png)]
在這裏插入圖片描述

最終

單線程環境下resize在rehash後
在這裏插入圖片描述

key 爲5 被hash到1的index位置,重新hash後,7這個原來的末尾值作爲新值插入到3的頭部

多線程環境時候 如果有兩個線程A和B 都在進行put操作//

如果線程A在transfer 的 newTable[i]= e 這行代碼中掛起//

在這裏插入圖片描述

在這裏插入圖片描述
擴容時候造成的數據丟失分析:

此時應該再放一遍transfer源碼:

void transfer(Entry [] newTable,boolean rehash){
    int newCapacity=newTable.length;
    for(Entry<K,V> e:table){//遍歷每一個key-value Entry節點
        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];//e要插入到鏈表的頭部,先用e.next指向新的hash表的第一個元素
            newTable[i]=e;//線程A在這裏掛起
            e=next;//轉移e節點開始下一步循環
        }
    }
}

線程A掛起時:
在這裏插入圖片描述
線程B完成後
在這裏插入圖片描述

開啓A線程

在A線程中e=7 next =5 newTable[3]=null

繼續開始執行後

此時e 爲5

next =e.next //next =null

e.next=newTable[1]//e.next=5

newTable[1]=e // newTable[1]=5

此時因爲newTable[3]=null //會有元素丟失

java8中的hashmap的優化:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))//如果沒有hash碰撞則直接插入元素
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

這是1.8中 HashMap中put操作的主函數,第6行中國若沒有hash碰撞會直接插入元素,如果線程A和線程B同時put操作,剛好不同的數據hash值都一樣,並該位置數據爲null,兩個線程都會進入第六行代碼,如果A進入後在還未進行數據插入時候掛起,B正常執行,和插入數據,A喚醒後,不用再進行hash判斷,會將B插入的數據覆蓋

總結:

jdk1.7的線程問題是:形成環形鏈或者數據丟失

jdk.1.8的線程問題是:發生數據覆蓋

參考資料

https://mp.weixin.qq.com/s/ktre8-C-cP_2HZxVW5fomQ

https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650126376&idx=1&sn=6ff01e62f001084c35b72e3615471100&chksm=f36ba509c41c2c1f502446a260890e6741d3a5257749e4b9757eacd4d6727042b02724f4c7e0&mpshare=1&scene=1&srcid=0617liVpx2YPGcNCYCAWqSI4&sharer_sharetime=1592403240996&sharer_shareid=48f6d0356198efc068ac3be54595c640&key=74cd249bb6cd420f039f4125d52dc4fbea61100f4280157b23544499f28ccce1cdc500271976554e140562bd506131590207227bd1fdcda4c648a3ec8c506c2b67fd90f2b47692967d4c413eeba9b683&ascene=1&uin=NzU1NjUzMDM2&devicetype=Windows+10+x64&version=62090070&lang=zh_CN&exportkey=A9OWou4STTudvBFdIQ0EIQE%3D&pass_ticket=oJhfBWnYt1x1XWTXlfV2aqTgTV8MldZMMbfbBWktBUmaVkhRwJM151xGFJ7kvrns

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章