<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          我把 ThreadLocal 能問(wèn)的,都寫(xiě)了

          共 8360字,需瀏覽 17分鐘

           ·

          2021-09-18 23:49

          今天我們?cè)賮?lái)盤(pán)一盤(pán) ThreadLocal ,這篇力求對(duì) ThreadLocal 一網(wǎng)打盡,徹底弄懂 ThreadLocal 的機(jī)制。

          話不多說(shuō),本文要解決的問(wèn)題如下:

          1. 為什么需要 ThreadLocal
          2. 應(yīng)該如何設(shè)計(jì) ThreadLocal
          3. 從源碼看ThreadLocal 的原理
          4. ThreadLocal 內(nèi)存泄露之為什么要用弱引用
          5. ThreadLocal 的最佳實(shí)踐
          6. InheritableThreadLocal

          好了,開(kāi)車(chē)!

          為什么需要 ThreadLocal

          最近不是開(kāi)放三胎政策嘛,假設(shè)你有三個(gè)孩子。

          現(xiàn)在你帶著三個(gè)孩子出去逛街,路過(guò)了玩具店,三個(gè)孩子都看中了一款變形金剛。

          所以你買(mǎi)了一個(gè)變形金剛,打算讓三個(gè)孩子輪著玩。

          回到家你發(fā)現(xiàn),孩子因?yàn)檫@個(gè)玩具吵架了,三個(gè)都爭(zhēng)著要玩,誰(shuí)也不讓著誰(shuí)。

          這時(shí)候怎么辦呢?你可以去拉架,去講道理,說(shuō)服孩子輪流玩,但這很累。

          所以一個(gè)簡(jiǎn)單的辦法就是出去再買(mǎi)兩個(gè)變形金剛,這樣三個(gè)孩子都有各自的變形金剛,世界就暫時(shí)得到了安寧。

          映射到我們今天的主題,變形金剛就是共享變量,孩子就是程序運(yùn)行的線程。

          有多個(gè)線程(孩子),爭(zhēng)搶同一個(gè)共享變量(玩具),就會(huì)產(chǎn)生沖突,而程序的解決辦法是加鎖(父母說(shuō)服,講道理,輪流玩),但加鎖就意味著性能的消耗(父母比較累)。

          所以有一種解決辦法就是避免共享(讓每個(gè)孩子都各自擁有一個(gè)變形金剛),這樣線程之間就不需要競(jìng)爭(zhēng)共享變量(孩子之間就不會(huì)爭(zhēng)搶)。

          所以為什么需要 ThreadLocal?

          就是為了通過(guò)本地化資源來(lái)避免共享,避免了多線程競(jìng)爭(zhēng)導(dǎo)致的鎖等消耗。

          這里需要強(qiáng)調(diào)一下,不是說(shuō)任何東西都能直接通過(guò)避免共享來(lái)解決,因?yàn)橛行r(shí)候就必須共享。

          舉個(gè)例子:當(dāng)利用多線程同時(shí)累加一個(gè)變量的時(shí)候,此時(shí)就必須共享,因?yàn)橐粋€(gè)線程的對(duì)變量的修改需要影響要另個(gè)線程,不然累加的結(jié)果就不對(duì)了。

          再舉個(gè)不需要共享的例子:比如現(xiàn)在每個(gè)線程需要判斷當(dāng)前請(qǐng)求的用戶來(lái)進(jìn)行權(quán)限判斷,那這個(gè)用戶信息其實(shí)就不需要共享,因?yàn)槊總€(gè)線程只需要管自己當(dāng)前執(zhí)行操作的用戶信息,跟別的用戶不需要有交集。

          好了,道理很簡(jiǎn)單,這下子想必你已經(jīng)清晰了 ThreadLocal 出現(xiàn)的緣由了。

          再來(lái)看一下 ThreadLocal 使用的小 demo。

          public class YesThreadLocal {

              private static final ThreadLocal<String> threadLocalName = ThreadLocal.withInitial(() -> Thread.currentThread().getName());

              public static void main(String[] args) {
                  for (int i = 0; i < 5; i++) {
                      new Thread(() -> {
                          System.out.println("threadName: " + threadLocalName.get());
                      }, "yes-thread-" + i).start();
                  }
              }
          }

          輸出結(jié)果如下:

          可以看到,我在 new 線程的時(shí)候,設(shè)置了每個(gè)線程名,每個(gè)線程都操作同一個(gè) ThreadLocal 對(duì)象的 get 卻返回的各自的線程名,是不是很神奇?

          應(yīng)該如何設(shè)計(jì) ThreadLocal ?

          那應(yīng)該怎么設(shè)計(jì) ThreadLocal 來(lái)實(shí)現(xiàn)以上的操作,即本地化資源呢?

          我們的目標(biāo)已經(jīng)明確了,就是用 ThreadLocal 變量來(lái)實(shí)現(xiàn)線程隔離。

          從代碼上看,可能最直接的實(shí)現(xiàn)方法就是將 ThreadLocal 看做一個(gè) map ,然后每個(gè)線程是  key,這樣每個(gè)線程去調(diào)用 ThreadLocal.get 的時(shí)候,將自身作為 key 去 map 找,這樣就能獲取各自的值了。

          聽(tīng)起來(lái)很完美?錯(cuò)了!

          這樣 ThreadLocal 就變成共享變量了,多個(gè)線程競(jìng)爭(zhēng) ThreadLocal ,那就得保證 ThreadLocal 的并發(fā)安全,那就得加鎖了,這樣繞了一圈就又回去了。

          所以這個(gè)方案不行,那應(yīng)該怎么做?

          答案其實(shí)上面已經(jīng)講了,是需要在每個(gè)線程的本地都存一份值,說(shuō)白了就是每個(gè)線程需要有個(gè)變量,來(lái)存儲(chǔ)這些需要本地化資源的值,并且值有可能有多個(gè),所以怎么弄呢?

          在線程對(duì)象內(nèi)部搞個(gè) map,把 ThreadLocal 對(duì)象自身作為 key,把它的值作為 map 的值。

          這樣每個(gè)線程可以利用同一個(gè)對(duì)象作為 key ,去各自的 map 中找到對(duì)應(yīng)的值。

          這不就完美了嘛!比如我現(xiàn)在有 3 個(gè) ThreadLocal  對(duì)象,2 個(gè)線程。

          ThreadLocal<String> threadLocal1 =  new ThreadLocal<>();
          ThreadLocal<Integer> threadLocal2 =  new ThreadLocal<>();
          ThreadLocal<Integer> threadLocal3 =  new ThreadLocal<>();

          那此時(shí) ThreadLocal  對(duì)象和線程的關(guān)系如下圖所示:

          這樣一來(lái)就滿足了本地化資源的需求,每個(gè)線程維護(hù)自己的變量,互不干擾,實(shí)現(xiàn)了變量的線程隔離,同時(shí)也滿足存儲(chǔ)多個(gè)本地變量的需求,完美!

          JDK就是這樣實(shí)現(xiàn)的!我們來(lái)看看源碼。

          從源碼看ThreadLocal 的原理

          前面我們說(shuō)到 Thread 對(duì)象里面會(huì)有個(gè) map,用來(lái)保存本地變量。

          我們來(lái)看下 jdk 的 Thread 實(shí)現(xiàn)

          public class Thread implements Runnable {
               // 這就是我們說(shuō)的那個(gè) map 。
              ThreadLocal.ThreadLocalMap threadLocals = null;
          }

          可以看到,確實(shí)有個(gè) map ,不過(guò)這個(gè) map 是 ThreadLocal 的靜態(tài)內(nèi)部類,記住這個(gè)變量的名字 threadLocals,下面會(huì)有用的哈。

          看到這里,想必有很多小伙伴會(huì)產(chǎn)生一個(gè)疑問(wèn)。

          竟然這個(gè) map 是放在 Thread 里面使用,那為什么要定義成 ThreadLocal 的靜態(tài)內(nèi)部類呢?

          首先內(nèi)部類這個(gè)東西是編譯層面的概念,就像語(yǔ)法糖一樣,經(jīng)過(guò)編譯器之后其實(shí)內(nèi)部類會(huì)提升為外部頂級(jí)類,和平日里外部定義的類沒(méi)有區(qū)別,也就是說(shuō)在 JVM 中是沒(méi)有內(nèi)部類這個(gè)概念的。

          一般情況下非靜態(tài)內(nèi)部類用在內(nèi)部類,跟其他類無(wú)任何關(guān)聯(lián),專屬于這個(gè)外部類使用,并且也便于調(diào)用外部類的成員變量和方法,比較方便。

          而靜態(tài)外部類其實(shí)就等于一個(gè)頂級(jí)類,可以獨(dú)立于外部類使用,所以更多的只是表明類結(jié)構(gòu)和命名空間

          所以說(shuō)這樣定義的用意就是說(shuō)明 ThreadLocalMap 是和 ThreadLocal 強(qiáng)相關(guān)的,專用于保存線程本地變量。

          現(xiàn)在我們來(lái)看一下 ThreadLocalMap 的定義:

          重點(diǎn)我已經(jīng)標(biāo)出來(lái)了,首先可以看到這個(gè) ThreadLocalMap 里面有個(gè) Entry 數(shù)組,熟悉 HashMap 的小伙伴可能有點(diǎn)感覺(jué)了。

          這個(gè) Entry 繼承了 WeakReference 即弱引用。這里需要注意,不是說(shuō) Entry 自己是弱引用,看到我標(biāo)注的 Entry 構(gòu)造函數(shù)的 super(k) 沒(méi),這個(gè) key 才是弱引用。

          所以 ThreadLocalMap 里有個(gè) Entry 的數(shù)組,這個(gè) Entry 的 key 就是 ThreadLocal 對(duì)象,value 就是我們需要保存的值。

          那是如何通過(guò) key 在數(shù)組中找到 Entry 然后得到 value 的呢 ?

          這就要從上面的 threadLocalName.get()說(shuō)起,不記得這個(gè)代碼的滑上去看下示例,其實(shí)就是調(diào)用 ThreadLocal 的 get 方法。

          此時(shí)就進(jìn)入 ThreadLocal#get方法中了,這里就可以得知為什么不同的線程對(duì)同一個(gè) ThreadLocal 對(duì)象調(diào)用 get 方法竟然能得到不同的值了。

          這個(gè)中文注釋想必很清晰了吧!

          ThreadLocal#get方法首先獲取當(dāng)前線程,然后得到當(dāng)前線程的 ThreadLocalMap 變量即 threadLocals,然后將自己作為 key 從 ThreadLocalMap 中找到 Entry ,最終返回 Entry 里面的 value 值。

          這里我們?cè)倏匆幌?key 是如何從 ThreadLocalMap 中找到 Entry 的,即map.getEntry(this)是如何實(shí)現(xiàn)的,其實(shí)很簡(jiǎn)單。

          可以看到 ThreadLocalMap 雖然和 HashMap 一樣,都是基于數(shù)組實(shí)現(xiàn)的,但是它們對(duì)于 Hash 沖突的解決方法不一樣。

          HashMap 是通過(guò)鏈表(紅黑樹(shù))法來(lái)解決沖突,而 ThreadLocalMap 是通過(guò)開(kāi)放尋址法來(lái)解決沖突。

          聽(tīng)起來(lái)好像很高級(jí),其實(shí)道理很簡(jiǎn)單,我們來(lái)看一張圖就很清晰了。

          所以說(shuō),如果通過(guò) key 的哈希值得到的下標(biāo)無(wú)法直接命中,則會(huì)將下標(biāo) +1,即繼續(xù)往后遍歷數(shù)組查找 Entry ,直到找到或者返回 null。

          可以看到,這種 hash 沖突的解決效率其實(shí)不高,但是一般 ThreadLocal 也不會(huì)太多,所以用這種簡(jiǎn)單的辦法解決即可。

          至于代碼中的expungeStaleEntry我們等下再分析,先來(lái)看下 ThreadLocalMap#set 方法,看看寫(xiě)入的怎樣實(shí)現(xiàn)的,來(lái)看看 hash 沖突的解決方法是否和上面說(shuō)的一致。

          可以看到 set 的邏輯也很清晰。

          先通過(guò) key 的 hash 值計(jì)算出一個(gè)數(shù)組下標(biāo),然后看看這個(gè)下標(biāo)是否被占用了,如果被占了看看是否就是要找的 Entry 。

          如果是則進(jìn)行更新,如果不是則下標(biāo)++,即往后遍歷數(shù)組,查找下一個(gè)位置,找到空位就 new 個(gè) Entry 然后把坑給占用了。

          當(dāng)然,這種數(shù)組操作一般免不了閾值的判斷,如果超過(guò)閾值則需要進(jìn)行擴(kuò)容。

          上面的清理操作和 key 為空的情況,下面再做分析,這里先略過(guò)。

          至此,我們已經(jīng)分析了 ThreadLocalMap 的核心操作 get 和 set ,想必你對(duì) ThreadLocalMap 的原理已經(jīng)從源碼層面清晰了!

          可能有些小伙伴對(duì) key 的哈希值的來(lái)源有點(diǎn)疑惑,所以我再來(lái)補(bǔ)充一下 key.threadLocalHashCode的分析。

          可以看到key.threadLocalHashCode其實(shí)就是調(diào)用 nextHashCode 進(jìn)行一個(gè)原子類的累加。

          注意看上面都是靜態(tài)變量和靜態(tài)方法,所以在 ThreadLocal 對(duì)象之間是共享的,然后通過(guò)固定累加一個(gè)奇怪的數(shù)字0x61c88647來(lái)分配 hash 值。

          這個(gè)數(shù)字當(dāng)然不是亂寫(xiě)的,是實(shí)驗(yàn)證明的一個(gè)值,即通過(guò) 0x61c88647 累加生成的值與 2 的冪取模的結(jié)果,可以較為均勻地分布在 2 的冪長(zhǎng)度的數(shù)組中,這樣可以減少 hash 沖突。

          有興趣的小伙伴可以深入研究一下,反正我沒(méi)啥興趣。

          ThreadLocal 內(nèi)存泄露之為什么要用弱引用

          接下來(lái)就是要解決上面挖的坑了,即 key 的弱引用、Entry 的 key 為什么可能為 null、還有清理 Entry 的操作。

          之前提到過(guò),Entry 對(duì) key 是弱引用,那為什么要弱引用呢?

          我們知道,如果一個(gè)對(duì)象沒(méi)有強(qiáng)引用,只有弱引用的話,這個(gè)對(duì)象是活不過(guò)一次 GC 的,所以這樣的設(shè)計(jì)就是為了讓當(dāng)外部沒(méi)有對(duì) ThreadLocal 對(duì)象有強(qiáng)引用的時(shí)候,可以將 ThreadLocal 對(duì)象給清理掉。

          那為什么要這樣設(shè)計(jì)呢?

          假設(shè) Entry 對(duì) key 的引用是強(qiáng)引用,那么來(lái)看一下這個(gè)引用鏈:

          從這條引用鏈可以得知,如果線程一直在,那么相關(guān)的 ThreadLocal 對(duì)象肯定會(huì)一直在,因?yàn)樗恢北粡?qiáng)引用著。

          看到這里,可能有人會(huì)說(shuō)那線程被回收之后就好了呀。

          重點(diǎn)來(lái)了!線程在我們應(yīng)用中,常常是以線程池的方式來(lái)使用的,比如 Tomcat 的線程池處理了一堆請(qǐng)求,而線程池中的線程一般是不會(huì)被清理掉的,所以這個(gè)引用鏈就會(huì)一直在,那么 ThreadLocal 對(duì)象即使沒(méi)有用了,也會(huì)隨著線程的存在,而一直存在著!

          所以這條引用鏈需要弱化一下,而能操作的只有 Entry 和 key 之間的引用,所以它們之間用弱引用來(lái)實(shí)現(xiàn)。

          與之對(duì)應(yīng)的還有一個(gè)條引用鏈,我結(jié)合著上面的線程引用鏈都畫(huà)出來(lái):

          另一條引用鏈就是棧上的 ThreadLocal 引用指向堆中的 ThreadLocal 對(duì)象,這個(gè)引用是強(qiáng)引用。

          如果有這條強(qiáng)引用存在,那說(shuō)明此時(shí)的 ThreadLocal 是有用的,此時(shí)如果發(fā)生 GC 則 ThreadLocal 對(duì)象不會(huì)被清除,因?yàn)橛袀€(gè)強(qiáng)引用存在。

          當(dāng)隨著方法的執(zhí)行完畢,相應(yīng)的棧幀也出棧了,此時(shí)這條強(qiáng)引用鏈就沒(méi)了,如果沒(méi)有別的棧有對(duì) ThreadLocal 對(duì)象的引用,那么說(shuō)明 ThreadLocal 對(duì)象無(wú)法再被訪問(wèn)到(定義成靜態(tài)變量的另說(shuō))。

          那此時(shí) ThreadLocal 只存在與 Entry 之間的弱引用,那此時(shí)發(fā)生 GC 它就可以被清除了,因?yàn)樗鼰o(wú)法被外部使用了,那就等于沒(méi)用了,是個(gè)垃圾,應(yīng)該被處理來(lái)節(jié)省空間。

          至此,想必你已經(jīng)明白為什么 Entry 和 key 之間要設(shè)計(jì)為弱引用,就是因?yàn)槠饺站€程的使用方式基本上都是線程池,所以線程的生命周期就很長(zhǎng),可能從你部署上線后一直存在,而 ThreadLocal 對(duì)象的生命周期可能沒(méi)這么長(zhǎng)。

          所以為了能讓已經(jīng)沒(méi)用 ThreadLocal 對(duì)象得以回收,所以 Entry 和 key 要設(shè)計(jì)成弱引用,不然 Entry 和 key是強(qiáng)引用的話,ThreadLocal 對(duì)象就會(huì)一直在內(nèi)存中存在。

          但是這樣設(shè)計(jì)就可能產(chǎn)生內(nèi)存泄漏。

          那什么叫內(nèi)存泄漏?

          就是指:程序中已經(jīng)無(wú)用的內(nèi)存無(wú)法被釋放,造成系統(tǒng)內(nèi)存的浪費(fèi)。

          當(dāng) Entry 中的 key 即 ThreadLocal 對(duì)象被回收了之后,會(huì)發(fā)生 Entry 中 key 為 null 的情況,其實(shí)這個(gè) Entry 就已經(jīng)沒(méi)用了,但是又無(wú)法被回收,因?yàn)橛?Thread->ThreadLocalMap ->Entry 這條強(qiáng)引用在,這樣沒(méi)用的內(nèi)存無(wú)法被回收就是內(nèi)存泄露。

          那既然會(huì)有內(nèi)存泄漏還這樣實(shí)現(xiàn)?

          這里就要填一填上面的坑了,也就是涉及到的關(guān)于 expungeStaleEntry即清理過(guò)期的 Entry 的操作。

          設(shè)計(jì)者當(dāng)然知道會(huì)出現(xiàn)這種情況,所以在多個(gè)地方都做了清理無(wú)用 Entry ,即 key 已經(jīng)被回收的 Entry 的操作。

          比如通過(guò) key 查找 Entry 的時(shí)候,如果下標(biāo)無(wú)法直接命中,那么就會(huì)向后遍歷數(shù)組,此時(shí)遇到 key 為 null 的 Entry 就會(huì)清理掉,再貼一下這個(gè)方法:

          這個(gè)方法也很簡(jiǎn)單,我們來(lái)看一下它的實(shí)現(xiàn):

          所以在查找 Entry 的時(shí)候,就會(huì)順道清理無(wú)用的 Entry ,這樣就能防止一部分的內(nèi)存泄露啦!

          還有像擴(kuò)容的時(shí)候也會(huì)清理無(wú)用的 Entry:

          其它還有,我就不貼了,反正知曉設(shè)計(jì)者是做了一些操作來(lái)回收無(wú)用的 Entry 的即可。

          ThreadLocal 的最佳實(shí)踐

          當(dāng)然,等著這些操作被動(dòng)回收不是最好的方法,假設(shè)后面沒(méi)人調(diào)用 get 或者調(diào)用 get 都直接命中或者不會(huì)發(fā)生擴(kuò)容,那無(wú)用的 Entry 豈不是一直存在了嗎?所以上面說(shuō)只能防止一部分的內(nèi)存泄露。

          所以,最佳實(shí)踐是用完了之后,調(diào)用一下 remove 方法,手工把 Entry 清理掉,這樣就不會(huì)發(fā)生內(nèi)存泄漏了!

          void yesDosth {
           threadlocal.set(xxx);
           try {
            // do sth
           } finally {
            threadlocal.remove();
           }
          }

          這就是使用 Threadlocal 的一個(gè)正確姿勢(shì)啦,即不需要的時(shí)候,顯示的 remove 掉。

          當(dāng)然,如果不是線程池使用方式的話,其實(shí)不用關(guān)系內(nèi)存泄漏,反正線程執(zhí)行完了就都回收了,但是一般我們都是使用線程池的,可能只是你沒(méi)感覺(jué)到。

          比如你用了 tomcat ,其實(shí)請(qǐng)求的執(zhí)行用的就是 tomcat 的線程池,這就是隱式使用。

          還有一個(gè)問(wèn)題,關(guān)于 withInitial 也就是初始化值的方法。

          由于類似 tomcat 這種隱式線程池的存在,即線程第一次調(diào)用執(zhí)行 Threadlocal 之后,如果沒(méi)有顯示調(diào)用 remove 方法,則這個(gè) Entry 還是存在的,那么下次這個(gè)線程再執(zhí)行任務(wù)的時(shí)候,不會(huì)再調(diào)用 withInitial 方法,也就是說(shuō)會(huì)拿到上一次執(zhí)行的值

          但是你以為執(zhí)行任務(wù)的是新線程,會(huì)初始化值,然而它是線程池里面的老線程,這就和預(yù)期不一致了,所以這里需要注意。

          InheritableThreadLocal

          這個(gè)其實(shí)之前文章寫(xiě)過(guò)了,不過(guò)這次竟然寫(xiě)了 threadlocal  就再拿出來(lái)。

          這玩意可以理解為就是可以把父線程的 threadlocal 傳遞給子線程,所以如果要這樣傳遞就用 InheritableThreadLocal ,不要用 threadlocal。

          原理其實(shí)很簡(jiǎn)單,在 Thread 中已經(jīng)包含了這個(gè)成員:

          在父線程創(chuàng)建子線程的時(shí)候,子線程的構(gòu)造函數(shù)可以得到父線程,然后判斷下父線程的 InheritableThreadLocal 是否有值,如果有的話就拷過(guò)來(lái)。

          這里要注意,只會(huì)在線程創(chuàng)建的時(shí)會(huì)拷貝 InheritableThreadLocal 的值,之后父線程如何更改,子線程都不會(huì)受其影響。

          最后

          至此有關(guān) ThreadLocal 的知識(shí)點(diǎn)就差不多了。

          想必你已經(jīng)清楚 ThreadLocal 的原理,包括如何實(shí)現(xiàn),為什么 key 要設(shè)計(jì)成弱引用,并且關(guān)于在線程池中使用的注意點(diǎn)等等。

          其實(shí)本沒(méi)打算寫(xiě) ThreadLocal 的,因?yàn)樽罱诳?Netty ,所以想寫(xiě)一下 FastThreadLocal ,但是前置知識(shí)點(diǎn)是 ThreadLocal ,所以就干了這篇。

          消化了這篇之后,出去面試 ThreadLocal 算是沒(méi)問(wèn)題了吧,最后再留個(gè)小小的思考題。

          那為什么 Entry 中的 value 不弱引用?

          這個(gè)題目來(lái)自群友的一個(gè)面試題哈,想必看完這篇文章之后,這個(gè)題目難不倒你,歡迎留言區(qū)寫(xiě)出答案!

          瀏覽 29
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲一区二区网站 | 永久免费 未满看片软件 | 大香蕉天天日 | 911香蕉视频 | 免费在线观看操逼视频 |