<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>

          細數(shù)線程池的10個坑

          共 15234字,需瀏覽 31分鐘

           ·

          2022-08-29 12:05

          前言

          大家好,我是撿田螺的小男孩。

          日常開發(fā)中,為了更好管理線程資源,減少創(chuàng)建線程和銷毀線程的資源損耗,我們會使用線程池來執(zhí)行一些異步任務。但是線程池使用不當,就可能會引發(fā)生產(chǎn)事故。今天田螺哥跟大家聊聊線程池的10個坑。大家看完肯定會有幫助的~

          1. 線程池默認使用無界隊列,任務過多導致OOM
          2. 線程創(chuàng)建過多,導致OOM
          3. 共享線程池,次要邏輯拖垮主要邏輯
          4. 線程池拒絕策略的坑
          5. Spring內(nèi)部線程池的坑
          6. 使用線程池時,沒有自定義命名
          7. 線程池參數(shù)設置不合理
          8. 線程池異常處理的坑
          9. 使用完線程池忘記關閉
          10. ThreadLocal與線程池搭配,線程復用,導致信息錯亂。

          1.線程池默認使用無界隊列,任務過多導致OOM

          JDK開發(fā)者提供了線程池的實現(xiàn)類,我們基于Executors組件,就可以快速創(chuàng)建一個線程池。日常工作中,一些小伙伴為了開發(fā)效率,反手就用Executors新建個線程池。寫出類似以下的代碼:

                
                /**
          ?*?公眾號:撿田螺的小男孩
          ?*/
          public?class?NewFixedTest?{

          ????public?static?void?main(String[]?args)?{
          ????????ExecutorService?executor?=?Executors.newFixedThreadPool(10);
          ????????for?(int?i?=?0;?i?<?Integer.MAX_VALUE;?i++)?{
          ????????????executor.execute(()?->?{
          ????????????????try?{
          ????????????????????Thread.sleep(10000);
          ????????????????}?catch?(InterruptedException?e)?{
          ????????????????????//do?nothing
          ????????????????}
          ????????????});
          ????????}
          ????}
          }

          使用newFixedThreadPool創(chuàng)建的線程池,是會有坑的,它默認是無界的阻塞隊列,如果任務過多,會導致OOM問題。運行一下以上代碼,出現(xiàn)了OOM。

                
                Exception?in?thread?"main"?java.lang.OutOfMemoryError:?GC?overhead?limit?exceeded
          ?at?java.util.concurrent.LinkedBlockingQueue.offer(LinkedBlockingQueue.java:416)
          ?at?java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
          ?at?com.example.dto.NewFixedTest.main(NewFixedTest.java:14)

          這是因為newFixedThreadPool使用了無界的阻塞隊列的LinkedBlockingQueue,如果線程獲取一個任務后,任務的執(zhí)行時間比較長(比如,上面demo代碼設置了10秒),會導致隊列的任務越積越多,導致機器內(nèi)存使用不停飆升, 最終出現(xiàn)OOM。

          看下newFixedThreadPool的相關源碼,是可以看到一個無界的阻塞隊列的,如下:

                
                //阻塞隊列是LinkedBlockingQueue,并且是使用的是無參構(gòu)造函數(shù)
          public?static?ExecutorService?newFixedThreadPool(int?nThreads)?{
          ????return?new?ThreadPoolExecutor(nThreads,?nThreads,
          ??????????????????????????????????0L,?TimeUnit.MILLISECONDS,
          ??????????????????????????????????new?LinkedBlockingQueue<Runnable>());
          }
          ????
          //無參構(gòu)造函數(shù),默認最大容量是Integer.MAX_VALUE,相當于無界的阻塞隊列的了
          public?LinkedBlockingQueue()?{
          ????this(Integer.MAX_VALUE);
          }

          因此,工作中,建議大家自定義線程池,并使用指定長度的阻塞隊列。

          2. 線程池創(chuàng)建線程過多,導致OOM

          有些小伙伴說,既然Executors組件創(chuàng)建出的線程池newFixedThreadPool,使用的是無界隊列,可能會導致OOM。那么,Executors組件還可以創(chuàng)建別的線程池,如newCachedThreadPool,我們用它也不行嘛?

          我們可以看下newCachedThreadPool的構(gòu)造函數(shù):

                
                ????public?static?ExecutorService?newCachedThreadPool()?{
          ????????return?new?ThreadPoolExecutor(0,?Integer.MAX_VALUE,
          ??????????????????????????????????????60L,?TimeUnit.SECONDS,
          ??????????????????????????????????????new?SynchronousQueue<Runnable>());
          ????}

          它的最大線程數(shù)是Integer.MAX_VALUE。大家應該意識到使用它,可能會引發(fā)什么問題了吧。沒錯,如果創(chuàng)建了大量的線程也有可能引發(fā)OOM!

          筆者在以前公司,遇到這么一個OOM問題:一個第三方提供的包,是直接使用new Thread實現(xiàn)多線程的。在某個夜深人靜的夜晚,我們的監(jiān)控系統(tǒng)報警了。。。這個相關的業(yè)務請求瞬間特別多,監(jiān)控系統(tǒng)告警OOM了。

          所以我們使用線程池的時候,還要當心線程創(chuàng)建過多,導致OOM問題。大家盡量不要使用newCachedThreadPool,并且如果自定義線程池時,要注意一下最大線程數(shù)。

          3. 共享線程池,次要邏輯拖垮主要邏輯

          要避免所有的業(yè)務邏輯共享一個線程池。比如你用線程池A來做登錄異步通知,又用線程池A來做對賬。如下圖:

          f5d4aba0f94aa6edaae697e97867e765.webp

          如果對賬任務checkBillService響應時間過慢,會占據(jù)大量的線程池資源,可能直接導致沒有足夠的線程資源去執(zhí)行loginNotifyService的任務,最后影響登錄。就這樣,因為一個次要服務,影響到重要的登錄接口,顯然這是絕對不允許的。因此,我們不能將所有的業(yè)務一鍋燉,都共享一個線程池,因為這樣做,風險太高了,猶如所有雞蛋放到一個籃子里。應當做線程池隔離!

          9739e90bac3e9488c0a9e74f363a268a.webp

          4. 線程池拒絕策略的坑,使用不當導致阻塞

          我們知道線程池主要有四種拒絕策略,如下:

          • AbortPolicy: 丟棄任務并拋出RejectedExecutionException異常。(默認拒絕策略)
          • DiscardPolicy:丟棄任務,但是不拋出異常。
          • DiscardOldestPolicy:丟棄隊列最前面的任務,然后重新嘗試執(zhí)行任務。
          • CallerRunsPolicy:由調(diào)用方線程處理該任務。

          如果線程池拒絕策略設置不合理,就容易有坑。我們把拒絕策略設置為DiscardPolicy或DiscardOldestPolicy并且在被拒絕的任務,Future對象調(diào)用get()方法,那么調(diào)用線程會一直被阻塞。

          我們來看個demo:

                
                /**
          ?*?關注公眾號:撿田螺的小男孩
          ?*/
          public?class?DiscardThreadPoolTest?{

          ????public?static?void?main(String[]?args)?throws?ExecutionException,?InterruptedException?{
          ????????//?一個核心線程,隊列最大為1,最大線程數(shù)也是1.拒絕策略是DiscardPolicy
          ????????ThreadPoolExecutor?executorService?=?new?ThreadPoolExecutor(1,?1,?1L,?TimeUnit.MINUTES,
          ????????????????new?ArrayBlockingQueue<>(1),?new?ThreadPoolExecutor.DiscardPolicy());

          ????????Future?f1?=?executorService.submit(()->?{
          ????????????System.out.println("提交任務1");
          ????????????try?{
          ????????????????Thread.sleep(3000);
          ????????????}?catch?(InterruptedException?e)?{
          ????????????????e.printStackTrace();
          ????????????}
          ????????});

          ????????Future?f2?=?executorService.submit(()->{
          ????????????System.out.println("提交任務2");
          ????????});

          ????????Future?f3?=?executorService.submit(()->{
          ????????????System.out.println("提交任務3");
          ????????});

          ????????System.out.println("任務1完成?"?+?f1.get());//?等待任務1執(zhí)行完畢
          ????????System.out.println("任務2完成"?+?f2.get());//?等待任務2執(zhí)行完畢
          ????????System.out.println("任務3完成"?+?f3.get());//?等待任務3執(zhí)行完畢

          ????????executorService.shutdown();//?關閉線程池,阻塞直到所有任務執(zhí)行完畢

          ????}
          }

          運行結(jié)果:一直在運行中。。。

          edfbc07d6e35e604689dc63c55799128.webp

          這是因為DiscardPolicy拒絕策略,是什么都沒做,源碼如下:

                
                public?static?class?DiscardPolicy?implements?RejectedExecutionHandler?{
          ????/**
          ??????*?Creates?a?{@code?DiscardPolicy}.
          ??????*/
          ????public?DiscardPolicy()?{?}

          ????/**
          ??????*?Does?nothing,?which?has?the?effect?of?discarding?task?r.
          ??????*/
          ????public?void?rejectedExecution(Runnable?r,?ThreadPoolExecutor?e)?{
          ????}
          }

          我們再來看看線程池 submit 的方法:

                
                public?Future<?>?submit(Runnable?task)?{
          ????if?(task?==?null)?throw?new?NullPointerException();
          ????//把Runnable任務包裝為Future對象
          ????RunnableFuture<Void>?ftask?=?newTaskFor(task,?null);
          ????//執(zhí)行任務
          ????execute(ftask);
          ????//返回Future對象
          ????return?ftask;
          }
          ????
          public?FutureTask(Runnable?runnable,?V?result)?{
          ??this.callable?=?Executors.callable(runnable,?result);
          ??this.state?=?NEW;??//Future的初始化狀態(tài)是New
          }

          我們再來看看Future的get() 方法

                
                ??//狀態(tài)大于COMPLETING,才會返回,要不然都會阻塞等待
          ??public?V?get()?throws?InterruptedException,?ExecutionException?{
          ????????int?s?=?state;
          ????????if?(s?<=?COMPLETING)
          ????????????s?=?awaitDone(false,?0L);
          ????????return?report(s);
          ????}
          ????
          ????FutureTask的狀態(tài)枚舉
          ????private?static?final?int?NEW??????????=?0;
          ????private?static?final?int?COMPLETING???=?1;
          ????private?static?final?int?NORMAL???????=?2;
          ????private?static?final?int?EXCEPTIONAL??=?3;
          ????private?static?final?int?CANCELLED????=?4;
          ????private?static?final?int?INTERRUPTING?=?5;
          ????private?static?final?int?INTERRUPTED??=?6;

          阻塞的真相水落石出啦,FutureTask的狀態(tài)大于COMPLETING才會返回,要不然都會一直阻塞等待。又因為拒絕策略啥沒做,沒有修改FutureTask的狀態(tài),因此FutureTask的狀態(tài)一直是NEW,所以它不會返回,會一直等待。

          這個問題,可以使用別的拒絕策略,比如CallerRunsPolicy,它讓主線程去執(zhí)行拒絕的任務,會更新FutureTask狀態(tài)。如果確實想用DiscardPolicy,則需要重寫DiscardPolicy的拒絕策略。

          溫馨提示,日常開發(fā)中,使用 Future.get() 時,盡量使用帶超時時間的,因為它是阻塞的。

                
                future.get(1,?TimeUnit.SECONDS);

          難道使用別的拒絕策略,就萬無一失了嘛?不是的,如果使用CallerRunsPolicy拒絕策略,它表示拒絕的任務給調(diào)用方線程用,如果這是主線程,那會不會可能也導致主線程阻塞呢?總結(jié)起來,大家日常開發(fā)的時候,多一份心眼吧,多一點思考吧。

          5. ?Spring內(nèi)部線程池的坑

          工作中,個別開發(fā)者,為了快速開發(fā),喜歡直接用spring@Async,來執(zhí)行異步任務。

                
                @Async
          public?void?testAsync()?throws?InterruptedException?{
          ????System.out.println("處理異步任務");
          ????TimeUnit.SECONDS.sleep(new?Random().nextInt(100));
          }

          Spring內(nèi)部線程池,其實是SimpleAsyncTaskExecutor,這玩意有點坑,它不會復用線程的,它的設計初衷就是執(zhí)行大量的短時間的任務。有興趣的小伙伴,可以去看看它的源碼:

                
                /**
          *?{@link?TaskExecutor}?implementation?that?fires?up?a?new?Thread?for?each?task,
          *?executing?it?asynchronously.
          *
          *?<p>Supports?limiting?concurrent?threads?through?the?"concurrencyLimit"
          *?bean?property.?By?default,?the?number?of?concurrent?threads?is?unlimited.
          *
          *?<p><b>NOTE:?This?implementation?does?not?reuse?threads!</b>?Consider?a
          *?thread-pooling?TaskExecutor?implementation?instead,?in?particular?for
          *?executing?a?large?number?of?short-lived?tasks.
          *
          *?@author?Juergen?Hoeller
          *?@since?2.0
          *?@see?#setConcurrencyLimit
          *?@see?SyncTaskExecutor
          *?@see?org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor
          *?@see?org.springframework.scheduling.commonj.WorkManagerTaskExecutor
          */
          @SuppressWarnings("serial")
          public?class?SimpleAsyncTaskExecutor?extends?CustomizableThreadCreator?implements?AsyncListenableTaskExecutor,?Serializable?{
          ......
          }

          也就是說來了一個請求,就會新建一個線程!大家使用spring@Async時,要避開這個坑,自己再定義一個線程池。正例如下:

                
                @Bean(name?=?"threadPoolTaskExecutor")
          public?Executor?threadPoolTaskExecutor()?{
          ????ThreadPoolTaskExecutor?executor=new?ThreadPoolTaskExecutor();
          ????executor.setCorePoolSize(5);
          ????executor.setMaxPoolSize(10);
          ????executor.setThreadNamePrefix("tianluo-%d");
          ????//?其他參數(shù)設置
          ????return?new?ThreadPoolTaskExecutor();
          }

          6. 使用線程池時,沒有自定義命名

          使用線程池時,如果沒有給線程池一個有意義的名稱,將不好排查回溯問題。這不算一個坑吧,只能說給以后排查埋坑,哈哈。我還是單獨把它放出來算一個點,因為個人覺得這個還是比較重要的。反例如下:

                
                /**
          ?*?關注公眾號:撿田螺的小男孩
          ?*/
          public?class?ThreadTest?{

          ????public?static?void?main(String[]?args)?throws?Exception?{
          ????????ThreadPoolExecutor?executorOne?=?new?ThreadPoolExecutor(5,?5,?1,?
          ????????????????TimeUnit.MINUTES,?new?ArrayBlockingQueue<Runnable>(20));
          ????????executorOne.execute(()->{
          ????????????System.out.println("關注公眾號:撿田螺的小男孩");
          ????????????throw?new?NullPointerException();
          ????????});
          ????}
          }

          運行結(jié)果:

                
                關注公眾號:撿田螺的小男孩
          Exception?in?thread?"pool-1-thread-1"?java.lang.NullPointerException
          ?at?com.example.dto.ThreadTest.lambda$main$0(ThreadTest.java:17)
          ?at?java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
          ?at?java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
          ?at?java.lang.Thread.run(Thread.java:748)

          可以發(fā)現(xiàn),默認打印的線程池名字是pool-1-thread-1,如果排查問題起來,并不友好。因此建議大家給自己線程池自定義個容易識別的名字。其實用CustomizableThreadFactory即可,正例如下:

                
                
          public?class?ThreadTest?{

          ????public?static?void?main(String[]?args)?throws?Exception?{
          ????????ThreadPoolExecutor?executorOne?=?new?ThreadPoolExecutor(5,?5,?1,
          ????????????????TimeUnit.MINUTES,?new?ArrayBlockingQueue<Runnable>(20),new?CustomizableThreadFactory("Tianluo-Thread-pool"));
          ????????executorOne.execute(()->{
          ????????????System.out.println("關注公眾號:撿田螺的小男孩");
          ????????????throw?new?NullPointerException();
          ????????});
          ????}
          }

          7. 線程池參數(shù)設置不合理

          線程池最容易出坑的地方,就是線程參數(shù)設置不合理。比如核心線程設置多少合理,最大線程池設置多少合理等等。當然,這塊不是亂設置的,需要結(jié)合具體業(yè)務。

          比如線程池如何調(diào)優(yōu),如何確認最佳線程數(shù)?

                
                最佳線程數(shù)目?=?((線程等待時間+線程CPU時間)/線程CPU時間?)*?CPU數(shù)目

          我們的服務器CPU核數(shù)為8核,一個任務線程cpu耗時為20ms,線程等待(網(wǎng)絡IO、磁盤IO)耗時80ms,那最佳線程數(shù)目:( 80 + 20 )/20 * 8 = 40。也就是設置 40個線程數(shù)最佳。

          8. ?線程池異常處理的坑

          我們來看段代碼:

                
                /**
          ?*?關注公眾號:撿田螺的小男孩
          ?*/
          public?class?ThreadTest?{

          ????public?static?void?main(String[]?args)?throws?Exception?{
          ????????ThreadPoolExecutor?executorOne?=?new?ThreadPoolExecutor(5,?5,?1,
          ????????????????TimeUnit.MINUTES,?new?ArrayBlockingQueue<Runnable>(20),new?CustomizableThreadFactory("Tianluo-Thread-pool"));
          ????????for?(int?i?=?0;?i?<?5;?i++)?{
          ????????????executorOne.submit(()->{
          ????????????????System.out.println("current?thread?name"?+?Thread.currentThread().getName());
          ????????????????Object?object?=?null;
          ????????????????System.out.print("result##?"?+?object.toString());
          ????????????});
          ????????}

          ????}
          }

          按道理,運行這塊代碼應該拋空指針異常才是的,對吧。但是,運行結(jié)果卻是這樣的;

                
                current?thread?nameTianluo-Thread-pool1
          current?thread?nameTianluo-Thread-pool2
          current?thread?nameTianluo-Thread-pool3
          current?thread?nameTianluo-Thread-pool4
          current?thread?nameTianluo-Thread-pool5

          這是因為使用submit提交任務,不會把異常直接這樣拋出來。大家有興趣的話,可以去看看源碼??梢愿臑?code style="font-size:14px;background-color:rgba(27,31,35,.05);font-family:'Operator Mono', Consolas, Monaco, Menlo, monospace;color:rgb(239,112,96);">execute方法執(zhí)行,當然最好就是try...catch捕獲,如下:

                
                /**
          ?*?關注公眾號:撿田螺的小男孩
          ?*/
          public?class?ThreadTest?{

          ????public?static?void?main(String[]?args)?throws?Exception?{
          ????????ThreadPoolExecutor?executorOne?=?new?ThreadPoolExecutor(5,?5,?1,
          ????????????????TimeUnit.MINUTES,?new?ArrayBlockingQueue<Runnable>(20),new?CustomizableThreadFactory("Tianluo-Thread-pool"));
          ????????for?(int?i?=?0;?i?<?5;?i++)?{
          ????????????executorOne.submit(()->{
          ????????????????System.out.println("current?thread?name"?+?Thread.currentThread().getName());
          ????????????????try?{
          ????????????????????Object?object?=?null;
          ????????????????????System.out.print("result##?"?+?object.toString());
          ????????????????}catch?(Exception?e){
          ????????????????????System.out.println("異常了"+e);
          ????????????????}
          ????????????});
          ????????}

          ????}
          }

          其實,我們還可以為工作者線程設置UncaughtExceptionHandler,在uncaughtException方法中處理異常。大家知道這個坑就好啦。

          9. 線程池使用完畢后,忘記關閉

          如果線程池使用完,忘記關閉的話,有可能會導致內(nèi)存泄露問題。所以,大家使用完線程池后,記得關閉一下。同時,線程池最好也設計成單例模式,給它一個好的命名,以方便排查問題。

                
                public?class?ThreadTest?{

          ????public?static?void?main(String[]?args)?throws?Exception?{

          ????????ThreadPoolExecutor?executorOne?=?new?ThreadPoolExecutor(5,?5,?1,
          ????????????????TimeUnit.MINUTES,?new?ArrayBlockingQueue<Runnable>(20),?new?CustomizableThreadFactory("Tianluo-Thread-pool"));
          ????????executorOne.execute(()?->?{
          ????????????System.out.println("關注公眾號:撿田螺的小男孩");
          ????????});

          ????????//關閉線程池
          ????????executorOne.shutdown();
          ????}
          }

          10. ThreadLocal與線程池搭配,線程復用,導致信息錯亂。

          使用ThreadLocal緩存信息,如果配合線程池一起,有可能出現(xiàn)信息錯亂的情況。先看下一下例子:

                
                private?static?final?ThreadLocal<Integer>?currentUser?=?ThreadLocal.withInitial(()?->?null);

          @GetMapping("wrong")
          public?Map?wrong(@RequestParam("userId")?Integer?userId)?{
          ????//設置用戶信息之前先查詢一次ThreadLocal中的用戶信息
          ????String?before??=?Thread.currentThread().getName()?+?":"?+?currentUser.get();
          ????//設置用戶信息到ThreadLocal
          ????currentUser.set(userId);
          ????//設置用戶信息之后再查詢一次ThreadLocal中的用戶信息
          ????String?after??=?Thread.currentThread().getName()?+?":"?+?currentUser.get();
          ????//匯總輸出兩次查詢結(jié)果
          ????Map?result?=?new?HashMap();
          ????result.put("before",?before);
          ????result.put("after",?after);
          ????return?result;
          }

          按理說,每次獲取的before應該都是null,但是呢,程序運行在 Tomcat 中,執(zhí)行程序的線程是Tomcat的工作線程,而Tomcat的工作線程是基于線程池的。

          線程池會重用固定的幾個線程,一旦線程重用,那么很可能首次從 ThreadLocal 獲取的值是之前其他用戶的請求遺留的值。這時,ThreadLocal 中的用戶信息就是其他用戶的信息。

          把tomcat的工作線程設置為1

                
                server.tomcat.max-threads=1

          用戶1,請求過來,會有以下結(jié)果,符合預期:

          177414e8eea6e3f40abeab88eab7bf10.webp

          用戶2請求過來,會有以下結(jié)果,「不符合預期」:

          924b4c5595527f0a71831d9f75410e82.webp

          因此,使用類似 ThreadLocal 工具來存放一些數(shù)據(jù)時,需要特別注意在代碼運行完后,顯式地去清空設置的數(shù)據(jù),正例如下:

                
                @GetMapping("right")
          public?Map?right(@RequestParam("userId")?Integer?userId)?{
          ????String?before??=?Thread.currentThread().getName()?+?":"?+?currentUser.get();
          ????currentUser.set(userId);
          ????try?{
          ????????String?after?=?Thread.currentThread().getName()?+?":"?+?currentUser.get();
          ????????Map?result?=?new?HashMap();
          ????????result.put("before",?before);
          ????????result.put("after",?after);
          ????????return?result;
          ????}?finally?{
          ????????//在finally代碼塊中刪除ThreadLocal中的數(shù)據(jù),確保數(shù)據(jù)不串
          ????????currentUser.remove();
          ????}
          }

          參考與感謝

          [1]

          線程池拒絕策略的坑,不得不防: http://rainbowhorse.site/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E8%B8%A9%E5%9D%91/

          [2]

          Java業(yè)務開發(fā)常見錯誤100例:: https://time.geekbang.org/column/article/220230


          最后


          最近把所有文章都同步帶github開源了,麻煩小伙伴動動手指,給我一個star,非常感謝,愛心f18cf12e8e9396cc4b920eef458bd22d.webp~每一個star對我都很重要。

          GitHub地址:https://github.com/whx123/JavaHome(建議收藏這個地址,后面我會持續(xù)更新)


          推薦閱讀:

          再有人問你什么是分庫分表,直接把這篇文章發(fā)給他

          后端思維篇:如何應用設計模式優(yōu)化代碼

          后端思想篇:設計好接口的36個錦囊!

          兩萬字詳解!InnoDB鎖專題!


          歡迎關注微信公眾號: 互聯(lián)網(wǎng)全棧架構(gòu) ,收取更多有價值的信息。

          瀏覽 70
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  国产日韩在线视频 | 中文字幕操逼网站 | 色欲影视,淫色淫香 | 中国激情网站 | 一级a一级a爰片免费免免水l软件 |