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

          多線程那些事,硬核有趣

          共 7956字,需瀏覽 16分鐘

           ·

          2020-12-12 16:08


          01
          起點(diǎn)



          小白,坐在這間屬于華夏國超一流互聯(lián)網(wǎng)公司企鵝巴巴的小會議室里,等著技術(shù)面試官的到來。


          02
          突如其來的面試



          Round 1


          科學(xué)家路人S:

          小伙子我看你簡歷上什么也沒寫,這次也是第一面,那我們就隨便問點(diǎn)簡單的多線程問題吧。先說說什么是Java的多線程吧,使用多線程有什么好處?有什么壞處?

          小白

          媽媽說專家的話不能信!果然,問個多線程還問好處壞處?我不想用不會用能進(jìn)企鵝巴巴么?

          但是作為打工人,我認(rèn)真的回答道:

          Java的多線程是指程序中包含多個執(zhí)行流,即在一個程序中可以同時運(yùn)行多個不同的線程來執(zhí)行不同的任務(wù)。

          而使用多線程的好處是可以提高 CPU 的利用率。在多線程程序中,一個線程必須等待的時候,CPU 可以運(yùn)行其它的線程而不是等待,這樣就大大提高了程序的效率。也就是說允許單個程序創(chuàng)建多個并行執(zhí)行的線程來完成各自的任務(wù)。

          至于多線程的壞處么,主要有三點(diǎn)。第一點(diǎn)是線程也是程序,所以線程需要占用內(nèi)存,線程越多占用內(nèi)存也越多;第二點(diǎn)是多線程需要協(xié)調(diào)和管理,所以需要 CPU 時間跟蹤線程;最后是線程之間對共享資源的訪問會相互影響,必須解決競用共享資源的問題。


          Round 2


          科學(xué)家路人S繼續(xù)追問:

          你剛才講了“并行”這個詞,那你說說并行和并發(fā)有什么區(qū)別?

          小白

          并發(fā),英文單詞是concurrency,就是多個任務(wù)在同一個 CPU 核上,按細(xì)分的時間片輪流(交替)執(zhí)行,從邏輯上來看那些任務(wù)是同時執(zhí)行。

          并行,英文單詞是parallelism,就是單位時間內(nèi),多個處理器或多核處理器同時處理多個任務(wù),是真正意義上的“同時進(jìn)行”。

          這兩句話,我相信99%的同學(xué)都知道!但是,如果想進(jìn)企鵝巴巴,如果想應(yīng)付P20的科學(xué)家!我就一定要自行的結(jié)合業(yè)務(wù)回答并發(fā)并行的優(yōu)勢!

          現(xiàn)在的系統(tǒng)動不動就要求百萬級甚至千萬級的并發(fā)量,而多線程并發(fā)編程正是開發(fā)高并發(fā)系統(tǒng)的基礎(chǔ),利用好多線程機(jī)制可以大大提高系統(tǒng)整體的并發(fā)能力以及性能。面對復(fù)雜業(yè)務(wù)模型,并行程序會比串行程序更適應(yīng)業(yè)務(wù)需求,而并發(fā)編程更能吻合這種業(yè)務(wù)拆分 。


          Round 3


          路人S和路人B果然都露出了滿意的笑容。

          路人B開始追問道:

          那你說說看,在操作系統(tǒng)中用戶級線程和內(nèi)核級線程是什么?這兩個線程在多核CPU的計算機(jī)上是否都能并行?

          小白

          在操作系統(tǒng)的設(shè)計中,為了防止用戶操作敏感指令而對OS帶來安全隱患,我們把OS分成了用戶空間(user space)和內(nèi)核空間(kernel space)。

          通過用戶空間的庫類實(shí)現(xiàn)的線程,就是用戶級線程(user-level threads,ULT)。這種線程不依賴于操作系統(tǒng)核心,進(jìn)程利用線程庫提供創(chuàng)建、同步、調(diào)度和管理線程的函數(shù)來控制用戶線程。

          說著,我拿了一支筆,畫了這么一張圖:

          在圖里,我們可以清楚的看到,線程表(管理線程的數(shù)據(jù)結(jié)構(gòu))是處于進(jìn)程內(nèi)部的,完全處于用戶空間層面,內(nèi)核空間對此一無所知!當(dāng)然,用戶線程也可以沒有線程表!

          相應(yīng)的,由OS內(nèi)核空間直接掌控的線程,稱為內(nèi)核級線程(kernel-level threads,KLT)。其依賴于操作系統(tǒng)核心,由內(nèi)核的內(nèi)部需求進(jìn)行創(chuàng)建和撤銷。

          接著,我畫下了這張圖:

          同樣的,在圖中,我們看到內(nèi)核線程的線程表(thread table)位于內(nèi)核中,包括了線程控制塊(TCB),一旦線程阻塞,內(nèi)核會從當(dāng)前或者其他進(jìn)程(process)中重新選擇一個線程保證程序的執(zhí)行。

          對于用戶級線程來說,其線程的切換發(fā)生在用戶空間,這樣的線程切換至少比陷入內(nèi)核要快一個數(shù)量級。但是該種線程有個嚴(yán)重的缺點(diǎn):如果一個線程開始運(yùn)行,那么該進(jìn)程中其他線程就不能運(yùn)行,除非第一個線程自動放棄CPU。因?yàn)樵谝粋€單獨(dú)的進(jìn)程內(nèi)部,沒有時鐘中斷,所以不能用輪轉(zhuǎn)調(diào)度(輪流)的方式調(diào)度線程。

          也就是說,同一進(jìn)程中的用戶級線程,在不考慮調(diào)起多個內(nèi)核級線程的基礎(chǔ)上,是沒有辦法利用多核CPU的,其實(shí)質(zhì)是并發(fā)而非并行

          對于內(nèi)核級線程來說,其線程在內(nèi)核中創(chuàng)建和撤銷線程的開銷比較大,需要考慮上下文切換的開銷。

          但是,內(nèi)核級線程是可以利用多核CPU的,即可以并行!

          這回答的累死我了,不過為了能進(jìn)企鵝巴巴,走向人生巔峰,一切都值了!


          Round 4


          路人B點(diǎn)了點(diǎn)頭說:

          嗯,小伙子基礎(chǔ)還是比較牢靠的!那你說說Java里的多線程是用戶級線程還是內(nèi)核級線程呢?

          小白

          是...當(dāng)我要脫口而出的時候,發(fā)現(xiàn)不對,這面試官在套路我!堂堂科學(xué)家,套路還沒入職的孩子么?

          Java里的多線程,既不是用戶級線程,也不是內(nèi)核級線程!

          首先,Java是跨操作平臺的語言,是使用JVM去運(yùn)行編譯文件的。不同的JVM對線程的實(shí)現(xiàn)不同,相同的JVM對不同操作平臺的線程實(shí)現(xiàn)方式也有區(qū)別!

          其次,要講明白程序級別實(shí)現(xiàn)多線程,就必須先說一下多線程模型。

          裂開!怎么感覺這又是一道大題??!B是操作系統(tǒng)的科學(xué)家吧!感覺問的都是很底層的東西了啊,現(xiàn)在程序員內(nèi)卷成這樣了么?實(shí)習(xí)生都問這么底層的問題了?雖然百般不爽,但是為了拿下美女HR,不!是橫掃offer。我要給路人B講明白這個線程模型!

          上面我說過OS上的線程分為ULT和KLT,我們寫程序的代碼只能是在用戶空間里寫代碼!而程序運(yùn)行中,基本上都會進(jìn)入內(nèi)核運(yùn)行,所以我們在實(shí)現(xiàn)程序級別多線程的時候,必須讓ULT映射到KLT上去。在程序級別的多線程設(shè)計里,有以下三種多線程模型。

          多對1模型:在多對一模型中,多個ULT映射到1個KLT上去,此時ULT的進(jìn)程表處于進(jìn)程之中。

          1對1模型:在一對一模型中,1個ULT對應(yīng)1個KLT。自己不在進(jìn)程中創(chuàng)建線程表來管理,幾行代碼之后直接通過系統(tǒng)調(diào)用調(diào)起KLT就能實(shí)現(xiàn)。

          多對多模型:在多對多模型中,N個ULT對應(yīng)小于等于N個的KLT。這種模型結(jié)合了1對1和多對1的優(yōu)點(diǎn),用戶創(chuàng)建線程沒有限制,阻塞內(nèi)核系統(tǒng)的命令不會阻塞整個進(jìn)程。

          最后,就拿最熱門的HotSpot VM來說吧,他在Solaris上就有兩種線程實(shí)現(xiàn)方式,可以讓用戶選擇一對一或多對多這兩種模型;而在Windows和Linux下,使用的都是一對一的多線程模型,Java的線程通過一一映射到Light Weight Process(輕量級進(jìn)程,LWP)從而實(shí)現(xiàn)了和KLT的一一對應(yīng)。


          Round 5


          路人B聽到這個回答,眼睛都亮了!直接追問道:

          ULT如何映射到KLT?怎么調(diào)起的?

          小白

          ULT在執(zhí)行的過程中,如果執(zhí)行的指令需要進(jìn)入內(nèi)核態(tài),則ULT會通過系統(tǒng)調(diào)用調(diào)起一個KLT!

          所謂系統(tǒng)調(diào)度,就是在OS中分割用戶空間和內(nèi)核空間的API。


          Round 6


          路人B繼續(xù)追問道:

          ULT的執(zhí)行過程中可以不調(diào)起KLT么?舉個例子。

          小白

          可以不調(diào)起,比如ULT中就只有sleep這個指令,就不會進(jìn)入內(nèi)核態(tài)執(zhí)行,更不會調(diào)起KLT。

          問到這里,我有點(diǎn)吐血了都!看著B對我的回答很滿意,我心中卻把B已經(jīng)問候了一百遍!


          Round 7


          路人S總算接過了話題:

          看來同學(xué)對于底層的知識理解還湊合,那你有沒有看過HotSpot的源碼?能不能簡單說說看Java的線程是怎么運(yùn)行的?

          小白

          這問的還上癮了?P20的問題咋這么“簡單”呢!說實(shí)話,自從前幾天發(fā)生了靈異事件之后,我確實(shí)技術(shù)突飛猛進(jìn),這個源代碼我好像還真的瞄了一眼,不過我不能暴露自己擁有金手指的秘密啊!

          于是我撓了撓頭,思考了1分鐘,然后說道:

          源碼以前看過,只能記得一個大概。

          1、在Java中,使用java.lang.Thread的構(gòu)造方法來構(gòu)建一個java.lang.Thread對象,此時只是對這個對象的部分字段(例如線程名,優(yōu)先級等)進(jìn)行初始化;

          2、調(diào)用java.lang.Thread對象的start()方法,開始此線程。此時,在start()方法內(nèi)部,調(diào)用start0() 本地方法來開始此線程;

          3、start0()在VM中對應(yīng)的是JVM_StartThread,也就是,在VM中,實(shí)際運(yùn)行的是JVM_StartThread方法(宏),在這個方法中,創(chuàng)建了一個JavaThread對象;

          4、在JavaThread對象的創(chuàng)建過程中,會根據(jù)運(yùn)行平臺創(chuàng)建一個對應(yīng)的OSThread對象,且JavaThread保持這個OSThread對象的引用;

          5、在OSThread對象的創(chuàng)建過程中,創(chuàng)建一個平臺相關(guān)的底層級線程,如果這個底層級線程失敗,那么就拋出異常;

          6、在正常情況下,這個底層級的線程開始運(yùn)行,并執(zhí)行java.lang.Thread對象的run方法;

          7、當(dāng)java.lang.Thread生成的Object的run()方法執(zhí)行完畢返回后,或者拋出異常終止后,終止native thread;

          8、最后就是釋放相關(guān)的資源(包括內(nèi)存、鎖等)

          大概就是以上這么個步驟吧。

          回答完這個,我要跪謝我的金手指了!我看見路人S在電腦上敲著什么,估計他也比較懵,沒想到我居然能答得上來吧!


          Round 8


          路人S對此不置可否,說道:

          那你說說什么是上下文切換吧。

          小白

          多線程編程中一般線程的個數(shù)都大于 CPU 核心的個數(shù),而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執(zhí)行,CPU 采取的策略是為每個線程分配時間片并輪轉(zhuǎn)的形式。

          時間片是CPU分配給各個線程的時間,因?yàn)闀r間非常短,所以CPU不斷通過切換線程,讓我們覺得多個線程是同時執(zhí)行的,時間片一般是幾十毫秒。

          當(dāng)一個線程的時間片用完的時候就會重新處于就緒狀態(tài)讓給其他線程使用,這個過程就屬于一次上下文切換。

          概括來說就是:當(dāng)前任務(wù)在執(zhí)行完 CPU 時間片切換到另一個任務(wù)之前會先保存自己的狀態(tài),以便下次再切換回這個任務(wù)時,可以再加載這個任務(wù)的狀態(tài)。任務(wù)從保存到再加載的過程就是一次上下文切換


          Round 9


          路人S繼續(xù)問道:

          頻繁切換上下文會有什么問題?

          小白

          上下文切換通常是計算密集型的,每次切換時,需要保存當(dāng)前的狀態(tài)起來,以便能夠進(jìn)行恢復(fù)先前狀態(tài),而這個切換時非常損耗性能。

          也就是說,它需要相當(dāng)可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統(tǒng)來說意味著消耗大量的 CPU 時間,事實(shí)上,可能是操作系統(tǒng)中時間消耗最大的操作。

          Linux 相比與其他操作系統(tǒng)(包括其他類 Unix 系統(tǒng))有很多的優(yōu)點(diǎn),其中有一項(xiàng)就是,其上下文切換和模式切換的時間消耗非常少。


          Round 10


          S繼續(xù)問:

          減少上下文切換的方式有哪些?

          小白

          通常減少上下文切換的方式有:

          1、無鎖并發(fā)編程:可以參照concurrentHashMap鎖分段的思想,不同的線程處理不同段的數(shù)據(jù),這樣在多線程競爭的條件下,可以減少上下文切換的時間。

          2、CAS算法:利用Atomic下使用CAS算法來更新數(shù)據(jù),使用了樂觀鎖,可以有效的減少一部分不必要的鎖競爭帶來的上下文切換。

          3、使用最少線程:避免創(chuàng)建不需要的線程,比如任務(wù)很少,但是創(chuàng)建了很多的線程,這樣會造成大量的線程都處于等待狀態(tài)。

          4、協(xié)程:在單線程里實(shí)現(xiàn)多任務(wù)的調(diào)度,并在單線程里維持多個任務(wù)間的切換。


          Round 11


          路人B聽了,眼睛一亮,立刻追問道:

          協(xié)程是什么?和用戶線程有什么區(qū)別?

          小白

          我聽了真想抽自己幾個嘴巴子,怎么又來了!B是只會OS吧!

          協(xié)程的英文單詞是Coroutine,這是一個程序組件,它既不是線程也不是進(jìn)程。它的執(zhí)行過程更類似于一個方法,或者說不帶返回值的函數(shù)調(diào)用。

          我看到過stack overflow和很多博客里,都認(rèn)為這兩者是一個東西。但是,在我的理解中,這兩者還是有區(qū)別的。

          不可否認(rèn)的是,協(xié)程和ULT做的是同一個事情。所以從某種角度上講,他們確實(shí)是等價的!

          但是,ULT這個概念被提出的時候,其背后的思想本質(zhì)是講ULT是個本機(jī)線程,也就是使用了OS的用戶空間內(nèi)提供的庫類直接創(chuàng)建的線程。這個時候,你不需要在OS上面添加一些其他第三方的庫類。

          而協(xié)程這個概念是康威定律的提出者M(jìn)elvin Edward Conway在1958年提出的一個概念,其背后的思想是不直接使用OS本身的庫類,自己做一些庫類去實(shí)現(xiàn)并發(fā)。在那個年代,OS上面的第三方庫類并不像現(xiàn)在這么流行,OS本身的庫類和其他第三方庫類的結(jié)合也并不像今天這么容易。所以協(xié)程并不是本機(jī)線程,他是需要借助一些其他不屬于OS的第三方庫類調(diào)用OS用戶空間的庫類來實(shí)現(xiàn)達(dá)到ULT的效果。

          當(dāng)然,這個概念在今天來看,就會顯得很讓人混淆了。因?yàn)榈降啄男祛愃闶荗S本機(jī)的庫類,哪些算是第三方庫類?這和1960年的時候已經(jīng)有絕大的區(qū)別了!所以大家認(rèn)為這兩者是一個東西,其實(shí)也不能說他說的不對,只能說可能對這個思想本身背后代表的東西不明白。


          Round 12


          路人B聽了,立刻坐直了身體,繼續(xù)追問道:

          那你知道fiber么?這個和上面兩個名詞有什么區(qū)別?

          小白

          fiber也是一種本機(jī)線程,其本質(zhì)是一種特殊的ULT,即更輕量級的ULT。說白了就是這種ULT的線程表一定存于進(jìn)程之中

          而我們在構(gòu)建一對一多線程模型的時候,ULT的線程表其實(shí)還是交給內(nèi)核了!這是兩者之間最直接的差別。所以我們經(jīng)常稱fiber就是協(xié)同調(diào)度的ULT,在win32中可以調(diào)用fiber來構(gòu)建多對多的多線程模型。

          其實(shí),fiber、coroutine和ULT在用戶層面能看到的效果是基本等價的。

          其中ULT是描述OS庫本身提供的功能;fiber描述的是OS提供的協(xié)同調(diào)度的ULT;coroutine描述的是第三方實(shí)現(xiàn)的并發(fā)并行功能。

          這些名詞很多都是歷史原因的問題,同時也是深入研究需要了解的事情,我們普通程序員在使用的時候,更多的關(guān)心的是應(yīng)用層方面的東西。而這些名詞的理解已經(jīng)深入到源碼層了。


          Round 13


          路人S估計被我秀的腦殼痛了,立刻說道:

          還是講講看在 Java 程序中怎么保證多線程的運(yùn)行安全吧。

          小白

          Java的線程安全在三個方面體現(xiàn):

          原子性:提供互斥訪問,同一時刻只能有一個線程對數(shù)據(jù)進(jìn)行操作,在Java中使用了atomic和synchronized這兩個關(guān)鍵字來確保原子性;

          可見性:一個線程對主內(nèi)存的修改可以及時地被其他線程看到,在Java中使用了synchronized和volatile這兩個關(guān)鍵字確保可見性;

          有序性:一個線程觀察其他線程中的指令執(zhí)行順序,由于指令重排序,該觀察結(jié)果一般雜亂無序,在Java中使用了happens-before原則來確保有序性。


          Round 14


          路人S繼續(xù)問道:

          你剛才講了有序性,那你說說代碼為什么會重排序?

          小白

          在執(zhí)行程序時,為了提高性能,處理器和編譯器常常會對指令進(jìn)行重排序。


          Round 15


          路人S繼續(xù)追問:

          重排序是想怎么重排就重排么?

          小白

          這面試官也很難纏啊,怎么一直在追問,是需要我給他孝敬一根華子么?要不是看著旁邊有個美女HR,我早就孝敬S他老人家了!

          當(dāng)然不是!不能隨意重排序,不是你想怎么排序就怎么排序,它需要滿足以下兩個條件:

          1、在單線程環(huán)境下不能改變程序運(yùn)行的結(jié)果;

          2、存在數(shù)據(jù)依賴關(guān)系的不允許重排序。

          所以重排序不會對單線程有影響,只會破壞多線程的執(zhí)行語義。


          Round 16


          路人S繼續(xù)追問道:

          那你講講看在Java中如何保障重排序不影響單線程的吧。

          小白

          保障這一結(jié)果是因?yàn)樵诰幾g器,runtime 和處理器都必須遵守as-if-serial語義規(guī)則。

          為了遵守as-if-serial語義,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序,因?yàn)檫@種重排序會改變執(zhí)行結(jié)果。但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系,這些操作可能被編譯器和處理器重排序。

          我來舉個例子吧

          說著我拿著筆在紙上寫了三行簡單的代碼:

          我們看這個例子,A和C之間存在數(shù)據(jù)依賴關(guān)系,同時B和C之間也存在數(shù)據(jù)依賴關(guān)系。因此在最終執(zhí)行的指令序列中,C不能被重排序到A和B的前面,如果C排到A和B的前面,那么程序的結(jié)果將會被改變。但A和B之間沒有數(shù)據(jù)依賴關(guān)系,編譯器和處理器可以重排序A和B之間的執(zhí)行順序。

          這就是as-if-serial語義。


          Round 17


          路人S繼續(xù)問道:

          那你說說看你剛才講的happens-before原則吧。

          小白

          happens-before說白了就是誰在誰前面發(fā)生的一個關(guān)系。

          HB規(guī)則是Java內(nèi)存模型(JMM)向程序員提供的跨線程內(nèi)存可見性保證。

          說的直白一點(diǎn),就是如果A線程的寫操作a與B線程的讀操作b之間存在happens-before關(guān)系,盡管a操作和b操作在不同的線程中執(zhí)行,但JMM向程序員保證a操作將對b操作可見。

          具體的定義為:

          1、如果一個操作happens-before另一個操作,那么第一個操作的執(zhí)行結(jié)果將對第二個操作可見,而且第一個操作的執(zhí)行順序排在第二個操作之前。

          2、兩個操作之間存在happens-before關(guān)系,并不意味著Java平臺的具體實(shí)現(xiàn)必須要按照happens-before關(guān)系指定的順序來執(zhí)行。如果重排序之后的執(zhí)行結(jié)果,與按happens-before關(guān)系來執(zhí)行的結(jié)果一致,那么這種重排序并不非法。

          具體的規(guī)則有8條:

          1、程序順序規(guī)則:一個線程中的每個操作,happens-before于該線程中的任意后續(xù)操作。

          2、監(jiān)視器鎖規(guī)則:對一個鎖的解鎖,happens-before于隨后對這個鎖的加鎖。

          3、volatile變量規(guī)則:對一個volatile域的寫,happens-before于任意后續(xù)對這個volatile域的讀。

          4、傳遞性:如果A happens-before B,且B happens-before C,那么A happens-before C。

          5、start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作。

          6、Join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。

          7、程序中斷規(guī)則:對線程interrupted()方法的調(diào)用先行于被中斷線程的代碼檢測到中斷時間的發(fā)生。

          8、對象finalize規(guī)則:一個對象的初始化完成(構(gòu)造函數(shù)執(zhí)行結(jié)束)先行于發(fā)生它的finalize()方法的開始。


          Round 18


          路人S接著追問:

          你剛才說HB規(guī)則不代表最終的執(zhí)行順序,能不能舉個例子。

          小白

          就拿講as-if-serial提到的例子舉例吧,例子很簡單就是面積=寬*高。

          利用HB的程序順序規(guī)則,存在三個happens-before關(guān)系:

          1、A happens-before B;

          2、B happens-before C;

          3、A happens-before C。

          這里的第三個關(guān)系是利用傳遞性進(jìn)行推論的。這里的第三個關(guān)系是利用傳遞性進(jìn)行推論的。

          A happens-before B,定義1要求A執(zhí)行結(jié)果對B可見,并且A操作的執(zhí)行順序在B操作之前;但與此同時利用HB定義中的第二條,A、B操作彼此不存在數(shù)據(jù)依賴性,兩個操作的執(zhí)行順序?qū)ψ罱K結(jié)果都不會產(chǎn)生影響。

          在不改變最終結(jié)果的前提下,允許A,B兩個操作重排序,即happens-before關(guān)系并不代表了最終的執(zhí)行順序。




          哈嘍,我是小林,就愛圖解計算機(jī)基礎(chǔ),如果覺得文章對你有幫助,歡迎分享給你的朋友,也給小林點(diǎn)個「在看」,這對小林非常重要,謝謝你們,給各位小姐姐小哥哥們抱拳了,我們下次見!


          推薦閱讀

          你不好奇 Linux 是如何收發(fā)網(wǎng)絡(luò)包的?

          小小的 float,藏著大大的學(xué)問

          瀏覽 58
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <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>
                  亚洲少妇xxxx | 日本成人黄色网址 | 国产在线视频一区二区三区 | 日韩欧美在线专区 | 成年人性爱网站 |