日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

Java8 ThreadLocal 源码分析

發布時間:2024/9/30 java 28 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java8 ThreadLocal 源码分析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

可參考文章: Java8 IdentityhashMap 源碼分析
IdentityhashMap 與 ThreadLocalMap 一樣都是采用線性探測法解決哈希沖突,有興趣的可以先了解下 IdentityhashMap 。

一、ThreadLocal 簡介

在學習源碼之前,有一個概念我們需要先明白:ThreadLocal 可以使多線程間數據讀寫隔離,因此 ThreadLocal 解決的是線程局部變量安全性問題,并不是多線程間共享變量安全性問題。

ThreadLocal 在使用時必須先初始化 value,否則會報空指針異常,你可以通過 set 方法與重寫 initialValue 方法兩種方式初始化 value。

下面是 ThreadLocal 原理圖,讀源碼的時候可以參考。

二、ThreadLocal 源碼

我們先來了解一下 ThreadLocal,然后再逐漸了解 ThreadLocalMap。

2.1 內部相關屬性

/** ThreadLocal 的哈希值通過一個原子類計算*/private final int threadLocalHashCode = nextHashCode();/*** 用于計算 ThreadLocal 哈希值的原子類*/private static AtomicInteger nextHashCode = new AtomicInteger();/** * 計算 ThreadLocal 哈希值的魔數 * 該值生成出來的值可以較為均勻地分布在 2 的冪大小的數組中* 據說與斐波那契散列有關...*/private static final int HASH_INCREMENT = 0x61c88647;

ThreadLocalMap 的結構是通過純數組實現的,因此 ThreadLocal 計算哈希值的方式也比較特殊,通過 nextHashCode() 方法生成哈希值,下面是具體實現。

private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}

生成哈希值時每次加上 0x61c88647,據了解通過 0x61c88647 計算出來的哈希值能夠均勻的分布在 2 的冪大小的數組中,有興趣的可以網上查一下進行詳細的了解。

2.2 set 方法

public void set(T value) {Thread t = Thread.currentThread();// 根據當前線程獲取對應的 mapThreadLocalMap map = getMap(t);if (map != null)// key 是當前 ThreadLocal 對象的引用map.set(this, value);elsecreateMap(t, value);}

在設置 value 時會先調用 getMap 方法根據當前線程獲取對應的 map,如果 map 存在就設置值,不存在則創建 map,下面跟別來看下對應的方法(map.set 方法會在下面分析)。

ThreadLocalMap getMap(Thread t) {return t.threadLocals;}

getMap 方法很簡單,就是返回當前線程的 threadLocals,這個 threadLocals 就是 ThreadLocalMap 對象。由此可以知道每個 Thread 內部都有一個 ThreadLocalMap 變量。

void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

createMap 方法也比較簡單,創建一個 ThreadLocalMap 并賦值給當前線程的 threadLocals 變量。

2.3 get 方法

public T get() {Thread t = Thread.currentThread();// 根據當前線程獲取對應的 mapThreadLocalMap map = getMap(t);if (map != null) {// 根據當前對象獲取到對應的 Entry,getEntry 方法會在下面 ThreadLocalMap 中看到ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;// 返回 Entry 中對應的 valuereturn result;}}// map 為空時創建return setInitialValue();}

如果 map 存在的話會先獲取到當前線程對應的 map,然后根據當前 ThreadLocal 的弱引用獲取 Entry,最終返回 Entry 中的 value 即可。如果 map 不存在則調用 setInitialValue 方法創建,下面是具體實現細節。

private T setInitialValue() {// 獲取 initialValue() 方法中對應的 value,// 如果沒有重寫 initialValue 方法會拋空指針異常T value = initialValue();Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);// 如果對應的 map 不為空,則重置對應的 valueif (map != null)map.set(this, value);// map 為空,初始化 mapelsecreateMap(t, value);return value;}

2.4 remove 方法

public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}

remove 方法調用了 ThreadLocalMap 中的 remove 方法刪除當前線程的,這個方法到下面介紹 ThreadLocalMap 時再詳細分析。

三、ThreadLocalMap 源碼分析

ThreadLocal 源碼中最有意思的就屬 ThreadLocalMap 了,它到底有哪些巧妙的設計呢?下面就來一探究竟吧。

3.1 內部相關屬性

/*** 哈比表數組默認初始化大小*/private static final int INITIAL_CAPACITY = 16;/*** 底層哈希表數組*/private Entry[] table;/*** 哈希表鍵值對個數*/private int size = 0;/*** 擴容閾值*/private int threshold; // Default to 0/*** 設置擴容閾值為容量的 2/3*/private void setThreshold(int len) {threshold = len * 2 / 3;}/*** Increment i modulo len.當到數組尾時會從頭開始*/private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}/*** Decrement i modulo len.當到數組頭部時會從尾部開始*/private static int prevIndex(int i, int len) {return ((i - 1 >= 0) ? i - 1 : len - 1);}

ThreadLocalMap 與 HashMap 最大的不同是當發生哈希沖突時不通過鏈表形式來解決沖突,而是使用線性探測法解決哈希沖突。ThreadLocalMap 的擴容閾值是 2/3,與 IdentityHashMap 一致,有興趣的可以看下 IdentityHashMap,它們兩個的結構是很相似的。

3.2 構造函數

我們來看其中一個構造函數。

/*** 第一次添加的時候會調用該構造函數進行初始化,并設置第一個線程對應的 key 與 value*/ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {// 初始化哈希表數組table = new Entry[INITIAL_CAPACITY];// 計算桶位置,這個哈希值的計算在上面我們解釋過int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);// 設置到對應的桶位置上(已經有了一個 key 與 value)table[i] = new Entry(firstKey, firstValue);// 初始化 size 為 1size = 1;// 設置擴容閾值為初始容量的 2/3setThreshold(INITIAL_CAPACITY);}

當設置擴容閾值時調用了 setThreshold 方法,這個方法很簡單,就是把閾值設置為數組長度的 2/3。

private void setThreshold(int len) {threshold = len * 2 / 3;}

3.3 Entry 結構

static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {// key 為弱引用super(k);value = v;}}

ThreadLocalMap 中存儲鍵值對的結構是 Entry,Entry 實現了 WeakReference 類使 key 成為一個弱引用。Java 語言的弱引用對象意味著只要被垃圾收集器線程掃描到,那么不管當前內存是否足夠都會被回收。關于強引用、軟引用、弱引用與虛引用的差別可以查閱資料進行詳細了解。

3.4 set 方法

ThreadLocalMap 添加鍵值對的方法不是 put 而是 set,如下:

private void set(ThreadLocal<?> key, Object value) {// 獲取哈希表數組 Entry[] tab = table;int len = tab.length;// 計算 key 對應的桶位置int i = key.threadLocalHashCode & (len-1);// e != null 意味著哈希沖突或是 key 重復// e = tab[i = nextIndex(i, len)] 線性探測法解決哈希沖突for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {// 獲取 key 的引用ThreadLocal<?> k = e.get();// key 重復,value 覆蓋if (k == key) {e.value = value;return;}// entry 不為 null,key 為 null,是因為 key 是弱引用,可能已經被 GC 回收了if (k == null) {replaceStaleEntry(key, value, i);return;}}// 找到插入的位置,存儲 key 與 valuetab[i] = new Entry(key, value);int sz = ++size;// cleanSomeSlots 用于刪除可能已經被 GC 回收的 key// 如果沒有 key 被 GC 回收,并且哈希表數組中的鍵值對數量大于 2/3,執行擴容操作if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}

當插入鍵值對的時候,先根據哈希值計算出在哈希表數組中的位置,如果當前桶位置上的 entry 不為空,意味著出現哈希沖突或者是 key 重復。key 重復時直接將原來的 value 覆蓋即可,上面我們已經提到了如果發生哈希沖突,ThreadLocalMap 通過線性探測法方式解決,因此需要繼續從數組當前位置向后查找可插入位置(nextIndex)。當插入鍵值對過后會判斷是否需要對哈希表數組擴容,整體的流程還是很清晰的。下面是 nextIndex 的具體實現:

private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}

當查找到數組尾部時,如果還沒有找到要插入的位置,會從頭繼續查找,因此可以把哈希表數組理解為一個環狀的結構。

ThreadLocalMap 的 key 因為是弱引用,因此當發生哈希沖突時,沖突的 entry 可能不為 null,而 key 為 null(弱引用被 GC 回收),如果 key 為 null 則調用 replaceStaleEntry 方法,下面就來看一下這個方法:

private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {// 獲取哈希表數組Entry[] tab = table;int len = tab.length;Entry e;// 記錄 key 被擦除的桶位置(為 staleSlot 位置前的第一個連續的 key 被擦除的索引// 或 staleSlot 位置后第一個連續的 key 被擦除或 key 重復的索引)int slotToExpunge = staleSlot;// 尋找 staleSlot 索引前連續不為 null 的 key 被擦除的桶位置// 注意循環結束的條件是 e == null 與 IdentityHashMap 相同,也是線性探測法解決哈希沖突的截止條件,有興趣的可以看下 IdentityHasHMapfor (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;// 向后查找for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// key 重復if (k == key) {e.value = value;// i 位置與 staleSlot 位置的 entry 互換,因為 staleSlot 位置上的 key 已經被回收,沒有意義了// TODO 那為什么不把 key 被 GC 回收的 entry 置為 null 而是位置互換呢?不要急,下面 expungeStaleEntry 方法會做tab[i] = tab[staleSlot];tab[staleSlot] = e;// Start expunge at preceding stale entry if it exists// slotToExpunge == staleSlot 意味著向前沒有查找到連續的鍵值對 key 被擦除的情況if (slotToExpunge == staleSlot)slotToExpunge = i;// 將 slotToExpunge 位置上的 entry 清除cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}// 如果 i 位置上的 key 也已經被擦除將 slotToExpunge 置為 iif (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// If key not found, put new entry in stale slot// 把新的鍵值對直接存儲在 staleSlot 位置tab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// If there are any other stale entries in run, expunge them// 如果向前或向后找到了 key 被擦除的 entry,則清除 slotToExpunge 位置上的鍵值對if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}

replaceStaleEntry 方法相對來說比較難以理解,這里總結下我的思路過程,如果大家覺得哪里不對,可以在下面留言。首先我們先確定下 replaceStaleEntry 方法中的 staleSlot 字段,它表示新增鍵值對時 key 重復且 key 被 GC 回收情況下在哈希表數組中的位置。

replaceStaleEntry 方法先從 staleSlot 位置向前查找 entry 不為 null,key 為 null 的 鍵值對,記錄在哈希表數組中的位置,注意這里循環結束的條件是 (e = tab[i]) != null,只要 entry 為 null 就停止循環,這個是線形探測法解決哈希沖突的重要判斷條件,在 IdentityHashMap 中也有體現。

向前查找過后開始向后查找,結束的條件與之前一致,只不過向后查找可能會出現 key 相同的情況,如果 key 重復則重置其 value,然后把 staleSlot 位置與 i 位置的鍵值對位置互換,為什么要互換呢?原因是 staleSlot 位置上的 entry 的 key 已經被 GC 回收了,為了保證哈希沖突的所有鍵值對連續,因此需要把后面沖突的鍵值對前移。

接下來看這一段代碼:

if (slotToExpunge == staleSlot)slotToExpunge = i;// 將 slotToExpunge 位置上的 entry 清除cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

slotToExpunge == staleSlot 表示向前沒有查找到連續的鍵值對 key 被擦除的情況,把 slotToExpunge 的值置為了 i,然后執行了 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len),這個 slotToExpunge 在這里表示鍵值對交換過之后 key 被 GC 回收的那個 entry 所在哈希表數組中索引的位置。因為它的 key 已經被 GC 回收了,就意味著這個鍵值對沒有存在的必要了,需要對其清除,于是就執行了 expungeStaleEntry 方法:

private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// value 置 null,對應桶位置上的 Entry 也置 nulltab[staleSlot].value = null;tab[staleSlot] = null;// 鍵值對數量減 1size--;Entry e;int i;// 刪除一個 key 被擦除的鍵值對,可能因為之前哈希沖突,導致后面桶位置上的鍵值對位置不準確,因此要向前調整后面桶位置上的鍵值對// 從 staleSlot 位置向后遍歷,要求必須連續,與 IdentityHashMap 一致 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal<?> k = e.get();// 如果后面桶位置上鍵值對被擦除,則直接清除,因此 expungeStaleEntry 方法并不是只清除 staleSlot 位置上的鍵值對if (k == null) {e.value = null;tab[i] = null;size--;} else {// 并不是向前移動,而是重新 rehash,計算對應的桶位置// TODO 重點理解,重新 rehash 解決之前可能存在哈希沖突的情況int h = k.threadLocalHashCode & (len - 1);if (h != i) {tab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.while (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}// 返回 staleSlot 之后第一個鍵值對為 null 的桶位置return i;}

expungeStaleEntry 中上來就對 staleSlot 位置上的鍵值對置 null,然后鍵值對的數量減一,但是并不說刪除一個鍵值對這里就結束了。我們說過 ThreadLocalMap 是通過線性探測法來解決哈希沖突的,當刪除一個鍵值對之后需要從當前刪除的位置向后循環,判斷后面是否存在因為哈希沖突被移動到后面去的鍵值對,如果有就重新計算其哈希值,然后存儲到對應的位置上,當然重新計算哈希值也要考慮哈希沖突。順便在這里提一下,這里與 IdentityhashMap 的處理方式是不同的,IdentitiHashMap 并不會重新計算后面沖突的 key 的哈希值而是采取向前移動的方式來解決。

到這里還不算完,expungeStaleEntry 方法中返回了一個 i,這個 i 表示 staleSlot 位置后第一個 key 被 GC 回收的數組索引位置。執行完 expungeStaleEntry 方法后根據其返回值又執行了 cleanSomeSlotsc 方法,這個方法又是干嘛的呢?下面來簡單的分析一下:

private boolean cleanSomeSlots(int i, int n) {boolean removed = false;Entry[] tab = table;int len = tab.length;do {i = nextIndex(i, len);// 獲取對應桶位置上的 EntryEntry e = tab[i];// Entry 不為 null,key 為 null 是因為 key 是弱引用可能會被 GC 回收,因此需要在哈希表中刪除if (e != null && e.get() == null) {n = len;// 如果有鍵值對被擦出就返回 trueremoved = true;// 刪除 i 位置上的鍵值對i = expungeStaleEntry(i);}} /* 對數掃描,并不會掃描整個哈希表數組 */while ( (n >>>= 1) != 0);return removed;}

根據 cleanSomeSlots 的方法名我們應該可以知道這個方法大概做了什么,清除一些哈希槽位置上的鍵值對。這個方法會循環向后判斷當前桶位置上的 key 是否被 GC 回收了,如果被回收了就調用 expungeStaleEntry 方法清除其鍵值對。注意這里不是一直向后循環,而是采取對數的方式,這就說明,整個循環下來并不會清除所有 key 被 GC 回收的鍵值對,會存在一些漏網之魚。

關于 set 方法就簡單的分析到這里,其中還有一些細節大家有興趣可以自己查看,如果哪里有錯誤的地方大家可以在下面留言交流。

3.5 set 方法之 rehash

我們上面一直在分析哈希沖突的情況,還有一個比較重要的 rehash 過程,添加過鍵值對后判斷是否需要 rehash 的是下面這段代碼:

// !cleanSomeSlots(i, sz) 表示沒有鍵值對因為 key 被回收而清除// sz >= threshold 表示哈希表數組中的鍵值對數量已經大于了擴容閾值if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();

當判斷條件通過后會調用 rehash 方法。

private void rehash() {// 清除所有 key 被擦出的鍵值對expungeStaleEntries();// Use lower threshold for doubling to avoid hysteresis// 再次判斷// TODO Q:這個判斷有什么作用?不可能是 false 的啊// A:因為上面調用了 expungeStaleEntries 方法,可能有的鍵值對被移除導致哈希表數組的鍵值對非常少,此時就沒有擴容的必要了if (size >= threshold - threshold / 4)resize();}

rehash() 方法先調用了 expungeStaleEntries() 方法,這個方法里會循環整個哈希表數組,然后清除所有的 key 被 GC 回收的鍵值對。

private void expungeStaleEntries() {Entry[] tab = table;int len = tab.length;for (int j = 0; j < len; j++) {Entry e = tab[j];if (e != null && e.get() == null)expungeStaleEntry(j);}}

為什么上面已經執行過了 cleanSomeSlots 方法來清除鍵值對,為什么這里又要判斷一次呢?原因就是 cleanSomeSlots 方法并不會循環整個哈希表,會存在一些漏網之魚,而 expungeStaleEntries() 方法會連那些漏網之魚一起處理掉。

調用了 expungeStaleEntries() 方法之后,需要重新判斷鍵值對數量,只有當條件滿足時才會調用 resize() 方法。下面是 resize() 方法的是實現:

private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;// 新哈希表的大小為原哈希表大小的 2 倍int newLen = oldLen * 2;// 初始化新哈希表Entry[] newTab = new Entry[newLen];// 記錄新哈希表中鍵值對的個數int count = 0;// 遍歷老哈希表數組,進行 rehashfor (int j = 0; j < oldLen; ++j) {// 獲取老哈希表桶位置上的 EntryEntry e = oldTab[j];if (e != null) {ThreadLocal<?> k = e.get();// 如果 key 被回收,則把 value 也置 null// 無時不刻判斷著 key 被擦除的情況if (k == null) {e.value = null; // Help the GC} else {// 計算老哈希表中的鍵值對在新哈希表中的桶位置int h = k.threadLocalHashCode & (newLen - 1);// 這里也可能會產生哈希沖突while (newTab[h] != null)h = nextIndex(h, newLen);newTab[h] = e;count++;}}}// 設置新的擴容閾值,2/3setThreshold(newLen);size = count;// 新的哈希表替代老的哈希表table = newTab;}

rehash 的過程其實是比較簡單的,生成新的哈希表,然后遍歷舊的哈希表數組,將鍵值對重新 rehash 存儲到新的哈希表數組中即可。

3.6 getEntry 方法

private Entry getEntry(ThreadLocal<?> key) {// 獲取桶位置int i = key.threadLocalHashCode & (table.length - 1);// 獲取桶位置上對應的鏈表Entry e = table[i];// 哈希不沖突,直接獲取對應的 value 并返回if (e != null && e.get() == key)return e;// 哈希沖突,則遍歷后面的桶位置,進行查找,當然 key 可能因為是弱引用被擦出,需要額外處理elsereturn getEntryAfterMiss(key, i, e);}

getEntry 方法實現很簡單,根據 key 的哈希值計算在哈希表數組中的桶位置,然后,如果當前對應的桶位置上的 key 是同一個則直接返回 Entry,反之則調用 getEntryAfterMiss 方法來處理哈希沖突的情況。

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {Entry[] tab = table;int len = tab.length;// Entry 為 null 作為循環結束條件while (e != null) {ThreadLocal<?> k = e.get();// 找到直接返回, valueif (k == key)return e;// 如果 key 被擦出,則清除if (k == null)expungeStaleEntry(i);else// 重置 i 用于循環遍歷i = nextIndex(i, len);e = tab[i];}// 沒有找到返回 nullreturn null;}

代碼邏輯很清晰,這里就不進行詳細總結了。

3.7 remove 方法

上面我們看了添加方法,下面來看一下刪除操作:

private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;// 計算出對應的桶位置,當然對應桶位置上的鍵值對并不一定是當前 key 對應的鍵值對,因為可能存在哈希沖突int i = key.threadLocalHashCode & (len-1);// 從 i 位置向后遍歷,遍歷結束的位置是后續桶位置上為 nullfor (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {// key 清空e.clear();// 調用 expungeStaleEntry 方法expungeStaleEntry(i);return;}}}

根據 key 就算哈希值,在哈希表數組中找到對應的位置開始循環判斷,如果 key 相同則調用 expungeStaleEntry 方法直接清除鍵值對。PS:注意循環結束條件 e != null。

關于 ThreadLocal 的源碼就分析到這里,一千個人眼里有一千個哈姆雷特,只有自己去看了才能有更深刻的了解。

關于 jdk1.8 更多的源碼分析,請點擊這里前往:jdk1.8 源碼閱讀

總結

以上是生活随笔為你收集整理的Java8 ThreadLocal 源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

主站蜘蛛池模板: 综合色99 | 女人扒开屁股让男人捅 | 亚洲欧美激情一区二区三区 | 亚洲最大成人综合网 | 久久久精品蜜桃 | 国产无人区码熟妇毛片多 | 久久精品a亚洲国产v高清不卡 | 日本三级视频在线观看 | 日韩一区二区高清视频 | 国模私拍大尺度裸体av | 中文字幕一区二区三区免费视频 | 成人二区三区 | 午夜爽视频 | 无码任你躁久久久久久久 | 久久亚洲中文字幕无码 | 亚洲熟妇一区二区三区 | 91娇羞白丝 | 国模精品一区二区三区 | 中文字幕精品视频在线 | 亚洲无吗av| 99久久久久久 | 精品人妻无码专区在线 | heyzo朝桐光一区二区 | 美女网站在线 | 禁断介护av一区二区 | 国模少妇一区二区 | 夜夜春夜夜爽 | 人人澡人人插 | 国产女女 | 一区二区三区四区视频在线观看 | 久久伊人草| 琪琪色18 | 涩涩视频在线 | 中文字幕av久久爽一区 | 精品+无码+在线观看 | 午夜av在线免费观看 | 黄色一级视频网站 | 67194午夜 | 一级免费看 | 日本四级电影 | 一级全黄色片 | 国产在线看片 | 国产日韩欧美高清 | 黄色特级片 | xxx国产在线观看 | 日韩网站在线 | 日韩欧美aⅴ综合网站发布 国产成人一区二区三区小说 | 国产日本欧美在线 | 美女国产精品 | 999伊人| 亚洲Av无码成人精品区伊人 | 老司机免费精品视频 | 9i免费看片黄 | 看91| 蜜桃视频污 | 亚欧毛片| 91婷婷色 | 小优视频污 | 亚洲国产婷婷 | 欧美系列第一页 | 一区二区福利视频 | 久久久久久久久久久久国产 | 国产免费无遮挡吸奶头视频 | 秋霞在线视频观看 | 国产91一区二区三区在线精品 | 三级视频在线 | 亚洲免费视频一区 | 免费黄av | 日本成人午夜视频 | 欧美性生活一级 | 色婷婷91 | 欧美亚洲自拍偷拍 | 写真福利片hd在线播放 | 美女一区二区三区四区 | 在线观看中文字幕码 | 亚洲欧美网址 | 黄色a网站| 国产人人爱 | 国产又爽又黄免费软件 | 亚洲视频一区二区三区在线观看 | 国产一区二区在线观看视频 | 精品国偷自产国产一区 | 伊人看片 | 五月天丁香视频 | 99riav国产精品 | 一区二区三区欧美 | 久久丫精品忘忧草西安产品 | 国产精品扒开做爽爽爽的视频 | av第一福利 | 诱惑av | 国产免费看 | 久久精品无码一区 | 重口变态虐黄网站 | 久久av免费观看 | 国产色中色 | 日本在线观看一区二区三区 | 亚洲男人的天堂在线视频 | 日韩精品卡通动漫网站 | 国产精品三级久久久久久电影 |