阻塞隊(duì)列和生產(chǎn)者-消費(fèi)者模式 Java并發(fā)編程實(shí)戰(zhàn)總結(jié)

??????

??阻塞隊(duì)列提供了可阻塞的put 和take方法, 以及支持定時(shí)的offer和poll方法。如果隊(duì)列已經(jīng)滿了, 那么put方法將阻塞直到有空間可用图毕;如果隊(duì)列為空, 那么take方法將會阻塞直到有元素可用间螟。隊(duì)列可以是有界的也可以是無界的吴旋, 無界隊(duì)列永遠(yuǎn)都不會充滿, 因此無界隊(duì)列上的put方法也永遠(yuǎn)不會阻塞厢破。

????????阻塞隊(duì)列支持生產(chǎn)者-消費(fèi)者這種設(shè)計(jì)模式荣瑟。該模式將“找出需要完成的工作” 與“執(zhí)行工作” 這兩個(gè)過程分離開來, 并把工作項(xiàng)放入一個(gè)“ 待完成” 列表中以便在隨后處理摩泪, 而不是找出后立即處理笆焰。生產(chǎn)者- 消費(fèi)者模式能簡化開發(fā)過程, 因?yàn)樗松a(chǎn)者類和消費(fèi)者類之間的代碼依賴性见坑, 此外嚷掠, 該模式還將生產(chǎn)數(shù)據(jù)的過程與使用數(shù)據(jù)的過程解耦開來以簡化工作負(fù)載的管理, 因?yàn)檫@兩個(gè)過程在處理數(shù)據(jù)的速率上有聽不同荞驴。

????????在基于阻塞隊(duì)列構(gòu)建的生產(chǎn)者-消費(fèi)者設(shè)計(jì)中不皆, 當(dāng)數(shù)據(jù)生成時(shí), 生產(chǎn)者把數(shù)據(jù)放人隊(duì)列熊楼,而當(dāng)消費(fèi)者準(zhǔn)備處理數(shù)據(jù)時(shí)霹娄, 將從隊(duì)列中獲取數(shù)據(jù)。生產(chǎn)者不需要知道消費(fèi)者的標(biāo)識或數(shù)量,或者它們是否是唯一的生產(chǎn)者犬耻, 而只需將數(shù)據(jù)放入隊(duì)列即可踩晶。同樣, 消費(fèi)者也不需要知道生產(chǎn)者是誰枕磁, 或者工作來自何處渡蜻。BlockingQueue 簡化了生產(chǎn)者- 消費(fèi)者設(shè)計(jì)的實(shí)現(xiàn)過程, 它支持任意數(shù)批的生產(chǎn)者和消費(fèi)者计济。一種最常見的生產(chǎn)者- 消費(fèi)者設(shè)計(jì)模式就是線程池與工作隊(duì)列的組合茸苇, 在Executor任務(wù)執(zhí)行框架中就體現(xiàn)了這種模式。

????????以兩個(gè)人洗盤子為例峭咒, 二者的勞動分工也是一種生產(chǎn)者一消費(fèi)者模式:其中一個(gè)人把洗好的盤子放在盤架上税弃, 而另一個(gè)人從盤架上取出盤子并把它們烘干纪岁。在這個(gè)示例中凑队, 盤架相當(dāng)于阻塞隊(duì)列。如果盤架上沒有盤子幔翰, 那么消費(fèi)者會一直等待漩氨, 直到有盤子需要烘干。如果盤架放滿了遗增, 那么生產(chǎn)者會停止清洗直到盤架上有更多的空間叫惊。我們可以將這種類比擴(kuò)展為多個(gè)生產(chǎn)(雖然可能存在對水槽的競爭)和多個(gè)消費(fèi)者, 每個(gè)工人只需與盤架打交道做修。人們不需要知道究競有多少生產(chǎn)者或消費(fèi)者霍狰, 或者誰生產(chǎn)了某個(gè)指定的工作項(xiàng)。

????????“生產(chǎn)者” 和“ 消費(fèi)者” 的角色是相對的饰及, 某種環(huán)境中的消費(fèi)者在另一種不同的環(huán)境中可能會成為生產(chǎn)者蔗坯。烘干盤子的工人將“ 消費(fèi)” 洗干凈的濕盤子, 而產(chǎn)生烘干的盤子燎含。第三個(gè)人把洗干凈的盤子整理好宾濒,在這種情況中, 烘干盤子的工人既是消費(fèi)者屏箍,也是生產(chǎn)者绘梦, 從而就有了兩個(gè)共享的工作隊(duì)列(每個(gè)隊(duì)列都可能阻塞烘干工作的運(yùn)行)。

????????阻塞隊(duì)列簡化了消費(fèi)者程序的編碼赴魁, 因?yàn)?take操作會一直阻塞直到有可用的數(shù)據(jù)卸奉。如果 生產(chǎn)者不能盡快地產(chǎn)生工作項(xiàng)使消費(fèi)者保持忙碌, 那么消費(fèi)者就只能一直等待颖御, 直到有工作可做榄棵。在某些情況下, 這種方式是非常合適的(例如, 在服務(wù)器應(yīng)用程序中秉继,沒有任何客戶請求 服務(wù))祈噪, 而在其他一些情況下, 這也表示需要調(diào)整生產(chǎn)者線程數(shù)量和消費(fèi)者線程數(shù)量之間的比率尚辑,從而實(shí)現(xiàn)更高的資源利用率(例如辑鲤,在 “ 網(wǎng)頁爬蟲[Web Crawler]"或其他應(yīng)用程序中,有無窮的工作需要完成)杠茬。

????????如果生產(chǎn)者生成工作的速率比消費(fèi)者處理工作的速率快月褥, 那么工作項(xiàng)會在隊(duì)列中累積起 , 最終耗盡內(nèi)存瓢喉。 同樣宁赤, put方法的阻塞特性也極大地簡化了生產(chǎn)者的編碼。 如果使用有界 隊(duì)列栓票, 那么當(dāng)隊(duì)列充滿時(shí)决左, 生產(chǎn)者將阻塞并且不能繼續(xù)生成工作, 而消費(fèi)者就有時(shí)間來趕上工 作處理進(jìn)度走贪。

????????阻塞隊(duì)列同樣提供了一個(gè) offer方法佛猛,如果數(shù)據(jù)項(xiàng)不能被添加到隊(duì)列中, 那么將返回一個(gè)失敗狀態(tài)坠狡。 這樣你就能夠創(chuàng)建更多靈活的策略來處理負(fù)荷過載的情況继找,例如減輕負(fù)載, 將多余 的工作項(xiàng)序列化并寫入磁盤逃沿,減少生產(chǎn)者線程的數(shù)量婴渡,或者通過某種方式來抑制生產(chǎn)者線程。

? ??????在構(gòu)建高可靠的應(yīng)用程序時(shí)凯亮,有界隊(duì)列是一種強(qiáng)大的資源管理工具:它們能抑制并防止產(chǎn)生過多的工作項(xiàng)边臼,使應(yīng)用程序在負(fù)荷過載的情況下

? ??????雖然生產(chǎn)者-消費(fèi)者模式能夠?qū)⑸a(chǎn)者和消費(fèi)者的代碼彼此解耦開來, 但它們的行為仍然會通過共享工作隊(duì)列間接地耦合在一起触幼。 開發(fā)人員總會假設(shè)消費(fèi)者處理工作的速率能趕上生產(chǎn)者生成工作項(xiàng)的速率硼瓣, 因此通常不會為工作隊(duì)列的大小設(shè)置邊界, 但這將導(dǎo)致在之后需要重新設(shè)計(jì)系統(tǒng)架構(gòu)置谦。因此堂鲤,應(yīng)該盡早地通過阻塞隊(duì)列在設(shè)計(jì)中構(gòu)建資源管理機(jī)制-這件事清做得越早, 就越容易媒峡。在許多情況下瘟栖, 阻塞隊(duì)列能使這項(xiàng)工作更加簡單,如果阻塞隊(duì)列并不完全符合設(shè)計(jì)需求谅阿, 那么還可以通過信號址(Semaphore)來創(chuàng)建其他的阻塞數(shù)據(jù)結(jié)構(gòu)半哟。

????????在類庫中包含了BlockingQueue的多種實(shí)現(xiàn)酬滤,其中,LinkedBlockingQueue和ArrayBlocking-Queue是FIFO隊(duì)列寓涨,二者分別與LinkedList和ArrayList類似盯串,但比同步List擁有更好的并發(fā)性能。PriorityBlockingQueue是一個(gè)按優(yōu)先級排序的隊(duì)列戒良,當(dāng)你希望按照某種順序而不是FIFO來處理元素時(shí)体捏,這個(gè)隊(duì)列將非常有用。正如其他有序的容器一樣糯崎,PriorityBlockingQueue既可以根據(jù)元素的自然順序來比較元素(如果它們實(shí)現(xiàn)了Comparable方法)几缭,也可以使用Comparator來比較。

????????最后一個(gè)BlockingQueue實(shí)現(xiàn)是SynchronousQueue, 實(shí)際上它不是一個(gè)真正的隊(duì)列沃呢, 因?yàn)樗粫殛?duì)列中元素維護(hù)存儲空間年栓。與其他隊(duì)列不同的是, 它維護(hù)一組線程薄霜, 這些線程在等待著把元素加入或移出隊(duì)列某抓。如果以洗盤子的比喻為例, 那么這就相當(dāng)千沒有盤架黄锤, 而是將洗好的盤子直接放入下一個(gè)空閑的烘干機(jī)中搪缨。這種實(shí)現(xiàn)隊(duì)列的方式看似很奇怪食拜, 但由于可以直接交付工作鸵熟,從而降低了將數(shù)據(jù)從生產(chǎn)者移動到消費(fèi)者的延遲。(在傳統(tǒng)的隊(duì)列中负甸, 在一個(gè)工作單元可以交付之前流强, 必須通過串行方式首先完成入列[Enqueue]或者出列[Dequeue]等操作。) 直接交付方式還會將更多關(guān)于任務(wù)狀態(tài)的信息反饋給生產(chǎn)者呻待。當(dāng)交付被接受時(shí)打月, 它就知道消費(fèi)者已經(jīng)得到了任務(wù), 而不是簡單地把任務(wù)放入一個(gè)隊(duì)列一一這種區(qū)別就好比將文件直接交給同事蚕捉, 還是將文件放到她的郵箱中并希望她能盡快拿到文件奏篙。因?yàn)镾ynchronousQueue 沒有存儲功能,因此put 和take 會一直阻塞迫淹,直到有另一個(gè)線程已經(jīng)準(zhǔn)備好參與到交付過程中秘通。僅當(dāng)有足夠多的消費(fèi)者,并且總是有一個(gè)消費(fèi)者準(zhǔn)備好獲取交付的工作時(shí)敛熬, 才適合使用同步隊(duì)列肺稀。

串行線程封閉

????????在java.util.coricurrent中實(shí)現(xiàn)的各種阻塞隊(duì)列都包含了足夠的內(nèi)部同步機(jī)制, 從而安全地將對象從生產(chǎn)者線程發(fā)布到消費(fèi)者線程应民。

????????對于可變對象话原, 生產(chǎn)者 - 消費(fèi)者這種設(shè)計(jì)與阻塞隊(duì)列一起夕吻, 促進(jìn)了串行線程封閉, 從而將對象所有權(quán)從生產(chǎn)者交付給消費(fèi)者繁仁。 線程封閉對象只能由單個(gè)線程擁有涉馅, 但可以通過安全地發(fā)布該對象來 “轉(zhuǎn)移 ” 所有權(quán)。在轉(zhuǎn)移所有權(quán)后黄虱, 也只有另一個(gè)線程能獲得這個(gè)對象的訪問權(quán)限控漠,并且發(fā)布對象的線程不會再訪問它。這種安全的發(fā)布確保了對象狀態(tài)對于新的所有者來說是可見的悬钳, 并且由于最初的所有者不會再訪問它盐捷, 因此對象將被封閉在新的線程中。 新的所有者線程可以對該對象做任意修改默勾, 因?yàn)樗哂歇?dú)占的訪問權(quán)碉渡。

????????對象池利用了串行線程封閉, 將對象 “借給“一個(gè)請求線程母剥。 只要對象池包含足夠的內(nèi)部同步來安全地發(fā)布池中的對象滞诺, 并且只要客戶代碼本身不會發(fā)布池中的對象, 或者在將對象返回給對象池后就不再使用它环疼, 那么就可以安全地在線程之間傳遞所有權(quán)习霹。

????????我們也可以使用其他發(fā)布機(jī)制來傳遞可變對象的所有權(quán), 但必須確保只有一個(gè)線程能接受被轉(zhuǎn)移的對象炫隶。 阻塞隊(duì)列簡化了這項(xiàng)工作淋叶。 除此之外, 還可以通過 ConcurrentMap 的原子方法remove 或者 AtomicReference 的原子方法 compareAndSet 來完成這項(xiàng)工作伪阶。

雙端隊(duì)列與工作密取

????????Java 6 增加了兩種容器類型煞檩, Deque (發(fā)音為 "deck") 和 BIockingDeque, 它們分別對 Queue 和 BlockingQueue 進(jìn)行了擴(kuò)展。 Deque 是一個(gè)雙端隊(duì)列栅贴, 實(shí)現(xiàn)了在隊(duì)列頭和隊(duì)列尾的高效插入和移除斟湃。 具體實(shí)現(xiàn)包括 ArrayDeque 和 LinkedBlockingDeque。

????????正如阻塞隊(duì)列適用于生產(chǎn)者 - 消費(fèi)者模式檐薯, 雙端隊(duì)列同樣適用于另一種相關(guān)模式凝赛, 即工作密取 (Work Stealing)。 在生產(chǎn)者-消費(fèi)者設(shè)計(jì)中坛缕,所有消費(fèi)者有一個(gè)共享的工作隊(duì)列墓猎, 而在 工作密取設(shè)計(jì)中, 每個(gè)消費(fèi)者都有各自的雙端隊(duì)列祷膳。 如果一個(gè)消費(fèi)者完成了自己雙端隊(duì)列中的 全部工作陶衅, 那么它可以從其他消費(fèi)者雙端隊(duì)列末尾秘密地獲取工作。 密取工作模式比傳統(tǒng)的生產(chǎn)者-消費(fèi)者模式具有更高的可伸縮性直晨, 這是因?yàn)楣ぷ髡呔€程不會在單個(gè)共享的任務(wù)隊(duì)列上發(fā) 生競爭搀军。 在大多數(shù)時(shí)候膨俐, 它們都只是訪問自己的雙端隊(duì)列, 從而極大地減少了競爭罩句。 當(dāng)工作者線程需要訪問另一個(gè)隊(duì)列時(shí)焚刺, 它會從隊(duì)列的尾部而不是從頭部獲取工作, 因此進(jìn)一步降低了隊(duì)列上的競爭程度门烂。

????????工作密取非常適用于既是消費(fèi)者也是生產(chǎn)者問題——當(dāng)執(zhí)行某個(gè)工作時(shí)可能導(dǎo)致出現(xiàn)更多的工作乳愉。 例如, 在網(wǎng)頁爬蟲程序中處理一個(gè)頁面時(shí)屯远, 通常會發(fā)現(xiàn)有更多的頁面需要處理蔓姚。 類似的還有許多搜索圖的算法, 例如在垃圾回收階段對堆進(jìn)行標(biāo)記慨丐, 都可以通過工作密取機(jī)制來實(shí) 現(xiàn)高效并行坡脐。 當(dāng)一個(gè)工作線程找到新的任務(wù)單元時(shí), 它會將其放到自己隊(duì)列的末尾(或者在工作共享設(shè)計(jì)模式中房揭, 放入其他工作者線程的隊(duì)列中)备闲。 當(dāng)雙端隊(duì)列為空時(shí), 它會在另一個(gè)線程的隊(duì)列隊(duì)尾查找新的任務(wù)捅暴, 從而確保每個(gè)線程都保持忙碌狀態(tài)恬砂。


阻塞方法與中斷方法

????????線程可能會阻塞或暫停執(zhí)行, 原因有多種:等待I/O操作結(jié)束蓬痒, 等待獲得一個(gè)鎖泻骤, 等待從Thread.sleep 方法中醒來, 或是等待另一個(gè)線程的計(jì)算結(jié)果乳幸。 當(dāng)線程阻塞時(shí)瞪讼, 它通常被掛起,并處于某種阻塞狀態(tài) (BLOCKED粹断、 WAITING或 TIMED_WAITING)。阻塞操作與執(zhí)行時(shí)間很長的普通操作的差別在于嫡霞,被阻塞的線程必須等待某個(gè)不受它控制的事件發(fā)生后才能繼續(xù)執(zhí)行瓶埋, 例如等待 I/0 操作完成, 等待某個(gè)鎖變成可用诊沪, 或者等待外部計(jì)算的結(jié)束养筒。 當(dāng)某個(gè)外部事件發(fā)生時(shí), 線程被置回 RUNNABLE狀態(tài)端姚,并可以再次被調(diào)度執(zhí)行晕粪。

????????BlockingQueue 的 put 和 take 等方法會拋出受檢查異常 (Checked Exception) Interrupted-Exception, 這與類庫中其他一些方法的做法相同,例如 Thread.sleep渐裸。當(dāng)某方法拋出 Interrupted-Exception 時(shí)巫湘, 表示該方法是一個(gè)阻塞方法装悲, 如果這個(gè)方法被中斷, 那么它將努力提前結(jié)束阻塞狀態(tài)尚氛。

????????Thread提供了interrupt方法诀诊,用于中斷線程或者查詢線程是否已經(jīng)被中斷。每個(gè)線程都有一個(gè)布爾類型的屬性阅嘶, 表示線程的中斷狀態(tài)属瓣, 當(dāng)中斷線程時(shí)將設(shè)置這個(gè)狀態(tài)。

????????中斷是一種協(xié)作機(jī)制讯柔。一個(gè)線程不能強(qiáng)制其他線程停止正在執(zhí)行的操作而去執(zhí)行其他的操作抡蛙。當(dāng)線程A中斷B時(shí), A僅僅是要求B在執(zhí)行到某個(gè)可以暫停的地方停止正在執(zhí)行的操作-前提是如果線程B愿意停止下來魂迄。雖然在API或者語言規(guī)范中并沒有為中斷定義任何特定應(yīng)用級別的語義溜畅, 但最常使用中斷的情況就是取消某個(gè)操作。方法對中斷請求的響應(yīng)度越高极祸, 就越容易及時(shí)取消那些執(zhí)行時(shí)間很長的操作慈格。

????????當(dāng)在代碼中調(diào)用了一個(gè)將拋出InterruptedException 異常的方法時(shí), 你自己的方法也就變成了一個(gè)阻塞方法遥金, 并且必須要處理對中斷的響應(yīng)浴捆。對于庫代碼來說, 有兩種基本選擇:

????????傳遞lnterruptedException稿械。避開這個(gè)異常通常是最明智的策略——只需把InterruptedException 傳遞給方法的調(diào)用者选泻。傳遞lnterruptedException 的方法包括, 根本不捕獲該異常美莫, 或者捕獲該異常页眯, 然后在執(zhí)行某種簡單的清理工作后再次拋出這個(gè)異常。

????????恢復(fù)中斷有時(shí)候不能拋出lnterruptedException, 例如當(dāng)代碼是Runnable 的一部分時(shí)厢呵。在這些情況下窝撵, 必須捕獲InterruptedException, 并通過調(diào)用當(dāng)前線程上的interrupt 方法恢復(fù)中斷狀態(tài), 這樣在調(diào)用棧中更高層的代碼將看到引發(fā)了一個(gè)中斷襟铭。

? ??????還可以采用一些更復(fù)雜的中斷處理方法碌奉, 但上述兩種方法已經(jīng)可以應(yīng)付大多數(shù)情況了。然而在出現(xiàn)InterruptedException 時(shí)不應(yīng)該做的事情是寒砖, 捕獲它但不做出任何響應(yīng)赐劣。這將使調(diào)用棧上更高層的代碼無法對中斷采取處理措施, 因?yàn)榫€程被中斷的證據(jù)已經(jīng)丟失哩都。只有在一種特殊的情況中才能屏蔽中斷魁兼, 即對Thread 進(jìn)行擴(kuò)展, 井且能控制調(diào)用棧上所有更高層的代碼漠嵌。


同步工具類

?????????在容器類中咐汞,阻塞隊(duì)列是一種獨(dú)特的類:它們不僅能作為保存對象的容器盖呼, 還能協(xié)調(diào)生產(chǎn)者和消費(fèi)者等線程之間的控制流, 因?yàn)閠ake 和put 等方法將阻塞碉考, 直到隊(duì)列達(dá)到期望的狀態(tài)(隊(duì)列既非空塌计, 也非滿)。

????????同步工具類可以是任何一個(gè)對象侯谁,只要它根據(jù)其自身的狀態(tài)來協(xié)調(diào)線程的控制流锌仅。 阻塞隊(duì)列可以作為同步工具類,其他類型的同步工具類還包括信號量(Semaphore)墙贱、 柵欄(Barrier) 以及閉鎖(Latch)热芹。在平臺類庫中還包含其他一些同步工具類的類, 如果這些類還無法滿足需要,那么可以按照第14章中給出的機(jī)制來創(chuàng)建自己的同步工具類。

????????所有的同步工具類都包含一些特定的結(jié)構(gòu)化屬性:它們封裝了一些狀態(tài)碗短, 這些狀態(tài)將決定執(zhí)行同步工具類的線程是繼續(xù)執(zhí)行還是等待,此外還提供了一些方法對狀態(tài)進(jìn)行操作报腔,以及另 一些方法用于高效地等待同步工具類進(jìn)入到預(yù)期狀態(tài)。

閉鎖

????????閉鎖是一種同步工具類剖淀, 可以延遲線程的進(jìn)度直到其到達(dá)終止?fàn)顟B(tài)[CPJ 3.4.2]纯蛾。 閉鎖的作用相當(dāng)于一扇門:在閉鎖到達(dá)結(jié)束狀態(tài)之前, 這扇門 一直是關(guān)閉的纵隔,并且沒有任何線程能通過翻诉,當(dāng)?shù)竭_(dá)結(jié)束狀態(tài)時(shí),這扇門會打開并允許所有的線程通過捌刮。 當(dāng)閉鎖到達(dá)結(jié)束狀態(tài)后 將不會再 改變狀態(tài)碰煌,因此這扇門將永遠(yuǎn)保持打開狀態(tài)。閉鎖可以用來確保某些活動直到其他活動都完成后才繼續(xù)執(zhí)行绅作,例如:

· 確保某個(gè)計(jì)算在其需要的所有資源都被初始化之后才繼續(xù)執(zhí)行芦圾。 二元閉鎖(包括兩個(gè)狀態(tài))可以用來表示 “資源R已經(jīng)被初始化 ”,而所有需要R的操作都必須先在這個(gè)閉鎖 上等待棚蓄。

· 確保某個(gè)服務(wù)在其依賴的所有其他服務(wù)都已經(jīng)啟動之后才啟動堕扶。 每個(gè)服務(wù)都有一個(gè)相關(guān)的二元閉鎖。 當(dāng)啟動服務(wù)S時(shí)梭依, 將首先在S依賴的其他服務(wù)的閉鎖上等待, 在所有依賴的服務(wù)都啟動后會釋放閉鎖S, 這樣其他依賴S的服務(wù)才能繼續(xù)執(zhí)行典尾。

· 等待直到某個(gè)操作的所有參與者(例如役拴,在多玩家游戲中的所有玩家)都就緒再繼續(xù)執(zhí) 行。在這種情況中钾埂, 當(dāng)所有玩家都準(zhǔn)備就緒時(shí)河闰, 閉鎖將到達(dá)結(jié)束狀態(tài)科平。

????????CountDownLatch是一種靈活的閉鎖實(shí)現(xiàn),可以在上述各種情況中使用姜性, 它可以使一個(gè)或多個(gè)線程等待一組事件發(fā)生瞪慧。閉鎖狀態(tài)包括一個(gè)計(jì)數(shù)器,該計(jì)數(shù)器被初始化為一個(gè)正數(shù)部念, 表示需要等待的事件數(shù)量弃酌。countDown方法遞減計(jì)數(shù)器, 表示有一個(gè)事件已經(jīng)發(fā)生了儡炼,而 await 方 法等待計(jì)數(shù)器達(dá)到零妓湘,這表示所有需要等待的事件都已經(jīng)發(fā)生。 如果計(jì)數(shù)器的值非零乌询, 那么 await會一直阻塞直到計(jì)數(shù)器為零榜贴, 或者等待中的線程中斷, 或者等待超時(shí)妹田。


????????在程序清單5-11的TestHamess中給出了閉鎖的兩種常見用法唬党。TestHarness創(chuàng)建一定數(shù)量的線程, 利用它們井發(fā)地執(zhí)行指定的任務(wù)鬼佣。 它使用兩個(gè)閉鎖驶拱, 分別表示 “起始門(Starting Gate)"和 “結(jié)束門(Ending Gate) "。 起始門計(jì)數(shù)器的初始值為 1, 而結(jié)束門計(jì)數(shù)器的初始值為工作線程的數(shù)量沮趣。 每個(gè)工作線程首先要做的值就是在啟動門上等待屯烦, 從而確保所有線程都就緒 后才開始執(zhí)行。 而每個(gè)線程要做的最后一件事情是將調(diào)用結(jié)束門的countDown方法減 1, 這能 使主線程高效地等待直到所有工作線程都執(zhí)行完成房铭, 因此可以統(tǒng)計(jì)所消耗的時(shí)間驻龟。

? ??????為什么要在TestHarness 中使用閉鎖, 而不是在線程創(chuàng)建后就立即啟動缸匪?或許翁狐, 我們希望測試n 個(gè)線程并發(fā)執(zhí)行某個(gè)任務(wù)時(shí)需要的時(shí)間。如果在創(chuàng)建線程后立即啟動它們凌蔬, 那么先啟動的線程將“ 領(lǐng)先” 后啟動的線程露懒, 并且活躍線程數(shù)量會隨著時(shí)間的推移而增加或減少, 競爭程度也在不斷發(fā)生變化砂心。啟動門將使得主線程能夠詞時(shí)釋放所有工作線程懈词, 而結(jié)束門則使主線程能夠等待最后一個(gè)線程執(zhí)行完成, 而不是順序地等待每個(gè)線程執(zhí)行完成辩诞。

FutureTask

????????FutureTask 也可以用做閉鎖坎弯。(FutureTask 實(shí)現(xiàn)了Future 語義,表示一種抽象的可生成結(jié)果的計(jì)算[CPJ 4.3.3])。FutureTask表示的計(jì)算是通過Callable 來實(shí)現(xiàn)的抠忘, 相當(dāng)于一種可生成結(jié)果的Runnable, 并且可以處于以下3 種狀態(tài): 等待運(yùn)行(Waiting to run), 正在運(yùn)行(Running) 和運(yùn)行完成(Completed)撩炊。“執(zhí)行完成” 表示計(jì)算的所有可能結(jié)束方式, 包括正常結(jié)束崎脉、由于取消而結(jié)束和由千異常而結(jié)束等拧咳。當(dāng)FutureTask 進(jìn)人完成狀態(tài)后, 它會永遠(yuǎn)停止在這個(gè)狀態(tài)上囚灼。

????????Future.get 的行為取決于任務(wù)的狀態(tài)骆膝。如果任務(wù)已經(jīng)完成, 那么get 會立即返回結(jié)果啦撮, 否則get 將阻塞直到任務(wù)進(jìn)入完成狀態(tài)谭网, 然后返回結(jié)果或者拋出異常。FutureTask 將計(jì)算結(jié)果從執(zhí)行計(jì)算的線程傳遞到獲取這個(gè)結(jié)果的線程赃春, 而FutureTask 的規(guī)范確保了這種傳遞過程能實(shí)現(xiàn)結(jié)果的安全發(fā)布愉择。

? ??????FutureTask在Executor框架中表示異步任務(wù), 此外還可以用來表示一些時(shí)間較長的計(jì)算织中,這些計(jì)算可以在使用計(jì)算結(jié)果之前啟動锥涕。程序清單5-12中的Preloader就使用了FutureTask來執(zhí)行一個(gè)高開銷的計(jì)算, 并且計(jì)算結(jié)果將在稍后使用狭吼。通過提前啟動計(jì)算层坠,可以減少在等待結(jié)果時(shí)需要的時(shí)間。

? ??????Preloader創(chuàng)建了一個(gè)FutureTask, 其中包含從數(shù)據(jù)庫加載產(chǎn)品信息的任務(wù)刁笙,以及一個(gè)執(zhí)行運(yùn)算的線程破花。由于在構(gòu)造函數(shù)或靜態(tài)初始化方法中啟動線程并不是一種好方法, 因此提供了一個(gè)start方法來啟動線程疲吸。當(dāng)程序隨后需要Productlnfo時(shí)座每, 可以調(diào)用get方法, 如果數(shù)據(jù)巳經(jīng)加載摘悴,那么將返回這些數(shù)據(jù)峭梳, 否則將等待加載完成后再返回。

????????Callable表示的任務(wù)可以拋出受檢查的或未受檢查的異常蹂喻, 并且任何代碼都可能拋出一個(gè)Error葱椭。無論任務(wù)代碼拋出什么異常, 都會被封裝到一個(gè)ExecutionException中口四, 并在Future.get中被重新拋出孵运。這將使調(diào)用get的代碼變得復(fù)雜, 因?yàn)樗粌H需要處理可能出現(xiàn)的Execution-Exception (以及未檢查的CancellationException), 而且還由于ExecutionException 是作為一個(gè)Throwable類返回的蔓彩, 因此處理起來并不容易掐松。

????????在Preloader中踱侣, 當(dāng)get方法拋出ExecutionException 時(shí)粪小,可能是以下三種情況之一: Callable拋出的受檢查異常大磺,RuntimeException, 以及Error。我們必須對每種情況進(jìn)行單獨(dú)處理探膊,但我們將使用程序清單5-13中的launderThrowable輔助方法來封裝一些復(fù)雜的異常處理邏輯杠愧。在調(diào)用launderThrowable之前,Preloader會首先檢查巳知的受檢查異常逞壁, 并重新拋出它們流济。剩下的是未檢查異常,Preloader將調(diào)用launderThrowable 并拋出結(jié)果腌闯。如果Throwable傳遞給launderThrowable 的是一個(gè) Error, 那么 launderThrowable 將直接再次拋出它绳瘟; 如果不 RuntimeException, 那么將拋出一個(gè) IllegalStateException 表示這是一個(gè)邏輯錯誤。 剩下的RuntimeException, launderThrowable 將把它們返回給調(diào)用者姿骏, 而調(diào)用者通常會重新拋出它們糖声。

信號量

????????計(jì)數(shù)信號量(Counting Semaphore) 用來控制同時(shí)訪問某個(gè)特定資源的操作數(shù)量, 或者同時(shí)執(zhí)行某個(gè)指定操作的數(shù)量[CPJ 3.4.1]分瘦。計(jì)數(shù)信號量還可以用來實(shí)現(xiàn)某種資源池蘸泻, 或者對容器施加邊界。

????????Semaphore 中管理著一組虛擬的許可(permit), 許可的初始數(shù)量可通過構(gòu)造函數(shù)來指定嘲玫。在執(zhí)行操作時(shí)可以首先獲得許可(只要還有剩余的許可)悦施, 并在使用以后釋放許可。如果沒有許可去团, 那么acquire 將阻塞直到有許可(或者直到被中斷或者操作超時(shí))抡诞。release 方法將返回一個(gè)許可給信號量。計(jì)算信號量的一種簡化形式是二值信號量土陪, 即初始值為1 的Semaphore昼汗。二值信號量可以用做互斥體(mutex), 并具備不可重入的加鎖語義: 誰擁有這個(gè)唯一的許可, 誰就擁有了互斥鎖旺坠。

????????Semaphore 可以用于實(shí)現(xiàn)資源池乔遮, 例如數(shù)據(jù)庫連接池。我們可以構(gòu)造一個(gè)固定長度的資源池取刃, 當(dāng)池為空時(shí)蹋肮, 請求資源將會失敗, 但你真正希望看到的行為是阻塞而不是失敗并且當(dāng)池非空時(shí)解除阻塞璧疗。如果將Semaphore 的計(jì)數(shù)值初始化為池的大小坯辩, 并在從池中獲取一個(gè)資源之前首先調(diào)用acquire 方法獲取一個(gè)許可, 在將資源返回給池之后調(diào)用release 釋放許可崩侠, 那么acquire 將一直阻塞直到資源池不為空漆魔。在第12 章的有界緩沖類中將使用這項(xiàng)技術(shù)。(在構(gòu)造阻塞對象池時(shí), 一種更簡單的方法是使用BlockingQueue 來保存池的資源改抡。)

????????同樣矢炼, 你也可以使用Semaphore 將任何一種容器變成有界阻塞容器, 如程序清單5-14 中的BoundedHashSet 所示阿纤。信號量的計(jì)數(shù)值會初始化為容器容批的最大值句灌。add 操作在向底層容器中添加一個(gè)元素之前, 首先要獲取一個(gè)許可欠拾。如果add 操作沒有添加任何元素胰锌, 那么會立刻釋放許可。 同樣藐窄, remove操作釋放一個(gè)許可资昧,使更多的元素能夠添加到容器中。底層的 Set 實(shí)現(xiàn)并不知道關(guān)于邊界的任何信息荆忍,這是由 BoundedHashSet 來處理的格带。

柵欄

? ??????我們已經(jīng)看到通過閉鎖來啟動一組相關(guān)的操作, 或者等待一組相關(guān)的操作結(jié)束东揣。閉鎖是一次性對象践惑, 一且進(jìn)入終止?fàn)顟B(tài), 就不能被重置嘶卧。

????????柵欄(Barrier) 類似于閉鎖尔觉, 它能阻塞一組線程直到某個(gè)事件發(fā)生[CPJ 4,4.3]。柵欄與閉鎖的關(guān)鍵區(qū)別在于芥吟, 所有線程必須同時(shí)到達(dá)柵欄位置侦铜, 才能繼續(xù)執(zhí)行。閉鎖用于等待事件钟鸵, 而柵欄用于等待其他線程钉稍。柵欄用于實(shí)現(xiàn)一些協(xié)議, 例如幾個(gè)家庭決定在某個(gè)地方集合: “所有人6:00 在麥當(dāng)勞碰頭棺耍, 到了以后要等其他人贡未, 之后再討論下一步要做的事情∶膳郏”

????????CyclicBarrier 可以使一定數(shù)量的參與方反復(fù)地在柵欄位置匯集俊卤, 它在井行迭代算法中非常有用:這種算法通常將一個(gè)問題拆分成一系列相互獨(dú)立的子問題。 當(dāng)線程到達(dá)柵欄位置時(shí)將調(diào)用await 方法害幅, 這個(gè)方法將阻塞直到所有線程都到達(dá)棚欄位置消恍。如果所有線程都到達(dá)了柵欄位,那么柵欄將打開以现, 此時(shí)所有線程都被釋放狠怨, 而柵欄將被重置以便下次使用约啊。如果對await的調(diào)用超時(shí), 或者await 阻塞的線程被中斷佣赖, 那么柵欄就被認(rèn)為是打破了恰矩, 所有阻塞的await調(diào)用都將終止并拋出 BrokenBarrierException。如果成功地通過柵欄茵汰,那么await將為每個(gè)線程返回一個(gè)唯一的到達(dá)索引號枢里, 我們可以利用這些索引來 “ 選舉 ” 產(chǎn)生一個(gè)領(lǐng)導(dǎo)線程, 并在下一次迭代中由該領(lǐng)導(dǎo)線程執(zhí)行一些特殊的工作蹂午。 CyclicBarrier 還可以使你將一個(gè)柵欄操作傳遞給構(gòu)造函數(shù), 這是一個(gè)Runnable, 當(dāng)成功通過棚欄時(shí)會(在一個(gè)子任務(wù)線程中)執(zhí)行它彬碱, 但在阻塞線程被釋放之前是不能執(zhí)行的豆胸。

????????在模擬程序中通常需要使用柵欄, 例如某個(gè)步驟中的計(jì)算可以并行執(zhí)行巷疼, 但必須等到該步驟中的所有計(jì)算都執(zhí)行完畢才能進(jìn)人下一個(gè)步驟晚胡。 例如, 在 n-body 粒子模擬系統(tǒng)中嚼沿, 每個(gè)步驟都根據(jù)其他粒子的位置和屬性來計(jì)算各個(gè)粒子的新位置估盘。 通過在每兩次更新之間等待柵欄, 能夠確保在第 k 步中的所有更新操作都已經(jīng)計(jì)算完畢骡尽, 才進(jìn)入第 k+1步遣妥。

? ? ????在程序清單5-15的CellularAutomata中給出了如何通過柵欄來計(jì)算細(xì)胞的自動化模擬,例如Conway的生命游戲(Gardner,1970)攀细。在把模擬過程并行化時(shí)箫踩,為每個(gè)元素(在這個(gè)示例 中相當(dāng)于一個(gè)細(xì)胞)分配一個(gè)獨(dú)立的線程是不現(xiàn)實(shí)的,因?yàn)檫@將產(chǎn)生過多的線程谭贪,而在協(xié)調(diào)這 些線程上導(dǎo)致的開銷將降低計(jì)算性能境钟。合理的做法是,將問題分解成一定數(shù)簸的子問題俭识,為每 個(gè)子問題分配一個(gè)線程來進(jìn)行求解慨削,之后再將所有的結(jié)果合并起來。CellularAutomata將間題 分解為Ncpu個(gè)子問題套媚,其中Ncpu等千可用CPU的數(shù)量缚态,并將每個(gè)子問題分配給一個(gè)線程。在每個(gè)步驟中凑阶,工作線程都為各自子問題中的所有細(xì)胞計(jì)算新值猿规。當(dāng)所有工作線程都到達(dá)柵欄 時(shí),柵欄會把這些新值提交給數(shù)據(jù)模型宙橱。在柵欄的操作執(zhí)行完以后姨俩,工作線程將開始下一步的計(jì)算蘸拔,包括調(diào)用isDone方法來判斷是否需要進(jìn)行下一次迭代。



????????另一種形式的柵欄是Exchanger, 它是一種兩方(Two-Party)柵欄环葵,各方在柵欄位置上交換數(shù)據(jù)[CPJ 3.4.3]调窍。當(dāng)兩方執(zhí)行不對稱的操作時(shí),Exchanger會非常有用张遭,例如當(dāng)一個(gè)線程向緩沖區(qū)寫入數(shù)據(jù)邓萨,而另一個(gè)線程從緩沖區(qū)中讀取數(shù)據(jù)。這些線程可以使用Exchanger來匯合菊卷,并將滿的緩沖區(qū)與空的緩沖區(qū)交換缔恳。當(dāng)兩個(gè)線程通過Exchanger交換對象時(shí),這種交換就把這兩個(gè)對象安全地發(fā)布給另一方洁闰。

????????數(shù)據(jù)交換的時(shí)機(jī)取決于應(yīng)用程序的響應(yīng)需求歉甚。最簡單的方案是, 當(dāng)緩沖區(qū)被填滿時(shí)扑眉,由填充任務(wù)進(jìn)行交換纸泄,當(dāng)緩沖區(qū)為空時(shí),由清空任務(wù)進(jìn)行交換腰素。這樣會把需要交換的次數(shù)降至最低聘裁,但如果新數(shù)據(jù)的到達(dá)率不可預(yù)測,那么一些數(shù)據(jù)的處理過程就將延遲弓千。另一個(gè)方法是衡便,不僅當(dāng)緩沖被填滿時(shí)進(jìn)行交換,并且當(dāng)緩沖被填充到一定程度并保持一定時(shí)間后计呈,也進(jìn)行交換砰诵。

構(gòu)建高效且可伸縮的結(jié)果緩存

????????幾乎所有的服務(wù)器應(yīng)用程序都會使用某種形式的緩存。重用之前的計(jì)算結(jié)果能降低延遲捌显, 提高吞吐量茁彭,但卻需要消耗更多的內(nèi)存。

????????像許多 “重復(fù)發(fā)明的輪子”一樣扶歪,緩存看上去都非常簡單理肺。然而,簡單的緩存可能會將性能瓶頸轉(zhuǎn)變成可伸縮性瓶頸善镰,即使緩存是用于提升單線程的性能妹萨。本節(jié)我們將開發(fā)一個(gè)高效且 可伸縮的緩存,用于改進(jìn)一個(gè)高計(jì)算開銷的函數(shù)炫欺。我們首先從簡單的HashMap開始乎完,然后分析它的并發(fā)性缺陷,并討論如何修復(fù)它們品洛。

? ??????在程序清單5-16的Computable接口中聲明了一個(gè)函數(shù)Computable, 其輸入類型為A, 輸出類型為V树姨。 在ExpensiveFunction中實(shí)現(xiàn)的Computable, 需要很長的時(shí)間來計(jì)算結(jié)果摩桶,我們將創(chuàng)建一個(gè)Computable包裝器, 幫助記住之前的計(jì)算結(jié)果帽揪, 并將緩存過程封裝起來硝清。 (這項(xiàng)技術(shù)被稱為 “記憶[Memoization]“。


????????在程序清單5-16中的Memoizerl給出了第一種嘗試:使用HashMap來保存之前計(jì)算的結(jié)果转晰。compute方法將首先檢查需要的結(jié)果是否巳經(jīng)在緩存中芦拿, 如果存在則返回之前計(jì)算的值。否則查邢, 將把計(jì)算結(jié)果緩存在HashMap中蔗崎, 然后再返回。

????????HashMap不是線程安全的侠坎, 因此要確保兩個(gè)線程不會同時(shí)訪問HashMap, Memoizerl采用了一種保守的方法蚁趁, 即對整個(gè)compute方法進(jìn)行同步。這種方法能確保線程安全性实胸, 但會帶來一個(gè)明顯的可伸縮性問題:每次只有一個(gè)線程能夠執(zhí)行compute。如果另一個(gè)線程正在計(jì)算結(jié)果番官, 那么其他調(diào)用compute的線程可能被阻塞很長時(shí)間庐完。如果有多個(gè)線程在排隊(duì)等待還未計(jì)算出的結(jié)果, 那么compute方法的計(jì)算時(shí)間可能比沒有“ 記憶” 操作的計(jì)算時(shí)間更長徘熔。在圖5-2中給出了當(dāng)多個(gè)線程使用這種方法中的“ 記憶” 操作時(shí)發(fā)生的情況门躯, 這顯然不是我們希望通過緩存獲得的性能提升結(jié)果。

????????程序清單5-17中的Memoizer2用ConcurrentHashMap代替HashMap來改進(jìn)Memoizerl中糟糕的并發(fā)行為酷师。由于ConcurrentHashMap是線程安全的讶凉,因此在訪問底層Map時(shí)就不需要進(jìn)行同步,因而避免了在對Memoizerl中的compute方法進(jìn)行同步時(shí)帶來的串行性山孔。

????????Memoizer2比Memoizerl有著更好的并發(fā)行為:多線程可以并發(fā)地使用它懂讯。但它在作為緩存時(shí)仍然存在一些不足——當(dāng)兩個(gè)線程同時(shí)調(diào)用compute時(shí)存在一個(gè)涌洞,可能會導(dǎo)致計(jì)算得到相同的值台颠。在使用memoization的情況下褐望,這只會帶來低效,因?yàn)榫彺娴淖饔檬潜苊庀嗤?數(shù)據(jù)被計(jì)算多次串前。但對千更通用的緩存機(jī)制來說瘫里,這種情況將更為糟糕。對于只提供單次初始化的對象緩存來說荡碾,這個(gè)漏洞就會帶來安全風(fēng)險(xiǎn)谨读。

????????Memoizer2 的問題在于, 如果某個(gè)線程啟動了一個(gè)開銷很大的計(jì)算坛吁, 而其他線程并不知道這個(gè)計(jì)算正在進(jìn)行劳殖, 那么很可能會重復(fù)這個(gè)計(jì)算铐尚, 如圖5-3 所示。我們希望通過某種方法來表達(dá)“線程X 正在計(jì)算f(27)" 這種情況闷尿, 這樣當(dāng)另一個(gè)線程查找f(27) 時(shí)塑径, 它能夠知道最高效的方法是等待線程X 計(jì)算結(jié)束, 然后再去查詢緩存"f(27) 的結(jié)果是多少填具? ”统舀。

????????我們已經(jīng)知道有一個(gè)類能基本實(shí)現(xiàn)這個(gè)功能: Future Task。FutureTask 表示一個(gè)計(jì)算的過程劳景,這個(gè)過程可能已經(jīng)計(jì)算完成誉简, 也可能正在進(jìn)行。如果有結(jié)果可用盟广,那么FutureTask.get 將立即返回結(jié)果闷串, 否則它會一直阻塞, 直到結(jié)果計(jì)算出來再將其返回筋量。

????????當(dāng)緩存的是Future而不是值時(shí)烹吵, 將導(dǎo)致緩存污染(Cache Pollution)問題: 如果某個(gè)計(jì)算被取消或者失敗,那么在計(jì)算這個(gè)結(jié)果時(shí)將指明計(jì)算過程被取消或者失敗桨武。為了避免 這種情況肋拔,如果 Memoizer 發(fā)現(xiàn)計(jì)算被取消,那么將把 Future 從緩存中移除呀酸。如果檢測到 RuntimeException, 那么也會移除 Future, 這樣將來的計(jì)算才可能成功凉蜂。Memoizer 同樣沒有解 決緩存逾期的問題,但它可以通過使用 FutureTask 的子類來解決性誉,在子類中為每個(gè)結(jié)果指定一 個(gè)逾期時(shí)間窿吩,并定期掃描緩存中逾期的元素。(同樣错览,它也沒有解決緩存清理的問題纫雁,即移除舊的計(jì)算結(jié)果以便為新的計(jì)算結(jié)果騰出空間,從而使緩存不會消耗過多的內(nèi)存蝗砾。)

????????在完成并發(fā)緩存的實(shí)現(xiàn)后先较,就可以為第2 章中因式分解 servlet 添加結(jié)果緩存。程序清單5-20 中的 Factorizer 使用 Memoizer來緩存之前的計(jì)算結(jié)果悼粮,這種方式不僅高效闲勺,而且可擴(kuò)展性 也更高。


小結(jié)

a.可變狀態(tài)至關(guān)重要:所有的并發(fā)問題都可以歸結(jié)為如何協(xié)調(diào)對并發(fā)狀態(tài)的訪問扣猫,可變狀態(tài)越少菜循,就越容易確保線程安全性。? ? ? ??

b.盡量將域聲明為final類型申尤,除非需要它們是可變的癌幕。

c.不可變對象一定是線程安全地:不可變對象能極大地降低并發(fā)編程的復(fù)雜性衙耕。它們更為簡單而且安全,可以任意共享而無須使用加鎖或保護(hù)性復(fù)制等機(jī)制勺远。

d.封裝有助于管理復(fù)雜性:在編寫線程安全的程序時(shí)橙喘,雖然可以將所有數(shù)據(jù)都保存在全局變量中,但為什么要這么做胶逢?將數(shù)據(jù)封裝在對象中厅瞎,更易于維持不變形條件:將同步機(jī)制封裝在對象中,更易于遵循同步策略初坠。

e.用鎖來保護(hù)每個(gè)可變變量和簸。

f.當(dāng)保護(hù)同一個(gè)不變性條件中的所有變量時(shí),要使用同一個(gè)鎖碟刺。

g.在執(zhí)行復(fù)合操作期間锁保,要持有鎖。

h.如果從多個(gè)線程中訪問同一個(gè)可變變量時(shí)沒有同步機(jī)制半沽,那么程序會出現(xiàn)問題爽柒。

i.不要故作聰明地推斷出不需要使用同步。

j.在設(shè)計(jì)程序過程中考慮線程安全者填,或者在文檔中明確指出它不是線程安全地霉赡。

k.將同步策略文檔化。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末幔托,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子蜂挪,更是在濱河造成了極大的恐慌重挑,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棠涮,死亡現(xiàn)場離奇詭異谬哀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)严肪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進(jìn)店門史煎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人驳糯,你說我怎么就攤上這事篇梭。” “怎么了酝枢?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵恬偷,是天一觀的道長。 經(jīng)常有香客問我帘睦,道長袍患,這世上最難降的妖魔是什么坦康? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮诡延,結(jié)果婚禮上滞欠,老公的妹妹穿的比我還像新娘。我一直安慰自己肆良,他們只是感情好筛璧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著妖滔,像睡著了一般隧哮。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上座舍,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天沮翔,我揣著相機(jī)與錄音,去河邊找鬼曲秉。 笑死采蚀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的承二。 我是一名探鬼主播榆鼠,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼亥鸠!你這毒婦竟也來了妆够?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤负蚊,失蹤者是張志新(化名)和其女友劉穎神妹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體家妆,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡鸵荠,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了伤极。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蛹找。...
    茶點(diǎn)故事閱讀 39,688評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖哨坪,靈堂內(nèi)的尸體忽然破棺而出庸疾,到底是詐尸還是另有隱情,我是刑警寧澤齿税,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布彼硫,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏拧篮。R本人自食惡果不足惜词渤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望串绩。 院中可真熱鬧缺虐,春花似錦、人聲如沸礁凡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽顷牌。三九已至剪芍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間窟蓝,已是汗流浹背罪裹。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留运挫,地道東北人状共。 一個(gè)月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像谁帕,于是被迫代替她去往敵國和親峡继。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容