日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > java >内容正文

java

hashmap value占用空间大小_【Java集合框架002】原理层面:HashMap全解析

發(fā)布時(shí)間:2023/12/10 java 46 豆豆
生活随笔 收集整理的這篇文章主要介紹了 hashmap value占用空间大小_【Java集合框架002】原理层面:HashMap全解析 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

一、前言

二、HashMap

2.1 HashMap數(shù)據(jù)結(jié)構(gòu) + HashMap線程不安全 + 哈希沖突

2.1.1 HashMap數(shù)據(jù)結(jié)構(gòu)

學(xué)習(xí)的時(shí)候,先整體后細(xì)節(jié),HashMap整體結(jié)構(gòu)是 底層數(shù)組+鏈表 ,先記住,再開始看下面的

HashMap相關(guān)知識(shí)點(diǎn):

  • 底層數(shù)據(jù)結(jié)構(gòu):HashMap基于哈希散列表實(shí)現(xiàn) ,可以實(shí)現(xiàn)對(duì)數(shù)據(jù)的讀寫。

  • 插入邏輯put()方法:將鍵值對(duì)傳遞給put方法時(shí),它調(diào)用鍵對(duì)象的hashCode()方法來計(jì)算hashCode,然后找到相應(yīng)的bucket位置(即數(shù)組)來儲(chǔ)存值對(duì)象。當(dāng)獲取對(duì)象時(shí),通過鍵對(duì)象的equals()方法找到正確的鍵值對(duì),然后返回值對(duì)象。

  • 哈希沖突:插入邏輯中,如果發(fā)生哈希沖突,使用鏈表來解決hash沖突問題,即當(dāng)發(fā)生沖突了,對(duì)象將會(huì)儲(chǔ)存在鏈表的頭節(jié)點(diǎn)中。HashMap在每個(gè)鏈表節(jié)點(diǎn)中儲(chǔ)存鍵值對(duì)對(duì)象,當(dāng)兩個(gè)不同的鍵對(duì)象的hashCode相同時(shí)(HashMap在兩個(gè)key-value,hashcode相同導(dǎo)致index相同,就認(rèn)為發(fā)生哈希沖突,equals相同任務(wù)同一個(gè),不重復(fù)插入,HashSet在hashcode和equals相同,認(rèn)為同一個(gè),不重復(fù)插入),它們會(huì)儲(chǔ)存在同一個(gè)bucket位置的鏈表中,如果鏈表大小超過閾值(TREEIFY_THRESHOLD,8),鏈表就會(huì)被改造為樹形結(jié)構(gòu)。

  • HashMap和HashSet:HashMap在兩個(gè)key-value,hashcode相同導(dǎo)致index相同,哈希沖突,equals相同任務(wù)同一個(gè),不重復(fù)插入,HashSet在hashcode和equals相同,認(rèn)為同一個(gè),不重復(fù)插入。

  • 2.1.2 HashMap線程不安全

    HashMap是應(yīng)用更廣泛的哈希表實(shí)現(xiàn),而且大部分情況下,都能在常數(shù)時(shí)間性能的情況下進(jìn)行put和get操作。但是,HashMap在多線程并發(fā)的情況下是不安全的,通過兩個(gè)問題的回答來解釋原因。

    問題1:為什么說HashMap是線程不安全的?
    回答1:在接近臨界點(diǎn)時(shí),若此時(shí)兩個(gè)或者多個(gè)線程進(jìn)行put操作,都會(huì)進(jìn)行resize(擴(kuò)容)和reHash(為key重新計(jì)算所在位置),而reHash在并發(fā)的情況下可能會(huì)形成鏈表環(huán)。即在多線程環(huán)境下,使用HashMap進(jìn)行put操作會(huì)引起死循環(huán),導(dǎo)致CPU利用率接近100%,所以在并發(fā)情況下不能使用HashMap。

    問題2:為什么在并發(fā)執(zhí)行put操作會(huì)引起死循環(huán)?
    回答2:因?yàn)槎嗑€程會(huì)導(dǎo)致HashMap的Entry鏈表形成環(huán)形數(shù)據(jù)結(jié)構(gòu),一旦形成環(huán)形數(shù)據(jù)結(jié)構(gòu),Entry的next節(jié)點(diǎn)永遠(yuǎn)不為空,就會(huì)產(chǎn)生死循環(huán)獲取Entry。值得注意的是,JDK1.7的情況下,并發(fā)擴(kuò)容時(shí)容易形成鏈表環(huán),此情況在1.8時(shí)就好太多太多了,因?yàn)樵?.8中當(dāng)鏈表長(zhǎng)度大于閾值(默認(rèn)長(zhǎng)度為8)時(shí),鏈表會(huì)被改成樹形(紅黑樹)結(jié)構(gòu)。

    2.1.3 哈希沖突

    哈希沖突定義:若干Key的哈希值按數(shù)組大小取模后,如果落在同一個(gè)數(shù)組下標(biāo)上,將組成一條Entry鏈,對(duì)Key的查找需要遍歷Entry鏈上的每個(gè)元素執(zhí)行equals()比較(tip:知道了HashMap的“數(shù)組+鏈表”結(jié)構(gòu),就很好懂哈希沖突了)。

    加載因子:為了降低哈希沖突的概率,默認(rèn)當(dāng)HashMap中的鍵值對(duì)達(dá)到數(shù)組大小的75%時(shí),即會(huì)觸發(fā)擴(kuò)容。因此,如果預(yù)估容量是100,即需要設(shè)定100/0.75=134的數(shù)組大小,又因?yàn)镠ashMap大小必須是2的整數(shù)次冪,所以是大于134的最小的2的整數(shù)次冪,即256。

    減少哈希沖突兩方法:降低加載因子,加大初始大小。

    HashMap中數(shù)組擴(kuò)容相關(guān)概念(size capacity size/capacity):

  • size是當(dāng)前現(xiàn)實(shí)的記錄數(shù);capacity是理論上的容量,第一次由初始化決定,之后由擴(kuò)容決定;initial capacity是初始的capacity,第一次的capacity。

  • 負(fù)載因子等于“size/capacity”,全稱為當(dāng)前負(fù)載因子,其作用是當(dāng)負(fù)載因子達(dá)到負(fù)載極限的時(shí)候擴(kuò)容。當(dāng)負(fù)載因子為0,表示空的hash表;負(fù)載因子為0.5,表示半滿的散列表,依此類推。輕負(fù)載的散列表具有沖突少、適宜插入與查詢的特點(diǎn)(但是使用Iterator迭代元素時(shí)比較慢)。

  • 負(fù)載極限:“負(fù)載極限”是一個(gè)0~1的數(shù)值,“負(fù)載極限”決定了hash表的最大填滿程度。當(dāng)hash表中的負(fù)載因子達(dá)到指定的“負(fù)載極限”時(shí),hash表會(huì)自動(dòng)成倍地增加容量(桶的數(shù)量),并將原有的對(duì)象重新分配,放入新的桶內(nèi),這稱為rehashing。HashMap和HashTable的構(gòu)造器允許指定一個(gè)負(fù)載極限,HashMap和HashTable默認(rèn)的“負(fù)載極限”為0.75,這表明當(dāng)該hash表的3/4已經(jīng)被填滿時(shí),hash表會(huì)發(fā)生rehashing。

  • “負(fù)載極限”的默認(rèn)值(0.75)是時(shí)間和空間成本上的一種折中:

    程序員可以根據(jù)實(shí)際情況來調(diào)整“負(fù)載極限”值。

    • 較高的“負(fù)載極限”可以降低hash表所占用的內(nèi)存空間,但會(huì)增加查詢數(shù)據(jù)的時(shí)間開銷,而查詢是最頻繁的操作(HashMap的get()與put()方法都要用到查詢),所以會(huì)影響時(shí)間性能;

    • 較低的“負(fù)載極限”會(huì)提高查詢數(shù)據(jù)的性能,但會(huì)增加hash表所占用的內(nèi)存開銷,影響空間性能;

    以上專有名詞連接起來是:size是記錄數(shù),capacity是桶數(shù)量,兩者size/capacity得到負(fù)載因子,當(dāng)負(fù)載因子達(dá)到理論設(shè)定的負(fù)載極限的時(shí)候擴(kuò)容。專有名詞(size、capacity、負(fù)載因子、負(fù)載極限、擴(kuò)容(具體擴(kuò)容邏輯)),所有的這些都可以在源碼中找到邏輯。

    2.2 JDK1.7中HashMap的實(shí)現(xiàn)

    2.2.1 基本元素Entry

    數(shù)組中的每一個(gè)元素其實(shí)就是Entry[] table,Map中的key和value就是以Entry的形式存儲(chǔ)的。Entry包含四個(gè)屬性:key、value、hash值和用于單向鏈表的next。關(guān)于Entry的具體定義參看如下源碼:

    static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry next; int hash; Entry(int h, K k, V v, Entry n) { value = v; next = n; key = k; hash = h; } public final K getKey() { return key; } public final V getValue() { return value; } public final V setValue(V newValue) { V oldValue = value; value = newValue; return oldValue; } public final boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry e = (Map.Entry)o; Object k1 = getKey(); Object k2 = e.getKey(); if (k1 == k2 || (k1 != null && k1.equals(k2))) { Object v1 = getValue(); Object v2 = e.getValue(); if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); } public final String toString() { return getKey() + "=" + getValue(); } /** * This method is invoked whenever the value in an entry is * overwritten by an invocation of put(k,v) for a key k that's already * in the HashMap. */ void recordAccess(HashMap m) { } /** * This method is invoked whenever the entry is * removed from the table. */ void recordRemoval(HashMap m) { }}

    JDK7中的HashMap,小結(jié)為以下幾點(diǎn):

  • 基本元素為Entry,Entry包含四個(gè)屬性:key、value、int類型的hash值和用于單向鏈表的Node類型的next;

  • hash不是用于新插入的Entry和原有的鏈表節(jié)點(diǎn)的hashcode比較的,只是用于計(jì)算一下數(shù)組index的;

  • key,value 是用于新插入的Entry和原有的鏈表的節(jié)點(diǎn)的 key,value 比較的,只有到equals和hashcode(index)比較都先相同,就認(rèn)為是相同元素,被認(rèn)為是相同就不會(huì)插入HashMap;

  • next用于下一個(gè)鏈表中下一個(gè)節(jié)點(diǎn)。

  • 2.2.2 插入邏輯

    2.2.2.1 put()方法 = hash()方法 + indexFor()方法 + equals()方法

    當(dāng)向 HashMap 中 put一對(duì)鍵值時(shí),它會(huì)根據(jù) key的 hashCode 值計(jì)算出一個(gè)位置, 該位置就是此對(duì)象準(zhǔn)備往數(shù)組中存放的位置。該計(jì)算過程參看如下代碼:

    transient int hashSeed = 0;final int hash(Object k) { int h = hashSeed; if (0 != h && k instanceof String) { return sun.misc.Hashing.stringHash32((String) k); } h ^= k.hashCode(); h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } static int indexFor(int h, int length) { return h & (length-1);?}

    hash(Object)方法是用來計(jì)算哈希值的,indexFor(hash,length)方法是用來計(jì)算數(shù)組下標(biāo)的。
    兩者關(guān)系:indexFor方法根據(jù)hash(Object)方法的返回值作為實(shí)參來計(jì)算數(shù)組下標(biāo)。

    金手指:(put操作中的)hashcode方法和equals方法

  • 第一步,確定數(shù)組下標(biāo),indexFor使用hash方法計(jì)算出來的值得到數(shù)組下標(biāo);

  • 第二步,插入,如果指定的數(shù)組下標(biāo)無對(duì)象存在,不發(fā)生哈希沖突,直接插入;

  • 第三步,如果指定的數(shù)組下標(biāo)有對(duì)象存在,發(fā)生哈希沖突,使用equals對(duì)鏈條上對(duì)象比較,全部為false插入,其中一個(gè)為true,表示已經(jīng)存在(hash和equals都相同就是存在)

  • 小結(jié):僅以put操作為例,插入操作中hash方法用來作為計(jì)算數(shù)組下標(biāo)的輸入,equals用于比較對(duì)象是否存在。

    問題1:指定數(shù)組下標(biāo)有值,哈希沖突后如何處理?
    回答1:put操作的時(shí)候,當(dāng)兩個(gè)key通過hashCode計(jì)算相同時(shí),則發(fā)生了hash沖突(碰撞),HashMap解決hash沖突的方式是用鏈表(拉鏈法),當(dāng)發(fā)生hash沖突時(shí),則將存放在數(shù)組中的Entry設(shè)置為新值的next(比如A和B都hash后都映射到下標(biāo)i中,之前已經(jīng)有A了,當(dāng)map.put(B)時(shí),將B放到下標(biāo)i中,A則為B的next,所以新值存放在鏈表最頭部的數(shù)組中,舊值在新值的鏈表上)。

    問題2:哈希沖突發(fā)生后,為什么后插入的值要放在鏈表頭部?
    回答2:因?yàn)楹蟛迦氲腅ntry是“熱乎的”,被查找的可能性更大(因?yàn)間et查詢的時(shí)候會(huì)遍歷整個(gè)鏈表),既然后插入的Entry是“熱乎的”,那么這個(gè)后插入的Entry應(yīng)該放在哪里呢?當(dāng)然是放在鏈表頭部,因?yàn)殒湵聿檎覐?fù)雜度為O(n),插入和刪除復(fù)雜度為O(1),如果將新值插在末尾,就需要先經(jīng)過一輪遍歷,這個(gè)開銷大,如果是插在頭結(jié)點(diǎn),省去了遍歷的開銷,還發(fā)揮了鏈表插入性能高的優(yōu)勢(shì)。

    2.2.2.2 addEntry() + createEntry()

    添加節(jié)點(diǎn)到鏈表中:找到數(shù)組下標(biāo)后,會(huì)先進(jìn)行key判重,如果沒有重復(fù),就準(zhǔn)備將新值放入到鏈表的表頭。

    void addEntry(int hash, K key, V value, int bucketIndex) { // addEntry方法中,如果當(dāng)前 HashMap 大小已經(jīng)達(dá)到了閾值,并且新值要插入的數(shù)組位置已經(jīng)有元素了,那么要擴(kuò)容 if ((size >= threshold) && (null != table[bucketIndex])) { // 擴(kuò)容 resize(2 * table.length); // 擴(kuò)容以后,重新計(jì)算 hash 值 hash = (null != key) ? hash(key) : 0; // 重新計(jì)算擴(kuò)容后的新的下標(biāo) bucketIndex = indexFor(hash, table.length); } // createEntry就是插入新值 createEntry(hash, key, value, bucketIndex); // key value由方法參數(shù)提供,未擴(kuò)容,hash bucketIndex使用方法參數(shù)傳遞的,擴(kuò)容,hash bucketIndex使用新計(jì)算的}// 這個(gè)很簡(jiǎn)單,其實(shí)就是將新值放到鏈表的表頭,然后 size++void createEntry(int hash, K key, V value, int bucketIndex) { Entry e = table[bucketIndex]; table[bucketIndex] = new Entry<>(hash, key, value, e); size++;}

    上述代碼解釋了:JDK7情況下的擴(kuò)容

  • addEntry方法中,如果當(dāng)前 HashMap 大小已經(jīng)達(dá)到了閾值,并且新值要插入的數(shù)組位置已經(jīng)有元素了,那么要擴(kuò)容

  • HashMap擴(kuò)容方式:兩倍擴(kuò)容

  • 擴(kuò)容后重新計(jì)算要已經(jīng)插入了的key的數(shù)組下標(biāo):先hash,然后indexFor

  • 新元素插入指定數(shù)組下標(biāo)的鏈頭,table[bucketIndex] = new Entry<>(hash, key, value, e); 新建一個(gè)Entry就是一個(gè)元素

  • 這個(gè)方法的主要邏輯就是先判斷是否需要擴(kuò)容,需要帶的話先擴(kuò)容,然后再將這個(gè)新的數(shù)據(jù)插入到擴(kuò)容后的數(shù)組的相應(yīng)位置處的鏈表的表頭。

    2.2.3 擴(kuò)容邏輯:resize()

    定義:擴(kuò)容就是用一個(gè)新的大數(shù)組替換原來的小數(shù)組,并將原來數(shù)組中的值遷移到新的數(shù)組中。

    由于是雙倍擴(kuò)容,遷移過程中,會(huì)將原來table[i]中的鏈表的所有節(jié)點(diǎn),分拆到新的數(shù)組的newTable[i]和newTable[i+oldLength]位置上。比如:原來數(shù)組長(zhǎng)度是16,那么擴(kuò)容后,原來table[0]處的鏈表中的所有元素會(huì)被分配到新數(shù)組中newTable[0]和newTable[16]這兩個(gè)位置。擴(kuò)容期間,由于會(huì)新建一個(gè)新的空數(shù)組,并且用舊的項(xiàng)填充到這個(gè)新的數(shù)組中去。所以,在這個(gè)填充的過程中,如果有線程獲取值,很可能會(huì)取到 null 值,而不是我們所希望的、原來添加的值。

    我們對(duì)照HashMap的結(jié)構(gòu)來說,如下:

    上圖中,左邊部分即代表哈希表,也稱為哈希數(shù)組(默認(rèn)數(shù)組大小是16,每對(duì)key-value鍵值對(duì)其實(shí)是存在map的內(nèi)部類entry里的),數(shù)組的每個(gè)元素都是一個(gè)單鏈表的頭節(jié)點(diǎn),跟著的藍(lán)色鏈表是用來解決沖突的,如果不同的key映射到了數(shù)組的同一位置處,就將其放入單鏈表中。

    當(dāng)size>=threshold( threshold等于“容量*負(fù)載因子”)時(shí),會(huì)發(fā)生擴(kuò)容。

    void addEntry(int hash, K key, V value, int bucketIndex) { if ((size >= threshold) && (null != table[bucketIndex])) { resize(2 * table.length); // 擴(kuò)容 hash = (null != key) ? hash(key) : 0; // 擴(kuò)容后要用新的hash,不能用參數(shù)的 bucketIndex = indexFor(hash, table.length); // 擴(kuò)容后要用新的bucketIndex,不用用參數(shù)的 } createEntry(hash, key, value, bucketIndex); // key value由方法參數(shù)提供,未擴(kuò)容,hash bucketIndex使用方法參數(shù)傳遞的,擴(kuò)容,hash bucketIndex使用新計(jì)算的}

    特別提示:JDK1.7中resize,只有當(dāng) size>=threshold 并且 table中的那個(gè)槽中已經(jīng)有Entry時(shí),才會(huì)發(fā)生resize。即有可能雖然size>=threshold,但是必須等到相應(yīng)的槽至少有一個(gè)Entry時(shí),才會(huì)觸發(fā)擴(kuò)容,可以通過上面的代碼看到每次resize都會(huì)擴(kuò)大一倍容量(2 * table.length)。

    2.2.4 null處理

    前面說過HashMap的key是允許為null的,當(dāng)出現(xiàn)這種情況時(shí),會(huì)放到table[0]中。

    private V putForNullKey(V value) { for (Entry e = table[0]; e != null; e = e.next) { if (e.key == null) { V oldValue = e.value; e.value = value; e.recordAccess(this); return oldValue; } } modCount++; addEntry(0, null, value, 0); return null;}

    put()邏輯:hash()方法、indexFor()方法、equals()方法
    金手指:(put操作中的)hashcode方法和equals方法
    JDK7 put 子步驟:擴(kuò)容 + createEntry插入 + 哈希沖突 ( JDK8 put 子步驟:擴(kuò)容 + createEntry插入 + 哈希沖突鏈表/樹化 )

  • 第一步,使用hash和tab.lenght計(jì)算數(shù)組下標(biāo)index,準(zhǔn)備插入,index=hash&(tab.length-1);

  • 第二步,執(zhí)行插入,如果指定的數(shù)組下標(biāo)無對(duì)象存在,不發(fā)生哈希沖突,直接插入;

  • 第三步,如果指定的數(shù)組下標(biāo)有對(duì)象存在,表示發(fā)生哈希沖突,使用equals對(duì)鏈條上對(duì)象比較,全部為false插入,其中一個(gè)為true,表示已經(jīng)存在(hash和equals都相同就是存在)

  • 小結(jié):插入操作中hash方法用來作為計(jì)算數(shù)組下標(biāo)的輸入?yún)?shù),equals用于比較鏈表上對(duì)象是否存在

    小結(jié):JDK7情況插入情況下的擴(kuò)容 addEntry

  • 擴(kuò)容方式:HashMap擴(kuò)容方式:兩倍擴(kuò)容 newsize=oldsize *2

  • 數(shù)組擴(kuò)容的觸發(fā)-兩個(gè)條件:addEntry方法中,如果當(dāng)前 HashMap 大小已經(jīng)達(dá)到了閾值75%,并且新值要插入的數(shù)組位置已經(jīng)有元素了,才執(zhí)行擴(kuò)容(兩個(gè)條件,即有可能雖然size>=threshold,但是必須等到相應(yīng)的槽至少有一個(gè)Entry時(shí),才會(huì)擴(kuò)容)

  • 擴(kuò)容后重新計(jì)算要已經(jīng)插入了的key的數(shù)組下標(biāo):使用hash和tab.lenght計(jì)算數(shù)組下標(biāo)index,準(zhǔn)備插入,index=hash&(tab.length-1);

  • 擴(kuò)容后的插入,對(duì)于原來數(shù)組的位置:擴(kuò)容就是用一個(gè)新的大數(shù)組替換原來的小數(shù)組,并將原來數(shù)組中的值遷移到新的數(shù)組中。由于是雙倍擴(kuò)容,遷移過程中,會(huì)將原來table[i]中的鏈表的所有節(jié)點(diǎn),分拆到新的數(shù)組的newTable[i]和newTable[i+oldLength]位置上。如原來數(shù)組長(zhǎng)度是16,那么擴(kuò)容后,原來table[0]處的鏈表中的所有元素會(huì)被分配到新數(shù)組中newTable[0]和newTable[16]這兩個(gè)位置,從而減小鏈表長(zhǎng)度。

  • 擴(kuò)容后的新插入:equals比較全部為false,然后將新元素插入指定數(shù)組下標(biāo)的鏈頭,table[bucketIndex] = new Entry<>(hash, key, value, e); 新建一個(gè)Entry就是一個(gè)元素

  • 擴(kuò)容過程中的隱患:擴(kuò)容期間,由于會(huì)新建一個(gè)新的空數(shù)組,并且用舊的項(xiàng)填充到這個(gè)新的數(shù)組中去。所以,在這個(gè)填充的過程中,如果有線程獲取值,很可能會(huì)取到 null 值,而不是我們所希望的、原來添加的值。

  • tip:ArrayList 1.5倍擴(kuò)容,Vector兩倍擴(kuò)容,HashTable newsize=2oldsize +1 ,HashMap newsize=2oldsize

    辨析:擴(kuò)容、樹化、哈希沖突

  • 擴(kuò)容:擴(kuò)容的對(duì)象是數(shù)組,擴(kuò)容兩個(gè)條件:addEntry方法中,如果當(dāng)前 HashMap 大小已經(jīng)達(dá)到了閾值75%,并且新值要插入的數(shù)組位置已經(jīng)有元素了,才觸發(fā)擴(kuò)容(擴(kuò)容的第二個(gè)條件一定要put操作才能滿足)

  • 樹化:樹化的對(duì)象是鏈表,鏈表節(jié)點(diǎn)數(shù)達(dá)到8,且要求數(shù)組長(zhǎng)度大于64
    樹化的時(shí)機(jī):鏈表樹化一定要在put操作才會(huì)出現(xiàn),樹鏈表化一定要在remove操作才會(huì)出現(xiàn)。
    注意:樹化第二個(gè)要求:數(shù)組長(zhǎng)度必須大于等于MIN_TREEIFY_CAPACITY(64),否則繼續(xù)采用擴(kuò)容策略;

  • 哈希沖突:哈希沖突的定義是要插入的數(shù)組index位置有元素了。
    JDK7:
    (1)如果未發(fā)生哈希沖突,直接放到數(shù)組中;
    (2)如果哈希沖突,沒有達(dá)到閾值的75%,插入到鏈表后面;
    (3)如果哈希沖突,達(dá)到閾值75%,數(shù)組擴(kuò)容操作,擴(kuò)容后原數(shù)組元素和新插入的數(shù)組元素都要變動(dòng)的;
    JDK8:
    (1)如果未發(fā)生哈希沖突,直接放到數(shù)組中;插入完成后判斷閾值是否擴(kuò)容
    (2)如果哈希沖突,鏈表節(jié)點(diǎn)數(shù)未達(dá)到8,但是數(shù)組長(zhǎng)度小于64,尾插法放在鏈表后面,插入完成后判斷閾值是否擴(kuò)容
    (3)如果哈希沖突,鏈表節(jié)點(diǎn)數(shù)未達(dá)到8,但是數(shù)組長(zhǎng)度大于等于64,尾插法放在鏈表后面,插入完成后判斷閾值是否擴(kuò)容
    (4)如果哈希沖突,鏈表節(jié)點(diǎn)數(shù)達(dá)到8,但是數(shù)組長(zhǎng)度小于64,尾插法放在鏈表后面,插入完成后判斷閾值是否擴(kuò)容
    (5)如果哈希沖突,鏈表節(jié)點(diǎn)數(shù)達(dá)到8,且要求數(shù)組長(zhǎng)度大于等于64,尾插法插入到鏈表后面,鏈表樹化,插入完成后判斷閾值是否擴(kuò)容
    小結(jié):擴(kuò)容是數(shù)組擴(kuò)容,哈希沖突是數(shù)組哈希沖突,但是對(duì)于JDK8的HashMap,擴(kuò)容和是否產(chǎn)生哈希沖突無關(guān),擴(kuò)容是判斷 size/capacity

  • 小結(jié):擴(kuò)容、樹化、哈希沖突都是在put操作出現(xiàn),但是三種不同。擴(kuò)容和樹化都是put操作發(fā)生哈希沖突導(dǎo)致的,put操作如果不哈希沖突啥是沒有,所以只最重要的是設(shè)計(jì)分布均衡的哈希算法,頭插和尾插,擴(kuò)容和樹化都是緩解措施。JDK7是put涉及哈希沖突、數(shù)組擴(kuò)容,JDK8是put涉及哈希沖突、數(shù)組擴(kuò)容、鏈表樹化,JDK8中remove可以涉及樹鏈表化。

  • 2.3 JDK1.8中HashMap的實(shí)現(xiàn)(所有內(nèi)容圍繞JDK7與JDK8最大不同——鏈表/紅黑樹而展開)

    2.3.1 屬性支持

    HashMap底層維護(hù)的是數(shù)組+鏈表,我們可以通過一小段源碼來看看:

    /** * The default initial capacity - MUST be a power of two. * 即 默認(rèn)初始大小,值為16 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. * 即 最大容量,必須為2^30 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * The load factor used when none specified in constructor. * 負(fù)載因子為0.75 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. * 大致意思就是說hash沖突默認(rèn)采用單鏈表存儲(chǔ),當(dāng)單鏈表節(jié)點(diǎn)個(gè)數(shù)大于8時(shí),會(huì)轉(zhuǎn)化為紅黑樹存儲(chǔ) */ static final int TREEIFY_THRESHOLD = 8; /** * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. * hash沖突默認(rèn)采用單鏈表存儲(chǔ),當(dāng)單鏈表節(jié)點(diǎn)個(gè)數(shù)大于8時(shí),會(huì)轉(zhuǎn)化 為紅黑樹存儲(chǔ)。* 當(dāng)紅黑樹中節(jié)點(diǎn)少于6時(shí),則轉(zhuǎn)化為單鏈表存儲(chǔ) */ static final int UNTREEIFY_THRESHOLD = 6; /** * The smallest table capacity for which bins may be treeified. * (Otherwise the table is resized if too many nodes in a bin.) * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts * between resizing and treeification thresholds. * hash沖突默認(rèn)采用單鏈表存儲(chǔ),當(dāng)單鏈表節(jié)點(diǎn)個(gè)數(shù)大于8時(shí),會(huì)轉(zhuǎn)化為紅黑樹存儲(chǔ)。 * 但是有一個(gè)前提:要求數(shù)組長(zhǎng)度大于64,否則不會(huì)進(jìn)行轉(zhuǎn)化 */?static?final?int?MIN_TREEIFY_CAPACITY?=?64;

    通過以上代碼可以看出初始容量(16)、負(fù)載因子以及對(duì)數(shù)組的說明。

    小結(jié):HashMap相關(guān)的變量:

  • 初始化默認(rèn)大小是16 initial capacity 16 JDK7+JDK8都一樣

  • 最大容量,必須為2^30 JDK7+JDK8都一樣

  • 默認(rèn)負(fù)載因子為0.75 達(dá)到0.75就擴(kuò)容,JDK7+JDK8都一樣

  • 樹化閾值為8,鏈表化閾值為6 JDK8新增

  • 樹化的兩個(gè)條件:鏈表節(jié)點(diǎn)數(shù)達(dá)到8,且要求數(shù)組長(zhǎng)度大于64

    HashMap中最重要的兩個(gè)操作是擴(kuò)容和哈希沖突,但是要注意以下幾點(diǎn):

  • 擴(kuò)容是數(shù)組擴(kuò)容,哈希沖突是鏈表/紅黑樹的哈希沖突,兩者是的對(duì)象是不同的,關(guān)系是擴(kuò)容是為了減低負(fù)載因子,減少哈希沖突;

  • 減少哈希沖突兩個(gè)設(shè)計(jì):設(shè)計(jì)一個(gè)好的哈希算法 + 數(shù)組擴(kuò)容;

  • 對(duì)于真正發(fā)生了哈希沖突:JDK7是使用頭插法插入鏈表,JDK8額外添加了樹化邏輯;

  • 無論是數(shù)組擴(kuò)容還是哈希沖突后的鏈表/紅黑樹,都是發(fā)生在put操作中的,無論JDK7還是JDK8。put操作中的數(shù)組擴(kuò)容和鏈表/紅黑樹哈希沖突:
    JDK7的put方法:擴(kuò)容 + hashcode生成index插入 + 哈希沖突;
    JDK8的put方法:擴(kuò)容 + hashcode生成index插入 + 哈希沖突。

  • 2.3.2 基本元素Node

  • 在JDK1.8中HashMap的內(nèi)部結(jié)構(gòu)可以看作是數(shù)組(Node[] table)和鏈表的復(fù)合結(jié)構(gòu)。

  • 數(shù)組被分為一個(gè)個(gè)桶(bucket),通過哈希值決定了鍵值對(duì)在這個(gè)數(shù)組中的尋址(哈希值相同的鍵值對(duì),就是哈希沖突,則以鏈表形式存儲(chǔ)。

  • 如果鏈表大小超過閾值(TREEIFY_THRESHOLD,8),圖中的鏈表就會(huì)被改造為樹形(紅黑樹)結(jié)構(gòu)。

  • transient?Node<K,V>[]?table;
  • Entry的名字變成了Node,原因是和紅黑樹的實(shí)現(xiàn)TreeNode相關(guān)聯(lián)。1.8與1.7最大的不同就是利用了紅黑樹,即由數(shù)組+鏈表(或紅黑樹)組成。JDK1.8中,當(dāng)同一個(gè)hash值的節(jié)點(diǎn)數(shù)不小于8時(shí),將不再以單鏈表的形式存儲(chǔ)了,會(huì)被調(diào)整成一顆紅黑樹(上圖中null節(jié)點(diǎn)沒畫)。這就是JDK1.7與JDK1.8中HashMap實(shí)現(xiàn)的最大區(qū)別。

    在分析JDK1.7中HashMap的哈希沖突時(shí),不知大家是否有個(gè)疑問就是萬一發(fā)生碰撞的節(jié)點(diǎn)非常多怎么辦?如果說成百上千個(gè)節(jié)點(diǎn)在hash時(shí)發(fā)生碰撞,存儲(chǔ)一個(gè)鏈表中,那么如果要查找其中一個(gè)節(jié)點(diǎn),那就不可避免的花費(fèi)O(N)的查找時(shí)間,這將是多么大的性能損失。這個(gè)問題終于在JDK1.8中得到了解決,在最壞的情況下,鏈表查找的時(shí)間復(fù)雜度為O(n),而紅黑樹一直是O(logn),這樣會(huì)提高HashMap的效率。

    JDK1.7中HashMap采用的是位桶+鏈表的方式,即我們常說的散列鏈表的方式;
    JDK1.8中采用的是位桶+鏈表/紅黑樹的方式,雖然加快鏈表查找效率,但是也是非線程安全的,因?yàn)橹挥挟?dāng)某個(gè)位桶的鏈表的長(zhǎng)度達(dá)到某個(gè)閥值的時(shí)候,這個(gè)鏈表才會(huì)轉(zhuǎn)換成紅黑樹。

    小結(jié):從JDK7的Entry變?yōu)镴DK8的Node

  • 基本元素使用Node,意為紅黑樹的節(jié)點(diǎn),Node包含四個(gè)屬性:key、value、hash值和用于單向鏈表的next,和JDK7的Entry節(jié)點(diǎn)一樣;

  • hash不是用于新插入的Entry和原有的鏈表節(jié)點(diǎn)的hashcode比較的,只是用于計(jì)算一下數(shù)組index的;

  • key,value 是用于新插入的Entry和原有的鏈表的節(jié)點(diǎn)的 key,value 比較的,只有到equals和hashcode(index)比較都先相同,就認(rèn)為是相同元素,被認(rèn)為是相同元素的不插入HashMap;

  • next用于下一個(gè)鏈表中下一個(gè)節(jié)點(diǎn)。

  • 2.3.3 插入邏輯:put()方法

    通過分析put方法的源碼,可以讓這種區(qū)別更直觀:

    static final int TREEIFY_THRESHOLD = 8; // 樹化 public V put(K key, V value) { return putVal(hash(key), key, value, false, true); // 調(diào)用putVal } final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node[] tab; Node p; int n, i; //如果當(dāng)前map中無數(shù)據(jù),執(zhí)行resize方法。并且返回n JDK7先擴(kuò)容再插入,可能無效擴(kuò)容,JDK8先插入再擴(kuò)容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; //如果要插入的鍵值對(duì)要存放的這個(gè)位置剛好沒有元素,那么把他封裝成Node對(duì)象,放在這個(gè)位置上即可,插入的時(shí)候沒有哈希沖突 if ((p = tab[i = (n - 1) & hash]) == null) // 這里對(duì)p賦值,就是新的要插入的節(jié)點(diǎn) tab[i] = newNode(hash, key, value, null); // 插入 //否則的話,說明這數(shù)組上面有元素,插入的時(shí)候發(fā)生哈希沖突 else { Node e; K k; //如果這個(gè)元素的key與要插入的一樣,那么就替換一下。 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 直接將p賦值給局部變量e // 1.如果這個(gè)元素的key與要插入的不一樣,如果當(dāng)前節(jié)點(diǎn)是TreeNode類型的數(shù)據(jù),執(zhí)行putTreeVal方法 else if (p instanceof TreeNode) e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); else { // else表示如果這個(gè)元素的key與要插入的不一樣,如果還是遍歷這條鏈子上的數(shù)據(jù),跟JDK7沒什么區(qū)別 for (int binCount = 0; ; ++binCount) { // 循環(huán) if ((e = p.next) == null) { // 循環(huán)找到一個(gè)空位置的就插入鏈表 p.next = newNode(hash, key, value, null); //2.完成了操作后多做了一件事情,判斷,并且可能執(zhí)行treeifyBin方法 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; // 插入并判斷是否樹化,break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // 如果鏈表上已經(jīng)存在了,直接break;這里不用樹化了,應(yīng)該根本沒插入 p = e; // 不斷將e賦值給p,更新p,就是p在鏈條上不斷往后移動(dòng) } } // e 不為null 要么第一個(gè)if替換,要么else if樹插入,要么鏈表插入,總之插入成功了,返回oldValue if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) //true || -- e.value = value; //3. afterNodeAccess(e); return oldValue; } } ++modCount; //判斷閾值,決定是否數(shù)組擴(kuò)容 插入后決定是否擴(kuò)容 if (++size > threshold) resize(); //4. 插入之后的操作 afterNodeInsertion(evict); return null;????}

    以上代碼中的特別之處如下:

    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st???????treeifyBin(tab,?hash);

    treeifyBin()就是將鏈表轉(zhuǎn)換成紅黑樹。

    源碼解析putVal()操作(語言組織),如下:

    擴(kuò)容:如果當(dāng)前map中無數(shù)據(jù),執(zhí)行resize方法。并且返回n JDK8先擴(kuò)容再插入,JDK7先插入再擴(kuò)容,可能無效擴(kuò)容沒有哈希沖突:如果要插入的鍵值對(duì)要存放的這個(gè)位置剛好沒有元素,那么把他封裝成Node對(duì)象,放在這個(gè)位置上即可,插入的時(shí)候沒有哈希沖突 這里對(duì)p賦值,就是新的要插入的節(jié)點(diǎn) else 表示 否則的話,說明這數(shù)組上面有元素,插入的時(shí)候發(fā)生哈希沖突 if表示 //如果這個(gè)元素的key與要插入的一樣,那么就替換一下。 else if 表示 1.如果這個(gè)元素的key與要插入的不一樣,如果當(dāng)前節(jié)點(diǎn)是TreeNode類型的數(shù)據(jù),執(zhí)行putTreeVal方法 else表示如果這個(gè)元素的key與要插入的不一樣,如果還是遍歷這條鏈子上的數(shù)據(jù),跟JDK7沒什么區(qū)別 // 循環(huán) // 循環(huán)找到一個(gè)空位置的就插入鏈表 //2.完成了操作后多做了一件事情,判斷,并且可能執(zhí)行treeifyBin方法 // 插入并判斷是否樹化,break; // 如果鏈表上已經(jīng)存在了,直接break;這里不用樹化了,應(yīng)該根本沒插入 // 不斷將e賦值給p,更新p,就是p在鏈條上不斷往后移動(dòng) 返回oldValue:e 不為null 要么第一個(gè)if替換,要么else if樹插入,要么鏈表插入,總之插入成功了,返回oldValue 插入后判斷是否擴(kuò)容:判斷閾值,決定是否數(shù)組擴(kuò)容 插入后決定是否擴(kuò)容 最后 插入之后的操作?完成了。

    關(guān)于putVal(),注意以下三點(diǎn):

  • 樹化有個(gè)要求就是數(shù)組長(zhǎng)度必須大于等于MIN_TREEIFY_CAPACITY(64),否則繼續(xù)采用擴(kuò)容策略;

  • resize方法兼顧兩個(gè)職責(zé),創(chuàng)建初始存儲(chǔ)表格,或者在容量不滿足需求的時(shí)候;

  • 在JDK1.8中取消了indefFor()方法,直接用(tab.length-1)&hash,所以看到這個(gè),代表的就是數(shù)組的下角標(biāo)。

  • 2.3.4 treeifyBin()樹化為紅黑樹(難點(diǎn):要看懂這個(gè),一定要懂?dāng)?shù)據(jù)結(jié)構(gòu)紅黑樹)

    樹化操作的過程有點(diǎn)復(fù)雜,可以結(jié)合源碼來看看。將原本的單鏈表轉(zhuǎn)化為雙向鏈表,再遍歷這個(gè)雙向鏈表轉(zhuǎn)化為紅黑樹。

    final void treeifyBin(Node[] tab, int hash) { int n, index; Node e; //樹形化還有一個(gè)要求就是數(shù)組長(zhǎng)度必須大于等于64,否則繼續(xù)采用擴(kuò)容策略 if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode hd = null, tl = null;//hd指向首節(jié)點(diǎn),tl指向尾節(jié)點(diǎn) do { TreeNode p = replacementTreeNode(e, null);//將鏈表節(jié)點(diǎn)轉(zhuǎn)化為紅黑樹節(jié)點(diǎn) if (tl == null) // 如果尾節(jié)點(diǎn)為空,說明還沒有首節(jié)點(diǎn) hd = p; // 當(dāng)前節(jié)點(diǎn)作為首節(jié)點(diǎn) else { // 尾節(jié)點(diǎn)不為空,構(gòu)造一個(gè)雙向鏈表結(jié)構(gòu),將當(dāng)前節(jié)點(diǎn)追加到雙向鏈表的末尾 p.prev = tl; // 當(dāng)前樹節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)指向尾節(jié)點(diǎn) tl.next = p; // 尾節(jié)點(diǎn)的后一個(gè)節(jié)點(diǎn)指向當(dāng)前節(jié)點(diǎn) } tl = p; // 把當(dāng)前節(jié)點(diǎn)設(shè)為尾節(jié)點(diǎn) } while ((e = e.next) != null); // 繼續(xù)遍歷單鏈表 //將原本的單鏈表轉(zhuǎn)化為一個(gè)節(jié)點(diǎn)類型為TreeNode的雙向鏈表 if ((tab[index] = hd) != null) // 把轉(zhuǎn)換后的雙向鏈表,替換數(shù)組原來位置上的單向鏈表 hd.treeify(tab); // 將當(dāng)前雙向鏈表樹形化 }}

    特別注意:樹化有個(gè)要求就是數(shù)組長(zhǎng)度必須大于等于MIN_TREEIFY_CAPACITY(64),否則繼續(xù)采用擴(kuò)容策略。

    總的來說,HashMap默認(rèn)采用數(shù)組+單鏈表方式存儲(chǔ)元素,當(dāng)元素出現(xiàn)哈希沖突時(shí),會(huì)存儲(chǔ)到該位置的單鏈表中。但是單鏈表不會(huì)一直增加元素,當(dāng)元素個(gè)數(shù)超過8個(gè)時(shí),會(huì)嘗試將單鏈表轉(zhuǎn)化為紅黑樹存儲(chǔ)。但是在轉(zhuǎn)化前,會(huì)再判斷一次當(dāng)前數(shù)組的長(zhǎng)度,只有數(shù)組長(zhǎng)度大于64才處理。否則,進(jìn)行擴(kuò)容操作。

    將雙向鏈表轉(zhuǎn)化為紅黑樹的實(shí)現(xiàn):

    final void treeify(Node[] tab) { TreeNode root = null; // 定義紅黑樹的根節(jié)點(diǎn) for (TreeNode x = this, next; x != null; x = next) { // 從TreeNode雙向鏈表的頭節(jié)點(diǎn)開始逐個(gè)遍歷 next = (TreeNode)x.next; // 頭節(jié)點(diǎn)的后繼節(jié)點(diǎn) x.left = x.right = null; if (root == null) { x.parent = null; x.red = false; root = x; // 頭節(jié)點(diǎn)作為紅黑樹的根,設(shè)置為黑色 } else { // 紅黑樹存在根節(jié)點(diǎn) K k = x.key; int h = x.hash; Class> kc = null; for (TreeNode p = root;;) { // 從根開始遍歷整個(gè)紅黑樹 int dir, ph; K pk = p.key; if ((ph = p.hash) > h) // 當(dāng)前紅黑樹節(jié)點(diǎn)p的hash值大于雙向鏈表節(jié)點(diǎn)x的哈希值 dir = -1; else if (ph < h) // 當(dāng)前紅黑樹節(jié)點(diǎn)的hash值小于雙向鏈表節(jié)點(diǎn)x的哈希值 dir = 1; else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) // 當(dāng)前紅黑樹節(jié)點(diǎn)的hash值等于雙向鏈表節(jié)點(diǎn)x的哈希值,則如果key值采用比較器一致則比較key值 dir = tieBreakOrder(k, pk); //如果key值也一致則比較className和identityHashCode TreeNode xp = p; if ((p = (dir <= 0) ? p.left : p.right) == null) { // 如果當(dāng)前紅黑樹節(jié)點(diǎn)p是葉子節(jié)點(diǎn),那么雙向鏈表節(jié)點(diǎn)x就找到了插入的位置 x.parent = xp; if (dir <= 0) //根據(jù)dir的值,插入到p的左孩子或者右孩子 xp.left = x; else xp.right = x; root = balanceInsertion(root, x); //紅黑樹中插入元素,需要進(jìn)行平衡調(diào)整(過程和TreeMap調(diào)整邏輯一模一樣) break; } } } } //將TreeNode雙向鏈表轉(zhuǎn)化為紅黑樹結(jié)構(gòu)之后,由于紅黑樹是基于根節(jié)點(diǎn)進(jìn)行查找,所以必須將紅黑樹的根節(jié)點(diǎn)作為數(shù)組當(dāng)前位置的元素 moveRootToFront(tab, root);}

    然后將紅黑樹的根節(jié)點(diǎn)移動(dòng)端數(shù)組的索引所在位置上:

    static void moveRootToFront(Node[] tab, TreeNode root) { int n; if (root != null && tab != null && (n = tab.length) > 0) { int index = (n - 1) & root.hash; //找到紅黑樹根節(jié)點(diǎn)在數(shù)組中的位置 TreeNode first = (TreeNode)tab[index]; //獲取當(dāng)前數(shù)組中該位置的元素 if (root != first) { //紅黑樹根節(jié)點(diǎn)不是數(shù)組當(dāng)前位置的元素 Node rn; tab[index] = root; TreeNode rp = root.prev; if ((rn = root.next) != null) //將紅黑樹根節(jié)點(diǎn)前后節(jié)點(diǎn)相連 ((TreeNode)rn).prev = rp; if (rp != null) rp.next = rn; if (first != null) //將數(shù)組當(dāng)前位置的元素,作為紅黑樹根節(jié)點(diǎn)的后繼節(jié)點(diǎn) first.prev = root; root.next = first; root.prev = null; } assert checkInvariants(root); }}

    putVal方法處理的邏輯比較多,包括初始化、擴(kuò)容、樹化,近乎在這個(gè)方法中都能體現(xiàn),針對(duì)源碼簡(jiǎn)單講解下幾個(gè)關(guān)鍵點(diǎn):

    如果Node[] table是null,resize方法會(huì)負(fù)責(zé)初始化,即如下代碼:

    if ((tab = table) == null || (n = tab.length) == 0)????n?=?(tab?=?resize()).length;

    resize方法兼顧兩個(gè)職責(zé):創(chuàng)建初始存儲(chǔ)表格 + 在容量不滿足需求的時(shí)候進(jìn)行擴(kuò)容(resize)。

    在放置新的鍵值對(duì)的過程中,如果發(fā)生下面條件,就會(huì)發(fā)生擴(kuò)容。

    if (++size > threshold)????resize();

    具體鍵值對(duì)在哈希表中的位置(數(shù)組index)取決于下面的位運(yùn)算:

    i?=?(n?-?1)?&?hash

    仔細(xì)觀察哈希值的源頭,會(huì)發(fā)現(xiàn)它并不是key本身的hashCode,而是來自于HashMap內(nèi)部的另一個(gè)hash方法。為什么這里需要將高位數(shù)據(jù)移位到低位進(jìn)行異或運(yùn)算呢?這是因?yàn)橛行?shù)據(jù)計(jì)算出的哈希值差異主要在高位,而HashMap里的哈希尋址是忽略容量以上的高位的,那么這種處理就可以有效避免類似情況下的哈希碰撞。

    在JDK1.8中取消了indefFor()方法,直接用(tab.length-1)&hash,所以看到這個(gè),代表的就是數(shù)組的下角標(biāo)。

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

    2.3.5 JDK8中的HashMap的王牌功能:問題1:HashMap為什么要樹化?

    問題:JDK8中的HashMap的王牌功能:為什么HashMap為什么要樹化?
    回答:一句話概括:制造哈希碰撞從而造成DOS攻擊,樹化后優(yōu)化哈希碰撞產(chǎn)生后的存取。
    解釋:其實(shí)這本質(zhì)上一個(gè)安全問題。因?yàn)樵谠胤胖眠^程中,如果一個(gè)對(duì)象哈希沖突,都被放置到同一個(gè)桶里,則會(huì)形成一個(gè)鏈表,我們知道鏈表查詢是線性的,會(huì)嚴(yán)重影響存取的性能。而在現(xiàn)實(shí)世界,構(gòu)造哈希沖突的數(shù)據(jù)并不是非常復(fù)雜的事情,惡意代碼就可以利用這些數(shù)據(jù)大量與服務(wù)器端交互,導(dǎo)致服務(wù)器端CPU大量占用,這就構(gòu)成了哈希碰撞拒絕服務(wù)攻擊,國(guó)內(nèi)一線互聯(lián)網(wǎng)公司就發(fā)生過類似攻擊事件。
    哈希碰撞攻擊:用哈希碰撞發(fā)起拒絕服務(wù)攻擊(DOS,Denial-Of-Service attack),常見的場(chǎng)景是攻擊者可以事先構(gòu)造大量相同哈希值的數(shù)據(jù)(制造哈希碰撞從而造成DOS攻擊,樹化后優(yōu)化哈希碰撞產(chǎn)生后的存取),然后以JSON數(shù)據(jù)的形式發(fā)送給服務(wù)器,服務(wù)器端在將其構(gòu)建成為Java對(duì)象過程中,通常以HashTable或HashMap等形式存儲(chǔ),哈希碰撞將導(dǎo)致哈希表發(fā)生嚴(yán)重退化,算法復(fù)雜度可能上升一個(gè)數(shù)據(jù)級(jí),進(jìn)而耗費(fèi)大量CPU資源。

    2.3.6 JDK8中的HashMap的王牌功能:問題2:鏈表樹化的兩個(gè)條件?/為什么要將鏈表中轉(zhuǎn)紅黑樹的閾值設(shè)為8?

    我們可以這么來看,當(dāng)鏈表長(zhǎng)度大于或等于閾值(默認(rèn)為 8)的時(shí)候,如果同時(shí)還滿足容量大于或等于 MIN_TREEIFY_CAPACITY(默認(rèn)為 64)的要求,就會(huì)把鏈表轉(zhuǎn)換為紅黑樹。同樣,后續(xù)如果由于刪除或者其他原因調(diào)整了大小,當(dāng)紅黑樹的節(jié)點(diǎn)小于或等于 6 個(gè)以后,又會(huì)恢復(fù)為鏈表形態(tài)。

    每次遍歷一個(gè)鏈表,平均查找的時(shí)間復(fù)雜度是 O(n),n 是鏈表的長(zhǎng)度。紅黑樹有和鏈表不一樣的查找性能,由于紅黑樹有自平衡的特點(diǎn),可以防止不平衡情況的發(fā)生,所以可以始終將查找的時(shí)間復(fù)雜度控制在 O(log(n))。最初鏈表還不是很長(zhǎng),所以可能 O(n) 和 O(log(n)) 的區(qū)別不大,但是如果鏈表越來越長(zhǎng),那么這種區(qū)別便會(huì)有所體現(xiàn)。所以為了提升查找性能,需要把鏈表轉(zhuǎn)化為紅黑樹的形式。

    還要注意很重要的一點(diǎn),單個(gè) TreeNode 需要占用的空間大約是普通 Node 的兩倍,所以只有當(dāng)包含足夠多的 Nodes 時(shí)才會(huì)轉(zhuǎn)成 TreeNodes,而是否足夠多就是由 TREEIFY_THRESHOLD 的值決定的。而當(dāng)桶中節(jié)點(diǎn)數(shù)由于移除或者 resize 變少后,又會(huì)變回普通的鏈表的形式,以便節(jié)省空間。

    默認(rèn)是鏈表長(zhǎng)度達(dá)到 8 就轉(zhuǎn)成紅黑樹,而當(dāng)長(zhǎng)度降到 6 就轉(zhuǎn)換回去,這體現(xiàn)了時(shí)間和空間平衡的思想,最開始使用鏈表的時(shí)候,空間占用是比較少的,而且由于鏈表短,所以查詢時(shí)間也沒有太大的問題。可是當(dāng)鏈表越來越長(zhǎng),需要用紅黑樹的形式來保證查詢的效率。

    在理想情況下,鏈表長(zhǎng)度符合泊松分布,各個(gè)長(zhǎng)度的命中概率依次遞減,當(dāng)長(zhǎng)度為 8 的時(shí)候,是最理想的值。

    事實(shí)上,鏈表長(zhǎng)度超過 8 就轉(zhuǎn)為紅黑樹的設(shè)計(jì),更多的是為了防止用戶自己實(shí)現(xiàn)了不好的哈希算法時(shí)導(dǎo)致鏈表過長(zhǎng),從而導(dǎo)致查詢效率低,而此時(shí)轉(zhuǎn)為紅黑樹更多的是一種保底策略,用來保證極端情況下查詢的效率。

    通常如果 hash 算法正常的話,那么鏈表的長(zhǎng)度也不會(huì)很長(zhǎng),那么紅黑樹也不會(huì)帶來明顯的查詢時(shí)間上的優(yōu)勢(shì),反而會(huì)增加空間負(fù)擔(dān)。所以通常情況下,并沒有必要轉(zhuǎn)為紅黑樹,所以就選擇了概率非常小,小于千萬分之一概率,也就是長(zhǎng)度為 8 的概率,把長(zhǎng)度 8 作為轉(zhuǎn)化的默認(rèn)閾值。

    鏈表樹化的兩個(gè)條件:當(dāng)鏈表長(zhǎng)度大于或等于閾值(默認(rèn)為 8)的時(shí)候,并且同時(shí)還滿足容量大于或等于 MIN_TREEIFY_CAPACITY(默認(rèn)為 64)的要求

    處理哈希沖突兩個(gè)方法:一個(gè)好的哈希算法、鏈表樹化(前者才是根本,后者只是網(wǎng)絡(luò)安全制造哈希

    問題:JDK8中的HashMap的王牌功能:鏈表樹化的兩個(gè)條件?/為什么要將鏈表中轉(zhuǎn)紅黑樹的閾值設(shè)為8?
    回答:碰撞從而造成DOS攻擊和不合理哈希算法的處理),所以設(shè)計(jì)為8。通常如果 hash 算法正常的話,那么鏈表的長(zhǎng)度也不會(huì)很長(zhǎng),那么紅黑樹也不會(huì)帶來明顯的查詢時(shí)間上的優(yōu)勢(shì),反而會(huì)增加空間負(fù)擔(dān)。所以通常情況下,并沒有必要轉(zhuǎn)為紅黑樹,所以就選擇了概率非常小,小于千萬分之一概率,也就是長(zhǎng)度為 8 的概率,把長(zhǎng)度 8 作為轉(zhuǎn)化的默認(rèn)閾值。
    值得注意的是,實(shí)際開發(fā)中,發(fā)現(xiàn) HashMap 內(nèi)部出現(xiàn)了紅黑樹的結(jié)構(gòu),那可能是我們的哈希算法出了問題,所以需要選用合適的hashCode方法,以便減少?zèng)_突。

    問題:為什么在JDK1.8中進(jìn)行對(duì)HashMap優(yōu)化的時(shí)候,把鏈表轉(zhuǎn)化為紅黑樹的閾值是8,而不是7或者不是20呢?
    標(biāo)準(zhǔn)回答:

  • 第一,避免頻繁轉(zhuǎn)換,樹化鏈表化轉(zhuǎn)換成本與二叉樹優(yōu)化查詢性能之間的平衡:如果選擇6和8(如果鏈表小于等于6樹還原轉(zhuǎn)為鏈表,大于等于8轉(zhuǎn)為樹),中間有個(gè)差值7可以有效防止鏈表和樹頻繁轉(zhuǎn)換。假設(shè)一下,如果設(shè)計(jì)成鏈表個(gè)數(shù)超過8則鏈表轉(zhuǎn)換成樹結(jié)構(gòu),鏈表個(gè)數(shù)小于8則樹結(jié)構(gòu)轉(zhuǎn)換成鏈表,如果一個(gè)HashMap不停的插入、刪除元素,鏈表個(gè)數(shù)在8左右徘徊,就會(huì)頻繁的發(fā)生樹轉(zhuǎn)鏈表、鏈表轉(zhuǎn)樹,效率會(huì)很低。

  • 第二,數(shù)學(xué)證明,泊松分布:還有一點(diǎn)重要的就是由于treenodes的大小大約是常規(guī)節(jié)點(diǎn)的兩倍,因此我們僅在容器包含足夠的節(jié)點(diǎn)以保證使用時(shí)才使用它們,當(dāng)它們變得太小(由于移除或調(diào)整大小)時(shí),它們會(huì)被轉(zhuǎn)換回普通的node節(jié)點(diǎn),容器中節(jié)點(diǎn)分布在hash桶中的頻率遵循泊松分布,桶的長(zhǎng)度超過8的概率非常非常小。所以作者應(yīng)該是根據(jù)概率統(tǒng)計(jì)而選擇了8作為閥值

  • 第三,統(tǒng)計(jì)學(xué)問題:一個(gè)統(tǒng)計(jì)的問題,java設(shè)計(jì)一定使用數(shù)學(xué)方法和統(tǒng)計(jì)方法,知道得到這個(gè)8,就像豐巢快遞為什么是12小時(shí)而不是24小時(shí)一樣。

  • 2.4 HashMap在1.7和1.8之間四個(gè)不同(對(duì)比方式,重要)

    我們可以簡(jiǎn)單列下HashMap在1.7和1.8之間的變化,四點(diǎn)變化(除了底層結(jié)構(gòu),都要從源碼層面解釋):

    第一,底層數(shù)據(jù)結(jié)構(gòu)不同

    1.7中采用數(shù)組+鏈表,1.8采用的是數(shù)組+鏈表/紅黑樹,即在1.7中鏈表長(zhǎng)度超過一定長(zhǎng)度后就改成紅黑樹存儲(chǔ)。

    第二,index:擴(kuò)容后index的計(jì)算

    1.7擴(kuò)容時(shí)需要重新計(jì)算哈希值hash,根據(jù)hash計(jì)算索引位置index,equals比較鏈表上的元素。

    1.8并不重新計(jì)算哈希值hash,巧妙地采用和擴(kuò)容后容量進(jìn)行&操作來計(jì)算新的索引位置index,即index = hash & (tab.length - 1) 。

    第三,插入:哈希沖突的時(shí)候插入的值*

    1.7是采用表頭插入法插入鏈表,1.8采用的是尾部插入法。

    在1.7中采用表頭插入法,
    缺點(diǎn):因?yàn)轭^插法,在擴(kuò)容時(shí)會(huì)改變鏈表中元素原本的順序,以至于在并發(fā)場(chǎng)景下導(dǎo)致鏈表成環(huán)的問題;
    優(yōu)點(diǎn):JDK7考慮剛剛插入的值是熱乎的,所以放在表頭;

    在1.8中采用尾部插入法,
    優(yōu)點(diǎn):因?yàn)槲膊宸?#xff0c;所以在擴(kuò)容時(shí)會(huì)保持鏈表元素原本的順序,就不會(huì)出現(xiàn)鏈表成環(huán)的問題了;
    缺點(diǎn):因?yàn)槲膊宸?#xff0c;所有剛剛插入的節(jié)點(diǎn)放在最后面了,要找很麻煩,所以引入鏈表樹化,超過8個(gè)節(jié)點(diǎn)就可以樹化,找新插入的節(jié)點(diǎn)只要O(lgN)。

    第四,擴(kuò)容的時(shí)機(jī):擴(kuò)容與插入的先后順序

    1.7中是先擴(kuò)容后插入新值的,1.8中是先插值再擴(kuò)容

    問題:為什么在JDK1.7的時(shí)候是先進(jìn)行擴(kuò)容后進(jìn)行插入,而在JDK1.8的時(shí)候則是先插入后進(jìn)行擴(kuò)容的呢?
    答案:代碼就是這樣寫的,JDK8插入的時(shí)候使用了樹化,所以將數(shù)組擴(kuò)容放到了后面。

  • 對(duì)于JDK1.8
    在JDK1.7中的話,是先進(jìn)行插入新值然后進(jìn)行擴(kuò)容操作的,主要是因?yàn)閷?duì)鏈表轉(zhuǎn)為紅黑樹進(jìn)行的優(yōu)化,因?yàn)槟悴迦脒@個(gè)節(jié)點(diǎn)的時(shí)候有可能是普通鏈表節(jié)點(diǎn),也有可能是紅黑樹節(jié)點(diǎn),所以導(dǎo)致先插入后擴(kuò)容,擴(kuò)容判斷與resize()函數(shù)調(diào)用如下:

  • //其實(shí)就是當(dāng)這個(gè)Map中實(shí)際插入的鍵值對(duì)的值的大小如果大于這個(gè)默認(rèn)的閾值的時(shí)候(初始是16*0.75=12)的時(shí)候才會(huì)觸發(fā)擴(kuò)容,//這個(gè)是在JDK1.8中的先插入后擴(kuò)容if (++size > threshold)????????????resize();
  • 對(duì)于JDK1.7
    在JDK1.7中的話,是先進(jìn)行擴(kuò)容操作然后進(jìn)行插入新值的,就是當(dāng)你發(fā)現(xiàn)你插入的桶是不是為空:
    (1)如果不為空說明存在值,當(dāng)前插入會(huì)發(fā)生哈希沖突,那么就必須得擴(kuò)容;
    (2)如果為空說明不存在值,當(dāng)前插入不會(huì)發(fā)生哈希沖突,那么本次插入不需要擴(kuò)容,那就等到下一次發(fā)生Hash沖突的時(shí)候在進(jìn)行擴(kuò)容,但是當(dāng)如果以后都沒有發(fā)生hash沖突產(chǎn)生,那么就不會(huì)進(jìn)行擴(kuò)容了,減少了一次無用擴(kuò)容,也減少了內(nèi)存的使用。
    先擴(kuò)容后插入代碼邏輯如下:

  • void addEntry(int hash, K key, V value, int bucketIndex) { //這里當(dāng)錢數(shù)組如果大于等于12(假如)閾值的話,并且當(dāng)前的數(shù)組的Entry數(shù)組還不能為空的時(shí)候就擴(kuò)容   if ((size >= threshold) && (null != table[bucketIndex])) {       //擴(kuò)容數(shù)組,比較耗時(shí)    resize(2 * table.length);   hash = (null != key) ? hash(key) : 0;   bucketIndex = indexFor(hash, table.length);   }   createEntry(hash, key, value, bucketIndex);  }?void?createEntry(int?hash,?K?key,?V?value,?int?bucketIndex)?{   Entry e = table[bucketIndex];    //把新加的放在原先在的前面,原先的是e,現(xiàn)在的是new,next指向e    table[bucketIndex] = new Entry<>(hash, key, value, e);//假設(shè)現(xiàn)在是new   size++;  }
  • 三、其他:HashTable、TreeMap、ConcurrentHashMap

    3.1 HashTable

    3.2 TreeMap

    TreeMap需要注意的三點(diǎn):

  • key-value是否為null,和HashTable一樣:
    與HashMap不同的是,TreeMap鍵、值都不能為null;

  • 紅黑樹是排序二叉樹:
    TreeMap自定義排序器,底層如何實(shí)現(xiàn)排序:樹中的每個(gè)節(jié)點(diǎn)的值都會(huì)大于或等于它的左子樹中的所有節(jié)點(diǎn)的值,并且小于或等于它的右子樹中的所有節(jié)點(diǎn)的值;

  • 紅黑樹是平衡二叉樹-時(shí)間復(fù)雜度:
    與HashMap不同的是它的get、put、remove之類操作都是O(log(n))的時(shí)間復(fù)雜度。

  • 對(duì)TreeMap做成如下小結(jié):
    TreeMap是基于紅黑樹的一種提供順序訪問的Map,與HashMap不同的是它的get、put、remove之類操作都是o(log(n))的時(shí)間復(fù)雜度,具體順序可以由指定的Comparator來決定,或者根據(jù)鍵的自然順序來判斷。

    3.3 ConcurrentHashMap(核心:鎖機(jī)制)

    Java5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的擴(kuò)展性更好,ConcurrentHashMap底層采用分段的數(shù)組+鏈表實(shí)現(xiàn),是線程安全。

    先看一下ConcurrentHashMap的類圖:

    由上面類圖,左邊是HashMap,右邊是ConcurrentHashMap,它都是繼承自AbstractMap抽象類,但是,在存儲(chǔ)結(jié)構(gòu)中,ConcurrentHashMap比HashMap多出了一個(gè)類Segment,而Segment就是一個(gè)可重入鎖,這個(gè)Segment就是ConcurrentHashMap實(shí)現(xiàn)分段鎖的關(guān)鍵,ConcurrentHashMap也正是使用這種鎖分段技術(shù)來保證線程安全的。

    鎖分段技術(shù)定義:首先將數(shù)據(jù)分成一段一段的存儲(chǔ),然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個(gè)線程占用鎖訪問其中一個(gè)段數(shù)據(jù)的時(shí)候,其他段的數(shù)據(jù)也能被其他線程訪問。

    ConcurrentHashMap與HashTable的不同:

  • HashTable中采用的鎖機(jī)制是一次鎖住整個(gè)hash表,從而在同一時(shí)刻只能由一個(gè)線程對(duì)其進(jìn)行操作;而ConcurrentHashMap中則是一次鎖住一個(gè)桶。

  • ConcurrentHashMap默認(rèn)將hash表分為16個(gè)桶,諸如get、put、remove等常用操作只鎖住當(dāng)前需要用到的桶。這樣,原來只能一個(gè)線程進(jìn)入,現(xiàn)在卻能同時(shí)有16個(gè)寫線程執(zhí)行,并發(fā)性能的提升是顯而易見的。

  • 問題:ConcurrentHashMap是如何實(shí)現(xiàn)鎖機(jī)制的?
    回答:

  • 實(shí)現(xiàn)上,Segment內(nèi)部類實(shí)現(xiàn)分段:
    ConcurrentHashMap具有一個(gè)內(nèi)部類Segment,正是因?yàn)閮?nèi)部類Segment,將數(shù)據(jù)分成一段一段的存儲(chǔ),然后給每一段數(shù)據(jù)配一把鎖,當(dāng)一個(gè)線程占用鎖訪問其中一個(gè)段數(shù)據(jù)的時(shí)候,其他段的數(shù)據(jù)也能被其他線程訪問;

  • 實(shí)現(xiàn)上,ConcurrentHashMap分段鎖就是Lock鎖:
    Segement extends ReentrantLock implements Serializable,所以說ConcurrentHashMap中的分段鎖就是一種普通的Lock鎖;

  • 方法上,段內(nèi)方法,分段操作,性能提高16倍,get() put() remove():
    ConcurrentHashMap默認(rèn)將hash表分為16個(gè)桶,諸如get、put、remove等常用操作只鎖住當(dāng)前需要用到的桶。ConcurrentHashMap是HashTable的替代,HashTable中采用的鎖機(jī)制是一次鎖住整個(gè)hash表,從而在同一時(shí)刻只能由一個(gè)線程對(duì)其進(jìn)行操作,而ConcurrentHashMap中則是一次鎖住一個(gè)桶;

  • 方法上,擴(kuò)容方法,分段鎖實(shí)現(xiàn)段內(nèi)擴(kuò)容resize():
    段內(nèi)數(shù)組擴(kuò)容(段內(nèi)元素超過該段對(duì)應(yīng)Entry數(shù)組長(zhǎng)度的75%觸發(fā)擴(kuò)容,不會(huì)對(duì)整個(gè)Map進(jìn)行擴(kuò)容),插入前檢測(cè)需不需要擴(kuò)容,有效避免無效擴(kuò)容;

  • 方法上,跨段方法,跨段方法size()和containsValue():
    有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個(gè)表而而不僅僅是某個(gè)段,這需要按順序鎖定所有段,操作完畢后,又按順序釋放所有段的鎖;

  • 讀不加鎖、寫加鎖:
    讀操作不加鎖,由于HashEntry的value變量是 volatile的,也能保證讀取到最新的值。

  • 3.4 HashTable/ConcurrentHashMap、HashMap、TreeMap

  • 存儲(chǔ)內(nèi)容為key-value鍵值對(duì):
    存儲(chǔ)的內(nèi)容是基于key-value的鍵值對(duì)映射,不能有重復(fù)的key,而且一個(gè)key只能映射一個(gè)value。HashSet底層就是基于HashMap實(shí)現(xiàn)的。

  • key、value是否為null:
    HashTable的key、value都不能為null;TreeMap鍵、值都不能為null;HashMap的key、value可以為null,不過只能有一個(gè)key為null,但可以有多個(gè)null的value;

  • HashTable、HashMap具有無序特性,TreeMap默認(rèn)升序,可以自定義排序方式:
    HashTable、HashMap具有無序特性,TreeMap是利用紅黑樹實(shí)現(xiàn)的(金手指:TreeMap自定義排序器,底層如何實(shí)現(xiàn)排序:樹中的每個(gè)節(jié)點(diǎn)的值都會(huì)大于或等于它的左子樹中的所有節(jié)點(diǎn)的值,并且小于或等于它的右子樹中的所有節(jié)點(diǎn)的值),實(shí)現(xiàn)了SortMap接口,能夠?qū)Ρ4娴挠涗浉鶕?jù)鍵進(jìn)行排序。所以一般需求排序的情況下首選TreeMap,默認(rèn)按鍵的升序排序(深度優(yōu)先搜索),也可以自定義實(shí)現(xiàn)Comparator接口實(shí)現(xiàn)排序方式。

  • 選用原則:一般用HashMap,需要排序使用TreeMap,需要保證線程安全使用ConcurrentHashMap
    一般情況下選用HashMap,因?yàn)镠ashMap的鍵值對(duì)在取出時(shí)是隨機(jī)的,其依據(jù)鍵的hashCode和鍵的equals方法存取數(shù)據(jù),具有很快的訪問速度,所以在Map中插入、刪除及索引元素時(shí)其是效率最高的實(shí)現(xiàn)。其他的,TreeMap的鍵值對(duì)在取出時(shí)是排過序的,效率會(huì)低一點(diǎn),而HashTable/ConcurrentHashMap是線程安全的,效率也低一點(diǎn)。

  • 四、小結(jié)

    HashMap全解析,完成了。

    天天打碼,天天進(jìn)步!!!

    寫留言

    總結(jié)

    以上是生活随笔為你收集整理的hashmap value占用空间大小_【Java集合框架002】原理层面:HashMap全解析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。

    日韩高清免费观看 | 亚洲欧美国产精品久久久久 | 久久国产精品系列 | 黄色的网站在线 | 51久久夜色精品国产麻豆 | 欧美色图88 | 91亚洲精品久久久中文字幕 | 亚洲精品字幕在线 | 色综合天天视频在线观看 | 91人人干| 在线观看成人福利 | 在线看小早川怜子av | 日韩久久精品一区二区 | 69成人在线| 国内精品国产三级国产aⅴ久 | 日韩av有码在线 | 久久婷婷一区二区三区 | 成人黄色小说视频 | 四虎免费av | 久久久天天操 | 一区二区丝袜 | 国产日韩欧美网站 | 日韩激情在线 | 中文在线免费一区三区 | av直接看 | 国产九九九精品视频 | 日本午夜在线观看 | av中文字幕第一页 | 97电影在线看视频 | 日韩av在线不卡 | 玖玖视频 | 精品日韩视频 | 中文字幕日本电影 | 在线免费观看黄色小说 | 五月婷色 | 国产又粗又猛又色又黄网站 | 在线观av | 美女国产精品 | 81精品国产乱码久久久久久 | 日韩精品免费一区二区三区 | 色婷婷色| 国产在线观看地址 | 狠狠色噜噜狠狠 | 日韩精品中文字幕在线不卡尤物 | 激情av网址| 91九色视频导航 | 国产午夜在线 | 九九九免费视频 | 久草视频在线免费 | 久久免费一级片 | 欧美午夜激情网 | 九九视频在线 | 免费福利片 | 视频在线日韩 | 亚洲人天堂 | 欧美午夜视频在线 | 99精品久久久久久久久久综合 | 成人9ⅰ免费影视网站 | 日韩精品2区 | 久久久精品一区二区 | 亚洲aⅴ乱码精品成人区 | 亚洲欧美日韩中文在线 | 日本mv大片欧洲mv大片 | 欧美一区二区三区在线播放 | 热久久免费国产视频 | 成人在线观看资源 | 色综合久久88色综合天天6 | 亚洲精品理论片 | 婷婷亚洲综合 | 97超碰人人看 | 波多野结衣在线观看视频 | 91在线影院 | 波多野结衣电影一区二区三区 | 97韩国电影 | 欧美日韩亚洲精品在线 | 色爱区综合激月婷婷 | 在线中文视频 | 国产在线不卡 | 九九久久电影 | 天天干天天天天 | 免费观看91视频大全 | 免费在线观看毛片网站 | av免费网站观看 | 久久久久国产一区二区三区四区 | 高清不卡毛片 | a午夜在线| 免费一级特黄录像 | 国产一卡久久电影永久 | 久久艹国产视频 | 激情久久五月 | 亚洲国产精品久久久久婷婷884 | 国产丝袜制服在线 | 欧美日韩视频精品 | 在线免费观看国产视频 | 波多野结衣一区二区三区中文字幕 | 亚洲播播| 色爽网站 | 国产在线观看xxx | 国产精品成人一区二区 | 久久蜜臀一区二区三区av | 91精品推荐| 国产高清一级 | 久久网站免费 | 国产日韩在线一区 | 欧洲精品在线视频 | 亚洲综合激情小说 | 成人动图| 亚洲免费av在线 | 亚洲欧美日韩国产 | 欧洲一区二区在线观看 | 成人av一二三区 | av福利第一导航 | 草久久久久久 | 日韩草比 | 欧美日韩一级视频 | 日韩av影片在线观看 | 日日躁你夜夜躁你av蜜 | 日韩av电影中文字幕 | 婷婷在线免费观看 | 色在线免费 | 激情在线免费视频 | av免费高清观看 | 91视视频在线直接观看在线看网页在线看 | 国产三级精品三级在线观看 | 成人全视频免费观看在线看 | 99中文视频在线 | 狠狠色丁香婷婷综合久小说久 | 成人性生交大片免费看中文网站 | 国产在线超碰 | 国产精品av在线免费观看 | 91在线小视频 | 中文字幕 二区 | 国产成人精品电影久久久 | 午夜精品一区二区三区在线 | 中文字幕一区二区三区视频 | 欧美日韩国产亚洲乱码字幕 | 91亚洲精品视频 | 日本黄网站 | 日韩字幕 | 欧美性爽爽 | a天堂在线看 | 正在播放久久 | 99久久激情视频 | 久久夜夜夜 | 欧美日韩精品区 | 最新国产在线 | 亚洲一级国产 | 婷婷丁香色 | 综合色久| 99精品国产99久久久久久97 | 亚洲视频在线看 | 国产精品久久久久免费 | 亚洲国产日韩欧美在线 | 午夜天使| 亚洲精品一区二区网址 | 91久久久久久久一区二区 | 最新国产在线 | 黄色性av | 激情综合网色播五月 | 国产特级毛片aaaaaa高清 | 在线观看成人av | 成人精品久久 | 午夜久久久久 | 99精品在线视频播放 | 三级黄色网址 | 超碰个人在线 | 中文字幕电影网 | 中文字幕日韩在线播放 | 亚洲黄色免费网站 | 国产三级午夜理伦三级 | 久久99精品国产一区二区三区 | 国产精品v欧美精品 | 国产精品一区二区 91 | 国产精品久久久久久久久软件 | 成人国产精品一区 | 黄色免费国产 | 久久视频这里有精品 | 欧美午夜性 | 操高跟美女 | 久久av电影 | 亚洲一区精品人人爽人人躁 | 综合久久久久久久 | 天天激情综合 | 在线观看aa | 亚洲成人黄色网址 | 又湿又紧又大又爽a视频国产 | 99国产在线 | 午夜少妇一区二区三区 | 亚洲精品欧美专区 | 欧美日韩亚洲第一 | 亚州天堂 | www91在线观看| 国产精品理论片在线播放 | 中文字幕av网站 | 天天看天天操 | 中文字幕免费观看视频 | 亚洲欧洲精品一区二区精品久久久 | 国产一区高清在线观看 | 午夜黄色影院 | 欧美日韩精品影院 | 国产高清网站 | 97色噜噜| 婷婷五综合 | 91麻豆看国产在线紧急地址 | 91成人免费视频 | 色婷婷久久 | 91桃色在线观看视频 | av免费看看 | 97碰碰精品嫩模在线播放 | 久久免费黄色大片 | 国产在线一线 | 日日摸日日添日日躁av | 免费碰碰| 国产精品一区二区久久精品 | 久久这里有 | 国内精品国产三级国产aⅴ久 | 精品国产一区二区三区男人吃奶 | 91日韩在线专区 | 少妇高潮流白浆在线观看 | 色就干| 国产无遮挡又黄又爽馒头漫画 | 久久久一本精品99久久精品66 | 国内精品久久久久久久久久久 | 天天曰天天射 | 天天色视频 | 国产96精品| 麻花豆传媒mv在线观看网站 | 亚洲黄色软件 | 天天操天天射天天 | 亚洲五月综合 | 日韩精品中文字幕在线不卡尤物 | 伊人永久| 草在线视频 | 99精品偷拍视频一区二区三区 | 人人玩人人添人人 | 中国一级片视频 | 久草9视频 | 亚洲欧洲成人精品av97 | av丁香花 | 激情五月激情综合网 | 黄色中文字幕 | 日本久久久久久久久 | 日本久久综合视频 | 日本爱爱片 | 国产一及片| 日韩电影在线看 | 国产99久久九九精品 | 国内亚洲精品 | 欧美日韩免费看 | 欧美一级片在线免费观看 | 亚洲精品午夜视频 | 精品在线观看一区二区 | 亚洲视频国产 | 国产成人综合在线观看 | 一本一本久久a久久精品综合小说 | 久久精品视频一 | 四虎免费av| 激情五月在线视频 | 精品国产1区2区3区 国产欧美精品在线观看 | 色99久久| 一区二区视频免费在线观看 | 国产无限资源在线观看 | 四虎在线免费观看 | www成人av| 狠狠操天天射 | 亚洲中字幕 | 久久九九国产精品 | 中文字幕乱在线伦视频中文字幕乱码在线 | 久久爽久久爽久久av东京爽 | 国产精品完整版 | 91视频久久 | 国产高清在线不卡 | 久久视频这里只有精品 | 毛片美女网站 | 91成人精品观看 | 日韩mv欧美mv国产精品 | 丁香婷五月 | 国产午夜精品免费一区二区三区视频 | 欧美午夜精品久久久久久孕妇 | 黄免费网站 | 成人av中文字幕 | 国产中文字幕视频在线观看 | 99热在| 中文字幕在线视频一区二区 | 中文字幕一区在线观看视频 | 日韩精品久久久久久久电影竹菊 | 手机在线看永久av片免费 | 久久这里只有精品首页 | 国产专区视频 | 精品免费视频 | 欧美日韩在线精品 | 午夜123| 久久免费美女视频 | 成人日韩av| 久久在线看 | 在线色资源| 午夜.dj高清免费观看视频 | 不卡av电影在线 | 免费国产ww | 丁香视频全集免费观看 | 亚洲一级电影视频 | 日日草夜夜操 | www操操操| 中文字幕一区在线观看视频 | 免费黄色特级片 | 91精品人成在线观看 | 九九热av| 欧美日韩在线免费观看视频 | 久久精彩免费视频 | 日本黄色免费在线观看 | 视频一区视频二区在线观看 | 亚洲高清久久久 | 亚洲国产精品成人va在线观看 | 亚洲在线视频网站 | 久久久久9999亚洲精品 | 激情久久影院 | 黄色资源网站 | 免费91麻豆精品国产自产在线观看 | 天堂久久电影网 | 午夜三级影院 | a视频在线观看免费 | 国产美女搞久久 | 国产资源在线播放 | 日韩在线视频国产 | 日韩精品网址 | 中文字幕中文 | 中文字幕婷婷 | 国产一区二区精品 | www毛片com| 色偷偷男人的天堂av | 久久综合九色欧美综合狠狠 | 欧美一级片免费观看 | 91在线视频导航 | 中文字幕在线观看不卡 | 最新国产一区二区三区 | 亚洲激情六月 | 国产又粗又硬又爽视频 | 欧美在线aaa | 欧美在线观看视频一区二区三区 | 激情综合色播五月 | 中文av网 | 欧美精品一区二区免费 | 色干干 | 337p西西人体大胆瓣开下部 | 五月婷婷毛片 | 亚洲成人精品影院 | 日韩欧美精选 | 91一区啪爱嗯打偷拍欧美 | 免费精品视频 | 97色婷婷 | 91精品久久久久久综合乱菊 | 午夜精品福利一区二区三区蜜桃 | 国产精品v欧美精品v日韩 | 91精品国产99久久久久久红楼 | 一区二区三区视频网站 | 国产中文| 久久久久久久久久久久影院 | 最近免费中文字幕 | 韩日精品在线观看 | 久久免费视频网 | 欧美大片大全 | 美女视频黄在线 | 欧美精品免费在线观看 | 色黄久久久久久 | 91成品人影院 | 欧美 日韩 久久 | 日本黄色一级电影 | 夜又临在线观看 | 91亚洲综合 | 中文字幕中文中文字幕 | 97人人模人人爽人人喊网 | 久草在线免费播放 | 国产在线不卡精品 | 欧美一级视频在线观看 | 亚洲码国产日韩欧美高潮在线播放 | 久久久影院一区二区三区 | 欧美视频在线观看免费网址 | 最近高清中文在线字幕在线观看 | 黄色99视频 | 99久久精品免费看国产麻豆 | 伊人午夜视频 | 亚洲成人精品 | 日韩在线观看的 | 久久国产电影院 | 免费日韩电影 | 成人禁用看黄a在线 | 91精品国产电影 | 日韩成人免费在线电影 | 久草| 在线观看不卡视频 | av免费在线观看1 | 青青河边草免费直播 | 免费看黄在线看 | 日韩精品久久久久久中文字幕8 | 欧美俄罗斯性视频 | 五月天色丁香 | 在线观看亚洲精品 | 日女人电影 | 精品久久久久久久 | 国语麻豆 | 国产亚洲精品综合一区91 | 亚洲一区二区视频 | 四虎www.| 免费国产一区二区视频 | 色在线免费观看 | 五月婷婷丁香六月 | 午夜少妇 | 玖玖爱免费视频 | 高清国产午夜精品久久久久久 | 欧美日韩中文国产 | 狠狠干狠狠操 | 久久精品观看 | 久久久影院官网 | 日韩在线中文字幕 | 毛片网站观看 | 欧美性色19p | 国内精品久久久久久久影视简单 | 91色国产在线 | 国产亚洲资源 | 国产69久久 | 91黄视频在线观看 | 日韩sese| 91免费在线| 成人免费网视频 | 久久久久二区 | 天天干天天操天天拍 | 国产精品欧美久久久久天天影视 | 亚洲伦理一区二区 | 久久爱导航 | 久久免费黄色网址 | 精品国精品自拍自在线 | www..com毛片 | 久草在线免费看视频 | 97在线免费观看 | av大全在线| 国产午夜一级毛片 | 99精品视频网 | 婷婷色综| 美女又爽又黄 | 国产成人免费观看 | 欧美 高跟鞋交 xxxxhd | 日本三级在线观看中文字 | a视频在线观看免费 | 亚洲理论影院 | 99精品久久久久久久久久综合 | 网站在线观看你们懂的 | 在线免费av网站 | 日韩精品第一区 | 成人97视频 | 成人免费网站视频 | 九九免费在线观看 | 911久久香蕉国产线看观看 | 免费碰碰 | 日日弄天天弄美女bbbb | 免费在线观看日韩视频 | 国产午夜精品av一区二区 | 欧美一级片免费在线观看 | 国产九色91| 91视频免费视频 | 天天色天天操天天爽 | 色香网| 中文字幕免费观看视频 | 91黄色小网站 | 亚洲国产精品电影在线观看 | 最新国产中文字幕 | 亚洲欧美精品一区二区 | av免费网| 久久久久久免费视频 | 超碰97中文| 色干干| 99久久er热在这里只有精品66 | 中文字幕区 | 98涩涩国产露脸精品国产网 | 久久免费视频2 | 成人高清av在线 | 一级黄色免费网站 | 波多野结衣精品视频 | 国产一及片 | 亚洲综合涩 | 日韩免费网址 | 成人在线视频免费观看 | 亚洲伦理精品 | 亚洲一级电影视频 | 一级大片在线观看 | 久久99精品热在线观看 | 国产精品久久久久久欧美 | 中文字幕精品一区二区精品 | 久久精品久久综合 | 8x成人免费视频 | 国产精品18久久久久久久网站 | 五月激情av | 久久99欧美 | 久草资源在线 | 婷婷在线免费 | 在线观av | 久草视频播放 | 天天色天天艹 | 国产精品高清免费在线观看 | 香蕉在线影院 | 久久久免费播放 | 九色琪琪久久综合网天天 | 97精品国产一二三产区 | 超级av在线 | 国产精品九九九九九 | 91大神电影 | 欧美日韩国产区 | 五月婷婷综合激情网 | 国产精品一区免费观看 | 黄在线免费观看 | 五月婷婷中文网 | 中文字幕在线看视频国产中文版 | 国产视频2021 | 国产福利在线不卡 | 最近日本韩国中文字幕 | 久产久精国产品 | 99超碰在线观看 | 午夜国产在线观看 | www.黄色 | 在线天堂视频 | 中文字幕在线播放日韩 | 国产色婷婷精品综合在线手机播放 | 日日夜夜骑 | 天天射网站| 不卡国产视频 | 国产精品久久久久久久免费大片 | 国产精品二区在线观看 | 99精品欧美一区二区三区 | 狠狠色丁香久久综合网 | 国产麻豆精品一区二区 | 日韩精品在线看 | 天天摸夜夜添 | 成人欧美一区二区三区黑人麻豆 | 黄色免费观看视频 | 免费视频成人 | 男女男视频 | 视频一区二区在线观看 | 人成午夜视频 | 免费视频xnxx com | 在线看片成人 | 久久伦理电影 | 国产一级免费片 | 久久免费成人网 | 久久成人视屏 | 美女久久久久久久久久 | 国产高清成人 | 永久免费的av电影 | 国产在线美女 | 久久久久免费精品 | 成人免费一级片 | 一区二区视频在线看 | 久草在线观 | 国产精品黑丝在线观看 | 国产69久久精品成人看 | 99视频精品视频高清免费 | 中文字幕在线看 | 深爱激情婷婷网 | 天天色天天艹 | 国产福利中文字幕 | 成人av播放 | 美女福利视频网 | 日韩网站在线观看 | 一本一本久久aa综合精品 | 97色涩| a在线v| 国产色拍拍拍拍在线精品 | 国内精品久久久久久久久久 | 美女视频黄色免费 | 午夜色影院 | 亚州免费视频 | 日韩免费成人 | 日韩成人黄色av | 色婷婷狠狠18 | 成年人免费在线观看网站 | 成人免费网站在线观看 | 在线亚洲欧美日韩 | 天天干天天操天天射 | 欧美激情片在线观看 | 久久夜夜夜 | 激情小说久久 | 久久在线看 | 又粗又长又大又爽又黄少妇毛片 | 福利视频入口 | 一区二区三区在线电影 | 欧美午夜精品久久久久久浪潮 | 亚洲精品国产精品国自产观看浪潮 | 色婷婷福利 | 菠萝菠萝在线精品视频 | 伊人永久在线 | 91视频啪 | 狠狠色丁香久久婷婷综合五月 | 亚洲国产成人精品电影在线观看 | 在线播放 日韩专区 | 黄色精品一区 | 2021国产精品视频 | 久久艹欧美 | 五月综合久久 | 很黄很色很污的网站 | 久久高清片 | 麻豆免费视频 | 最近中文字幕完整视频高清1 | 免费网站v| 久久久久久久免费看 | 成人黄色资源 | 丁香网婷婷| 91久久久国产精品 | 久久免费99精品久久久久久 | 成人av电影在线播放 | 99九九热只有国产精品 | 少妇精69xxtheporn | 久久综合久久综合久久综合 | 日韩高清免费无专码区 | 99中文在线 | 日韩午夜在线观看 | 又污又黄的网站 | 在线观看日韩国产 | 日日摸日日| 国产黄色理论片 | 精品国产一区二区在线 | 国产又粗又长的视频 | 久久久www成人免费精品张筱雨 | 免费在线观看的av网站 | 91成人在线网站 | 天天激情站| av网站在线观看免费 | 精品字幕 | 一级黄色片毛片 | 婷婷色网址 | 手机av电影在线 | 成人午夜电影久久影院 | 成人网页在线免费观看 | 成人免费视频视频在线观看 免费 | 国产直播av | 成人在线免费看 | 香蕉网站在线观看 | 一区二区精品久久 | 国产一级片观看 | 久久精品一级片 | 国产午夜精品一区二区三区嫩草 | 国产偷v国产偷∨精品视频 在线草 | 久久婷婷色 | 久久久精品一区二区三区 | 在线中文字幕观看 | 久久久久免费精品视频 | 在线观看日韩精品视频 | 免费日韩 精品中文字幕视频在线 | 天天操天天曰 | 国产精品99久久久久久有的能看 | 国产一级电影在线 | 欧美国产大片 | 在线观看亚洲国产精品 | 婷婷丁香社区 | 中日韩男男gay无套 日韩精品一区二区三区高清免费 | 日韩在线无 | 国内精品视频在线 | 九九九九九精品 | 久久成人午夜 | 欧美激情精品 | 日韩av一区二区在线 | 日韩夜夜爽 | 九草在线观看 | 91色综合 | 日韩精品一区二区三区三炮视频 | 日韩精品久久久免费观看夜色 | 天天射天 | 国产在线理论片 | 波多野结衣小视频 | 亚洲成人av电影 | 久久人91精品久久久久久不卡 | 美女黄色网在线播放 | 伊人天天色| 色吊丝av中文字幕 | 91精品国产91 | 日韩中文字幕91 | 亚洲精品国偷拍自产在线观看蜜桃 | 久草在线精品观看 | 久久午夜精品视频 | 91超国产| 91免费版成人 | 国产亚洲成av片在线观看 | 97超视频 | 亚洲成人网在线 | 狠狠操操网 | 九七人人干| 激情在线网站 | 国产精品美女久久久久久久久 | www,黄视频 | 综合伊人久久 | 又紧又大又爽精品一区二区 | 日本女人的性生活视频 | 一区二区三区四区在线免费观看 | 日韩在线观看的 | 成年人视频免费在线播放 | 美女视频黄是免费的 | 成人在线观看免费视频 | 国产精品久久av | 激情视频免费观看 | 麻豆传媒视频在线 | 美女网站视频免费都是黄 | 日韩和的一区二在线 | av九九| 亚洲 成人 一区 | 九九有精品 | 国产色影院 | 精品国产一区二区三区av性色 | 国内精品久久久久久久久久清纯 | 国产精品久久一区二区无卡 | 婷婷去俺也去六月色 | 在线看片日韩 | 在线欧美日韩 | 亚洲五月 | 五月综合色婷婷 | 免费在线激情电影 | 波多野结衣在线播放一区 | 亚洲成人国产精品 | 久久dvd | 国产成人精品一区二区三区网站观看 | 91av官网| 国产精品久久久av久久久 | 搡bbbb搡bbb视频 | 色狠狠一区二区 | 天天草av | 亚洲 欧美 国产 va在线影院 | 涩涩网站在线看 | 日本特黄特色aaa大片免费 | 中文字幕av最新更新 | 亚洲欧美国产精品久久久久 | 国产美女视频免费观看的网站 | av网址aaa| 国内精品美女在线观看 | 国产不卡在线观看 | 干狠狠 | 久久99免费视频 | 国产在线一线 | 精品国产乱码一区二区三区在线 | 久久国产精品99久久久久久老狼 | 丁香六月色 | 日韩av区 | 97国产在线观看 | 国内精品亚洲 | 精品久久久久久久久久久久久久久久久久 | 天天插狠狠干 | 国产高清精品在线 | 在线免费91| 久久久国产精品久久久 | 五月天色网站 | a成人v | 久久久久久伊人 | 黄色特级一级片 | 永久免费精品视频网站 | 欧美巨大 | 中文字幕精品视频 | 亚洲韩国一区二区三区 | 2019久久精品 | 在线国产日本 | 一本一道波多野毛片中文在线 | 中文不卡视频 | 欧美影片| 精品欧美一区二区精品久久 | 天天夜夜狠狠操 | 亚洲精品视频一 | 日韩电影中文字幕在线观看 | 久久国产剧场电影 | 黄色影院在线观看 | 久久久久国产成人精品亚洲午夜 | 欧美一级性生活片 | 免费观看的黄色 | 久久精品牌麻豆国产大山 | 国产亚洲欧美精品久久久久久 | 精品成人久久 | 国产99亚洲 | 激情欧美一区二区免费视频 | 亚洲在线观看av | 97精品一区 | 午夜精品一二三区 | 日韩专区中文字幕 | 久久69av | 亚洲成人蜜桃 | 一性一交视频 | 日韩在线观看视频一区二区三区 | 国产精品久久久久毛片大屁完整版 | 在线播放亚洲激情 | 99在线观看免费视频精品观看 | 日韩影视在线 | 婷五月激情 | 久久久久久久久国产 | 中文字幕在线观看播放 | 久久久久国 | 欧美性另类 | 色国产精品 | 91亚洲网站| 国产在线欧美日韩 | 久久视精品 | 久久狠狠一本精品综合网 | 亚洲性xxxx | 少妇搡bbbb搡bbb搡忠贞 | 精品国产乱子伦一区二区 | 综合久久网站 | 亚洲精品视频在线观看免费视频 | 蜜臀av免费一区二区三区 | 91在线看视频免费 | 国产一区视频在线播放 | 国产精品 亚洲精品 | 日本精品视频免费 | 国产精品观看 | 欧美日韩超碰 | 9999在线视频 | 国产99久久久国产精品免费二区 | 91丨九色丨首页 | 国产区高清在线 | 超薄丝袜一二三区 | 天天操天天操天天干 | 久久精品国产成人 | 免费手机黄色网址 | wwwav视频| 国产免费不卡 | 天天人人综合 | 中文字幕 在线看 | 亚洲最新av在线网站 | 精品视频久久久久久 | 国产精品高清av | 国产精品毛片久久 | 免费高清无人区完整版 | 久久永久免费 | 91精品啪在线观看国产81旧版 | 特片网久久 | 欧美日本不卡视频 | 国产色婷婷精品综合在线手机播放 | 国产一区二区精品久久91 | av电影一区 | 欧美肥妇free | 国产精品福利无圣光在线一区 | 欧美日韩高清一区 | 国产一区国产二区在线观看 | 人人爽人人爽人人爽学生一级 | 免费男女羞羞的视频网站中文字幕 | 婷婷九月丁香 | 亚洲理论片 | www.黄色网.com| 国产丝袜制服在线 | 免费a v在线| 欧美久久九九 | av电影中文| 日本精品视频免费观看 | 黄色片免费在线 | 免费看国产黄色 | 精品免费一区 | 午夜精品在线看 | 麻豆视频在线观看免费 | 国产黑丝一区二区 | 91人人在线| 国产亚洲精品久久久久久久久久久久 | 日本特黄特色aaa大片免费 | 日日碰狠狠添天天爽超碰97久久 | 日韩精品欧美专区 | 天天射天天艹 | 免费a网址| 国产伦理剧 | 国产福利91精品一区二区三区 | 91毛片视频| 婷婷.com| 狠狠躁夜夜躁人人爽视频 | 国产午夜麻豆影院在线观看 | 天天爱天天操天天射 | 久久黄色成人 | 亚洲春色奇米影视 | 久久久久五月 | 热久久视久久精品18亚洲精品 | 日日爽夜夜操 | 日韩视频免费在线 | 97色涩| 国产高清av | 99综合电影在线视频 | 黄色av电影 | 日韩动态视频 | 亚洲.www | 久久99热精品这里久久精品 | 毛片视频电影 | 天天综合网 天天综合色 | 国产精品免费人成网站 | 久久免费视频网 | 激情在线免费视频 | 免费视频久久久 | 日本久久中文字幕 | 国产区精品在线观看 | 免费麻豆网站 | 亚洲欧美视频网站 | 中文字幕一区二区三区在线播放 | 免费国产在线视频 | 国产91免费看 | 激情网婷婷 | 国产一级免费视频 | av网站有哪些 | 久久久久久久久久国产精品 | 91亚洲国产成人 | 色老板在线 | 亚洲欧美精品一区二区 | 久久久69| 91免费看黄 | 国内精品久久久久久中文字幕 | 黄色三级网站在线观看 | 波多野结衣一区 | 国产免费久久 | 狠色狠色综合久久 | 啪啪免费观看网站 | 91热视频 | 不卡的一区二区三区 | 毛片久久久 | 五月婷婷综合色拍 | 在线免费色 | 99视频+国产日韩欧美 | 国产在线观看一区 | 福利网在线 | 国色天香第二季 | 国产成人精品一区二区三区在线 | 免费网址在线播放 | 国产高清免费视频 | 国产精品va在线观看入 | 国产精品一区二区62 | 日韩精品2区 | 香蕉影院在线播放 | 五月天最新网址 | 国产精品久久99精品毛片三a | 亚洲乱码精品久久久久 | 91在线视频免费 | 天天操天天操天天操 | 最新一区二区三区 | 国产一区二区在线视频观看 | 91色影院 | 欧美乱码精品一区二区 | 色操插| 激情网五月婷婷 | 国产精品久久一区二区三区, | 国产一区私人高清影院 | 精品国产一区二区三区不卡 | 亚洲最新视频在线播放 | 久草在线中文888 | 国产一二三区在线观看 | 国产成人精品av | 色综合天天色综合 | 中文字幕在线日亚洲9 | 中文字幕在线中文 | 亚洲国产影院 | 黄a在线观看 | 四虎影视精品永久在线观看 | 亚洲国产网址 | 国产精品99久久99久久久二8 | 久久不卡免费视频 | 亚在线播放中文视频 | 久草视频在线观 | 国产黄在线观看 | 视频在线精品 | 中文字幕在线免费看线人 | 国产一级片在线播放 | 高清国产在线一区 | 久久久久一区二区三区 | 精品国产欧美 | 国产精品剧情在线亚洲 | 亚州精品在线视频 | 久久综合久久综合九色 | 国产精品不卡视频 | 在线看国产一区 | 国产视频观看 | 日韩国产精品久久 | 亚洲激情 欧美激情 | 激情婷婷久久 | 日韩高清激情 | 午夜婷婷在线播放 | 精品在线小视频 | 日韩二区在线 | 丁香综合激情 | 亚洲最新av在线网址 | 欧美在线18 | 久久天天躁狠狠躁亚洲综合公司 | 中文字幕观看av | 99久久精品免费一区 | 五月婷婷视频 | 天天爽天天碰狠狠添 | 在线导航av| 激情av综合 | 在线国产中文字幕 | 日韩aa视频 | 韩国av免费 | 午夜精品一区二区三区视频免费看 | 国产精品久久久久永久免费 | 午夜视频在线观看一区 | 亚洲综合激情小说 | 可以免费观看的av片 | 亚洲精品在线视频网站 | 国产手机av | 久久久精品国产免费观看同学 | 久久综合桃花 | 狠狠狠狠狠狠狠干 | 97色综合| 韩日三级在线 | 国产精品入口麻豆 | www.com.黄| 国产成人精品一区二区三区 | 成人免费看片网址 |