摘要:并發(fā)模塊本身有兩種不同的類型進(jìn)程和線程,兩個(gè)基本的執(zhí)行單元。調(diào)用以啟動(dòng)新線程。在大多數(shù)系統(tǒng)中,時(shí)間片發(fā)生不可預(yù)知的和非確定性的,這意味著線程可能隨時(shí)暫?;蚧謴?fù)。
大綱
什么是并發(fā)編程?
進(jìn)程,線程和時(shí)間片
交織和競(jìng)爭(zhēng)條件
線程安全
策略1:監(jiān)禁
策略2:不可變性
策略3:使用線程安全數(shù)據(jù)類型
策略4:鎖定和同步
如何做安全論證
總結(jié)
并發(fā)
并發(fā)性:多個(gè)計(jì)算同時(shí)發(fā)生。
在現(xiàn)代編程中無(wú)處不在:
網(wǎng)絡(luò)上的多臺(tái)計(jì)算機(jī)中的多臺(tái)計(jì)算機(jī)
在一臺(tái)計(jì)算機(jī)上運(yùn)行的多個(gè)應(yīng)用程序一臺(tái)計(jì)算機(jī)上的多個(gè)應(yīng)用程序
計(jì)算機(jī)中的多個(gè)處理器(今天,通常是單個(gè)芯片上的多個(gè)處理器內(nèi)核)一個(gè)CPU上的多核處理器
并發(fā)在現(xiàn)代編程中至關(guān)重要:
網(wǎng)站必須處理多個(gè)同時(shí)使用的用戶。多用戶并發(fā)請(qǐng)求服務(wù)器的計(jì)算資源
移動(dòng)應(yīng)用程序需要在云中執(zhí)行一些處理。 App在手機(jī)端和在云端都有計(jì)算
圖形用戶界面幾乎總是需要不中斷用戶的后臺(tái)工作。例如,Eclipse在編輯它時(shí)編譯你的Java代碼。 GUI的前端用戶操作和后臺(tái)的計(jì)算
為什么要“并發(fā)”?
處理器時(shí)鐘速度不再增加。摩爾定律失效了
相反,我們每個(gè)新一代芯片都會(huì)獲得更多內(nèi)核。 “核”變得越來(lái)越多
為了讓計(jì)算更快運(yùn)行,我們必須將計(jì)算分解為并發(fā)塊。為了充分利用多核和多處理器,需要將程序轉(zhuǎn)化為并行執(zhí)行
并行編程的兩種模型
共享內(nèi)存:并發(fā)模塊通過(guò)在內(nèi)存中讀寫共享對(duì)象進(jìn)行交互。
共享內(nèi)存:在內(nèi)存中讀寫共享數(shù)據(jù)
消息傳遞:并發(fā)模塊通過(guò)通信通道相互發(fā)送消息進(jìn)行交互。模塊發(fā)送消息,并將傳入的消息發(fā)送到每個(gè)模塊以便處理。
消息傳遞:通過(guò)信道(channel)交換消息
共享內(nèi)存
共享內(nèi)存模型的示例:
A和B可能是同一臺(tái)計(jì)算機(jī)中的兩個(gè)處理器(或處理器核),共享相同的物理內(nèi)存。
兩個(gè)處理器,共享內(nèi)存
A和B可能是在同一臺(tái)計(jì)算機(jī)上運(yùn)行的兩個(gè)程序,它們共享一個(gè)通用文件系統(tǒng)及其可讀取和寫入的文件。
同一臺(tái)機(jī)器上的兩個(gè)程序,共享文件系統(tǒng)
A和B可能是同一個(gè)Java程序中的兩個(gè)線程,共享相同的Java對(duì)象。
同一個(gè)Java的程序內(nèi)的兩個(gè)線程,共享的Java對(duì)象
消息傳遞
消息傳遞的例子:
A和B可能是網(wǎng)絡(luò)中的兩臺(tái)計(jì)算機(jī),通過(guò)網(wǎng)絡(luò)連接進(jìn)行通信。
網(wǎng)絡(luò)上的兩臺(tái)計(jì)算機(jī),通過(guò)網(wǎng)絡(luò)連接通訊
A和B可能是一個(gè)Web瀏覽器和一個(gè)Web服務(wù)器
A打開與B的連接并請(qǐng)求網(wǎng)頁(yè),B將網(wǎng)頁(yè)數(shù)據(jù)發(fā)送回A.
瀏覽器和Web服務(wù)器,A請(qǐng)求頁(yè)面,B發(fā)送頁(yè)面數(shù)據(jù)給A
A和B可能是即時(shí)消息客戶端和服務(wù)器。
即時(shí)通訊軟件的客戶端和服務(wù)器
A和B可能是在同一臺(tái)計(jì)算機(jī)上運(yùn)行的兩個(gè)程序,其輸入和輸出已通過(guò)管道連接,如鍵入命令提示符中的ls | grep。
同一臺(tái)計(jì)算機(jī)上的兩個(gè)程序,通過(guò)管道連接進(jìn)行通訊
進(jìn)程,線程,時(shí)間片進(jìn)程和線程
消息傳遞和共享內(nèi)存模型是關(guān)于并發(fā)模塊如何通信的。
并發(fā)模塊本身有兩種不同的類型:進(jìn)程和線程,兩個(gè)基本的執(zhí)行單元。
并發(fā)模塊的類型:進(jìn)程和線程
進(jìn)程是正在運(yùn)行的程序的一個(gè)實(shí)例,與同一臺(tái)計(jì)算機(jī)上的其他進(jìn)程隔離。 特別是,它有自己的機(jī)器內(nèi)存專用部分。進(jìn)程:私有空間,彼此隔離
線程是正在運(yùn)行的程序中的一個(gè)控制軌跡。 把它看作是正在運(yùn)行的程序中的一個(gè)地方,再加上導(dǎo)致那個(gè)地方的方法調(diào)用堆棧(所以當(dāng)線程到達(dá)返回語(yǔ)句時(shí)可以返回堆棧)。線程:程序內(nèi)部的控制機(jī)制
(1)進(jìn)程
進(jìn)程抽象是一個(gè)虛擬計(jì)算機(jī)(一個(gè)獨(dú)立的執(zhí)行環(huán)境,具有一套完整的私有基本運(yùn)行時(shí)資源,尤其是內(nèi)存)。進(jìn)程:擁有整臺(tái)計(jì)算機(jī)的資源
它使程序感覺自己擁有整臺(tái)機(jī)器
就像一臺(tái)全新的計(jì)算機(jī)一樣,創(chuàng)建了新的內(nèi)存,只是為了運(yùn)行該程序。
就像連接網(wǎng)絡(luò)的計(jì)算機(jī)一樣,進(jìn)程通常在它們之間不共享內(nèi)存。多進(jìn)程之間不共享內(nèi)存
進(jìn)程無(wú)法訪問(wèn)另一個(gè)進(jìn)程的內(nèi)存或?qū)ο蟆?/p>
相比之下,新的流程自動(dòng)準(zhǔn)備好傳遞消息,因?yàn)樗鞘褂脴?biāo)準(zhǔn)輸入輸出流創(chuàng)建的,這些流是您在Java中使用的System.out和System.in流。進(jìn)程之間通過(guò)消息傳遞進(jìn)行協(xié)作
進(jìn)程通常被視為與程序或應(yīng)用程序的同義詞。一般來(lái)說(shuō),進(jìn)程==程序==應(yīng)用
但是,用戶將其視為單一應(yīng)用程序?qū)嶋H上可能是一組協(xié)作過(guò)程。但一個(gè)應(yīng)用中可能包含多個(gè)進(jìn)程
為了促進(jìn)進(jìn)程之間的通信,大多數(shù)操作系統(tǒng)都支持進(jìn)程間通信(IPC)資源,例如管道和套接字。 OS支持的IPC機(jī)制(pipe / socket)支持進(jìn)程間通信
IPC不僅用于同一系統(tǒng)上的進(jìn)程之間的通信,還用于不同系統(tǒng)上的進(jìn)程。不僅是本機(jī)的多個(gè)進(jìn)程之間,也可以是不同機(jī)器的多個(gè)進(jìn)程之間。
Java虛擬機(jī)的大多數(shù)實(shí)現(xiàn)都是作為單個(gè)進(jìn)程運(yùn)行的。但是Java應(yīng)用程序可以使用ProcessBuilder對(duì)象創(chuàng)建其他進(jìn)程。 JVM通常運(yùn)行單一進(jìn)程,但也可以創(chuàng)建新的進(jìn)程。
(2)線程
線程和多線程編程
就像一個(gè)進(jìn)程代表一個(gè)虛擬計(jì)算機(jī)一樣,線程抽象代表一個(gè)虛擬處理器,線程有時(shí)稱為輕量級(jí)進(jìn)程 進(jìn)程=虛擬機(jī);線程=虛擬CPU
制作一個(gè)新的線程模擬在由該進(jìn)程表示的虛擬計(jì)算機(jī)內(nèi)部制造新的處理器。
這個(gè)新的虛擬處理器運(yùn)行相同的程序,并與進(jìn)程中的其他線程共享相同的資源(內(nèi)存,打開的文件等),即“線程存在于進(jìn)程中”。程序共享,資源共享,都隸屬于進(jìn)程
線程自動(dòng)準(zhǔn)備好共享內(nèi)存,因?yàn)榫€程共享進(jìn)程中的所有內(nèi)存。共享內(nèi)存
需要特殊的努力才能獲得專用于單個(gè)線程的“線程本地”內(nèi)存。很難獲得線程私有的內(nèi)存空間(線程堆棧怎么樣?)
通過(guò)創(chuàng)建和使用隊(duì)列數(shù)據(jù)結(jié)構(gòu),還需要顯式設(shè)置消息傳遞。通過(guò)創(chuàng)建消息隊(duì)列在線程之間進(jìn)行消息傳遞
線程與進(jìn)程
線程是輕量級(jí)的 進(jìn)程是重量級(jí)的
線程共享內(nèi)存空間 進(jìn)程有自己的
線程需要同步(當(dāng)調(diào)用可變對(duì)象時(shí)線程保有鎖) 進(jìn)程不需要
殺死線程是不安全的 殺死進(jìn)程是安全的
多線程執(zhí)行是Java平臺(tái)的基本功能。
每個(gè)應(yīng)用程序至少有一個(gè)線程。每個(gè)應(yīng)用至少有一個(gè)線程
從應(yīng)用程序員的角度來(lái)看,你只從一個(gè)叫做主線程的線程開始。這個(gè)線程有能力創(chuàng)建額外的線程。主線程,可以創(chuàng)建其他的線程
兩種創(chuàng)建線程的方法:
(很少使用)子類化線程。從Thread類派生子類
(更常用)實(shí)現(xiàn)Runnable接口并使用new Thread(..)構(gòu)造函數(shù)。從Runnable接口構(gòu)造線程對(duì)象
如何創(chuàng)建一個(gè)線程:子類Thread
子類Thread
Thread類本身實(shí)現(xiàn)了Runnable,盡管它的run方法什么都不做。應(yīng)用程序可以繼承Thread,提供自己的run()實(shí)現(xiàn)。
調(diào)用Thread.start()以啟動(dòng)新線程。
創(chuàng)建線程的方法:提供一個(gè)Runnable對(duì)象
提供一個(gè)Runnable對(duì)象
Runnable接口定義了一個(gè)方法run(),意在包含在線程中執(zhí)行的代碼。
Runnable對(duì)象被傳遞給Thread構(gòu)造函數(shù)。
如何創(chuàng)建線程
一個(gè)非常常見的習(xí)慣用法是用一個(gè)匿名的Runnable啟動(dòng)一個(gè)線程,它消除了命名的類:
Runnable接口表示要由線程完成的工作。
為什么使用線程?
面對(duì)阻塞活動(dòng)的表現(xiàn)
考慮一個(gè)Web服務(wù)器
在多處理器上的性能
干凈地處理自然并發(fā)
在Java中,線程是生活中的事實(shí)
示例:垃圾收集器在其自己的線程中運(yùn)行(回憶:第8-1節(jié))
我們都是并發(fā)程序員......
為了利用我們的多核處理器,我們必須編寫多線程代碼
好消息:它很多都是為你寫的
存在優(yōu)秀的庫(kù)(java.util.concurrent)
壞消息:你仍然必須了解基本面
有效地使用庫(kù)
調(diào)試使用它們的程序
Interleaving and Race Condition交錯(cuò)和競(jìng)爭(zhēng)
(1) 時(shí)間分片(Time slicing)在具有單個(gè)執(zhí)行核心的計(jì)算機(jī)系統(tǒng)中,在任何給定時(shí)刻只有一個(gè)線程正在執(zhí)行。雖然有多線程,但只有一個(gè)核,每個(gè)時(shí)刻只能執(zhí)行一個(gè)線程
單個(gè)內(nèi)核的處理時(shí)間通過(guò)稱為時(shí)間分片的操作系統(tǒng)功能在進(jìn)程和線程間共享。通過(guò)時(shí)間分片,在多個(gè)進(jìn)程/線程之間共享處理器
今天的計(jì)算機(jī)系統(tǒng)具有多個(gè)處理器或具有多個(gè)執(zhí)行核心的處理器。那么,我的計(jì)算機(jī)中只有一個(gè)或兩個(gè)處理器的多個(gè)并發(fā)線程如何處理?即使是多核CPU,進(jìn)程/線程的數(shù)目也往往大于核的數(shù)目
當(dāng)線程數(shù)多于處理器時(shí),并發(fā)性通過(guò)時(shí)間分片模擬,這意味著處理器在線程之間切換。時(shí)間分片
時(shí)間分片的一個(gè)例子
三個(gè)線程T1,T2和T3可能在具有兩個(gè)實(shí)際處理器的機(jī)器上進(jìn)行時(shí)間分割。
首先一個(gè)處理器運(yùn)行線程T1,另一個(gè)運(yùn)行線程T2,然后第二個(gè)處理器切換到運(yùn)行線程T3。
線程T2只是暫停,直到下一個(gè)時(shí)間片在同一個(gè)處理器或另一個(gè)處理器上。
在大多數(shù)系統(tǒng)中,時(shí)間片發(fā)生不可預(yù)知的和非確定性的,這意味著線程可能隨時(shí)暫?;蚧謴?fù)。時(shí)間分片是由操作系統(tǒng)自動(dòng)調(diào)度的
(2) 線程間的共享內(nèi)存共享內(nèi)存示例
線程之間的共享內(nèi)存可能會(huì)導(dǎo)致微妙的錯(cuò)誤!
例如:一家銀行擁有使用共享內(nèi)存模式的取款機(jī),因此所有取款機(jī)都可以在內(nèi)存中讀取和寫入相同的賬戶對(duì)象。
將銀行簡(jiǎn)化為一個(gè)賬戶,在余額變量中存儲(chǔ)美元余額,以及兩個(gè)操作存款和取款,只需添加或刪除美元即可:
客戶使用現(xiàn)金機(jī)器進(jìn)行如下交易:
每筆交易只是一美元存款,然后是基礎(chǔ)提款,所以它應(yīng)該保持賬戶余額不變。
在整個(gè)一天中,我們網(wǎng)絡(luò)中的每臺(tái)自動(dòng)提款機(jī)正在處理一系列存款/提款交易。
在這一天結(jié)束時(shí),無(wú)論有多少現(xiàn)鈔機(jī)在運(yùn)行,或者我們處理了多少交易,我們都應(yīng)該預(yù)期帳戶余額仍然為0.按理說(shuō),余額應(yīng)該始終為0
但是如果我們運(yùn)行這個(gè)代碼,我們經(jīng)常發(fā)現(xiàn)在一天結(jié)束時(shí)的余額不是0.如果多個(gè)cashMachine()調(diào)用同時(shí)運(yùn)行
例如,在同一臺(tái)計(jì)算機(jī)上的不同處理器上
那么在一天結(jié)束時(shí)余額可能不會(huì)為零。為什么不?
交錯(cuò)
假設(shè)兩臺(tái)取款機(jī)A和B同時(shí)在存款上工作。
以下是deposit()步驟通常如何分解為低級(jí)處理器指令的方法:
當(dāng)A和B同時(shí)運(yùn)行時(shí),這些低級(jí)指令彼此交錯(cuò)...
余額現(xiàn)在是1
A的美元丟失了!
A和B同時(shí)讀取余額,計(jì)算多帶帶的最終余額,然后進(jìn)行存儲(chǔ)以返回新的余額
沒(méi)有考慮到對(duì)方的存款。
競(jìng)爭(zhēng)條件:程序的正確性(后置條件和不變量的滿足)取決于并發(fā)計(jì)算A和B中事件的相對(duì)時(shí)間。發(fā)生這種情況時(shí),我們說(shuō)“A與B競(jìng)爭(zhēng)”。
事件的一些交織可能是可以的,因?yàn)樗鼈兣c單個(gè)非并發(fā)進(jìn)程會(huì)產(chǎn)生什么一致,但是其他交織會(huì)產(chǎn)生錯(cuò)誤的答案 - 違反后置條件或不變量。
調(diào)整代碼將無(wú)濟(jì)于事
所有這些版本的銀行賬戶代碼都具有相同的競(jìng)爭(zhēng)條件!
你不能僅僅從Java代碼中看出處理器將如何執(zhí)行它。
你不能說(shuō)出原子操作是什么。
它不是原子,因?yàn)樗皇且恍蠮ava。
僅僅因?yàn)槠胶鈽?biāo)識(shí)符只在一行中出現(xiàn)一次才平衡一次。單行,單條語(yǔ)句都未必是原子的
Java編譯器不會(huì)對(duì)您的代碼生成的低級(jí)操作做出任何承諾。是否原子,由JVM確定
一個(gè)典型的現(xiàn)代Java編譯器為這三個(gè)版本生成完全相同的代碼!
競(jìng)爭(zhēng)條件
關(guān)鍵的教訓(xùn)是,你無(wú)法通過(guò)觀察一個(gè)表達(dá)來(lái)判斷它是否會(huì)在競(jìng)爭(zhēng)條件下安全。
競(jìng)爭(zhēng)條件也被稱為“線程干擾”
現(xiàn)在不僅是自動(dòng)取款機(jī)模塊,而且賬戶也是模塊。
模塊通過(guò)相互發(fā)送消息進(jìn)行交互。
傳入的請(qǐng)求被放入一個(gè)隊(duì)列中,一次處理一個(gè)請(qǐng)求。
發(fā)件人在等待對(duì)其請(qǐng)求的回答時(shí)不停止工作。它處理來(lái)自其自己隊(duì)列的更多請(qǐng)求。對(duì)其請(qǐng)求的回復(fù)最終會(huì)作為另一條消息返回。
消息傳遞能否解決競(jìng)爭(zhēng)條件?
不幸的是,消息傳遞并不能消除競(jìng)爭(zhēng)條件的可能性。消息傳遞機(jī)制也無(wú)法解決競(jìng)爭(zhēng)條件問(wèn)題
假設(shè)每個(gè)賬戶都支持收支平衡和撤銷操作,并帶有相應(yīng)的消息。
兩臺(tái)A和B取款機(jī)的用戶都試圖從同一賬戶中提取一美元。
他們首先檢查余額,以確保他們永遠(yuǎn)不會(huì)超過(guò)賬戶余額,因?yàn)橥钢?huì)觸發(fā)大銀行的處罰。
問(wèn)題是再次交錯(cuò),但是這次將消息交給銀行賬戶,而不是A和B所執(zhí)行的指令。仍然存在消息傳遞時(shí)間上的交錯(cuò)
如果賬戶以一美元開始,那么什么交錯(cuò)的信息會(huì)欺騙A和B,使他們認(rèn)為他們既可以提取一美元,從而透支賬戶?
使用測(cè)試發(fā)現(xiàn)競(jìng)爭(zhēng)條件非常困難。很難測(cè)試和調(diào)試因?yàn)楦?jìng)爭(zhēng)條件導(dǎo)致的錯(cuò)誤
即使一次測(cè)試發(fā)現(xiàn)了一個(gè)錯(cuò)誤,也可能很難將其本地化到引發(fā)該錯(cuò)誤的程序部分。 - - 為什么?
并發(fā)性錯(cuò)誤表現(xiàn)出很差的重現(xiàn)性。因?yàn)榻诲e(cuò)的存在,導(dǎo)致很難復(fù)現(xiàn)錯(cuò)誤
很難讓它們以同樣的方式發(fā)生兩次。
指令或消息的交織取決于受環(huán)境強(qiáng)烈影響的事件的相對(duì)時(shí)間。
延遲是由其他正在運(yùn)行的程序,其他網(wǎng)絡(luò)流量,操作系統(tǒng)調(diào)度決策,處理器時(shí)鐘速度的變化等引起的。
每次運(yùn)行包含競(jìng)爭(zhēng)條件的程序時(shí),您都可能得到不同的行為。
Heisenbugs和Bohrbugs
一個(gè)heisenbug是一個(gè)軟件錯(cuò)誤,當(dāng)一個(gè)人試圖研究它時(shí),它似乎消失或改變了它的行為。
順序編程中幾乎所有的錯(cuò)誤都是bohrbugs。
并發(fā)性很難測(cè)試和調(diào)試!
當(dāng)您嘗試用println或調(diào)試器查看heisenbug時(shí),甚至可能會(huì)消失!增加打印語(yǔ)句甚至導(dǎo)致這種錯(cuò)誤消失!?
原因是打印和調(diào)試比其他操作慢得多,通常慢100-1000倍,所以它們會(huì)顯著改變操作的時(shí)間和交錯(cuò)。神奇的原因
因此,將一個(gè)簡(jiǎn)單的打印語(yǔ)句插入到cashMachine()中:
...突然間,平衡總是0,并且錯(cuò)誤似乎消失了。但它只是被掩蓋了,并沒(méi)有真正固定。
3.5干擾線程自動(dòng)交錯(cuò)的一些操作
Thread.sleep()方法
使用Thread.sleep(time)暫停執(zhí)行:導(dǎo)致當(dāng)前線程暫停指定時(shí)間段的執(zhí)行。線程的休眠
這是使處理器時(shí)間可用于其他線程或可能在同一臺(tái)計(jì)算機(jī)上運(yùn)行的其他應(yīng)用程序的有效方法。將某個(gè)線程休眠,意味著其他線程得到更多的執(zhí)行機(jī)會(huì)
線程睡眠不會(huì)丟失當(dāng)前線程獲取的任何監(jiān)視器或鎖。進(jìn)入休眠的線程不會(huì)失去對(duì)現(xiàn)有監(jiān)視器或鎖的所有權(quán)
Thread.interrupt()方法
一個(gè)線程通過(guò)調(diào)用Thread對(duì)象上的中斷來(lái)發(fā)送一個(gè)中斷,以便使用interrupt()方法中斷的線程 向線程發(fā)出中斷信號(hào)
t.interrupt()在其他線程里向t發(fā)出中斷信號(hào)
要檢查線程是否中斷,請(qǐng)使用isInterrupted()方法。檢查線程是否被中斷
t.isInterrupted()檢查t是否已在中斷狀態(tài)中
中斷表示線程應(yīng)該停止正在執(zhí)行的操作并執(zhí)行其他操作。 當(dāng)某個(gè)線程被中斷后,一般來(lái)說(shuō)應(yīng)停止其run()中的執(zhí)行,取決于程序員在run()中處理
由程序員決定線程是如何響應(yīng)中斷的,但線程終止是非常常見的。 一般來(lái)說(shuō),線程在收到中斷信號(hào)時(shí)應(yīng)該中斷,直接終止
但是,線程收到其他線程發(fā)來(lái)的中斷信號(hào),并不意味著一定要“停止”...
Thread.yield()方法
這種靜態(tài)方法主要用于通知系統(tǒng)當(dāng)前線程愿意“放棄CPU”一段時(shí)間。使用該方法,線程告知調(diào)度器:我可以放棄CPU的占用權(quán),從而可能引起調(diào)度器喚醒其他線程。
總體思路是:線程調(diào)度器將選擇一個(gè)不同的線程來(lái)運(yùn)行而不是當(dāng)前的線程。
這是線程編程中很少使用的方法,因?yàn)檎{(diào)度應(yīng)該由調(diào)度程序負(fù)責(zé)。盡量避免在代碼中使用
Thread.join()方法
join()方法用于保存當(dāng)前正在運(yùn)行的線程的執(zhí)行,直到指定的線程死亡(執(zhí)行完畢)。讓當(dāng)前線程保持執(zhí)行,直到其執(zhí)行結(jié)束
在正常情況下,我們通常擁有多個(gè)線程,線程調(diào)度程序調(diào)度線程,但不保證線程執(zhí)行的順序。一般不需要這種顯式指定線程執(zhí)行次序
通過(guò)使用join()方法,我們可以讓一個(gè)線程等待另一個(gè)線程。
(6) 總結(jié)并發(fā)性:同時(shí)運(yùn)行多個(gè)計(jì)算
共享內(nèi)存和消息傳遞參數(shù)
進(jìn)程和線程
進(jìn)程就像一臺(tái)虛擬計(jì)算機(jī);線程就像一個(gè)虛擬處理器
競(jìng)爭(zhēng)條件
結(jié)果的正確性(后置條件和不變量)取決于事件的相對(duì)時(shí)間
多個(gè)線程共享相同的可變變量,但不協(xié)調(diào)他們正在做的事情。
這是不安全的,因?yàn)槌绦虻恼_性可能取決于其低級(jí)操作的時(shí)間安排事故。
這些想法主要以糟糕的方式與我們的優(yōu)秀軟件的關(guān)鍵屬性相關(guān)聯(lián)。
并發(fā)是必要的,但它會(huì)導(dǎo)致嚴(yán)重的正確性問(wèn)題:
從錯(cuò)誤安全。并發(fā)性錯(cuò)誤是找到并修復(fù)最難的錯(cuò)誤之一,需要仔細(xì)設(shè)計(jì)才能避免。
容易明白。預(yù)測(cè)并發(fā)代碼如何與其他并發(fā)代碼交錯(cuò)對(duì)于程序員來(lái)說(shuō)非常困難。最好以這樣的方式設(shè)計(jì)代碼,程序員根本不必考慮交錯(cuò)。
線程安全競(jìng)爭(zhēng)條件:多個(gè)線程共享相同的可變變量,但不協(xié)調(diào)他們正在做的事情。
這是不安全的,因?yàn)槌绦虻恼_性可能取決于其低級(jí)操作時(shí)間的事故。
線程之間的“競(jìng)爭(zhēng)條件”:作用于同一個(gè)可變數(shù)據(jù)上的多線程,彼此之間存在對(duì)該數(shù)據(jù)的訪問(wèn)競(jìng)爭(zhēng)并導(dǎo)致交錯(cuò),導(dǎo)致postcondition可能被違反,這是不安全的。
線程安全的意思
如果數(shù)據(jù)類型或靜態(tài)方法在從多個(gè)線程使用時(shí)行為正確,則無(wú)論這些線程如何執(zhí)行,都無(wú)需線程安全,也不需要調(diào)用代碼進(jìn)行額外協(xié)調(diào)。線程安全:ADT或方法在多線程中要執(zhí)行正確
如何捕捉這個(gè)想法?
“正確行為”是指滿足其規(guī)范并保留其不變性;不違反規(guī)范,保持RI
“不管線程如何執(zhí)行”意味著線程可能在多個(gè)處理器上或在同一個(gè)處理器上進(jìn)行時(shí)間片化;與多少處理器,如何調(diào)度線程,均無(wú)關(guān)
“沒(méi)有額外的協(xié)調(diào)”意味著數(shù)據(jù)類型不能在與定時(shí)有關(guān)的調(diào)用方上設(shè)置先決條件,如“在set()進(jìn)行時(shí)不能調(diào)用get()”。不需要在spec中強(qiáng)制要求客戶端滿足某種“線程安全”的義務(wù)
還記得迭代器嗎?這不是線程安全的。
迭代器的規(guī)范說(shuō),你不能在迭代它的同時(shí)修改一個(gè)集合。
這是一個(gè)與調(diào)用程序相關(guān)的與時(shí)間有關(guān)的前提條件,如果違反它,Iterator不保證行為正確。
線程安全意味著什么:remove()的規(guī)范
作為這種非本地契約現(xiàn)象的一個(gè)癥狀,考慮Java集合類,這些類通常記錄在客戶端和實(shí)現(xiàn)者之間的非常明確的契約中。
嘗試找到它在客戶端記錄關(guān)鍵要求的位置,以便在迭代時(shí)無(wú)法修改集合。
線程安全的四種方法
監(jiān)禁數(shù)據(jù)共享。不要在線程之間共享變量。
共享不可變數(shù)據(jù)。使共享數(shù)據(jù)不可變。
線程安全數(shù)據(jù)類型共享線程安全的可變數(shù)據(jù)。將共享數(shù)據(jù)封裝在為您協(xié)調(diào)的現(xiàn)有線程安全數(shù)據(jù)類型中。
同步 同步機(jī)制共享共享線程不安全的可變數(shù)據(jù),對(duì)外即為線程安全的ADT。使用同步來(lái)防止線程同時(shí)訪問(wèn)變量。同步是您構(gòu)建自己的線程安全數(shù)據(jù)類型所需的。
不要共享:在多帶帶的線程中隔離可變狀態(tài)
不要改變:只共享不可變的狀態(tài)
如果必須共享可變狀態(tài),請(qǐng)使用線程安全數(shù)據(jù)類型或同步
線程監(jiān)禁是一個(gè)簡(jiǎn)單的想法:
通過(guò)將數(shù)據(jù)監(jiān)禁在單個(gè)線程中,避免在可變數(shù)據(jù)上進(jìn)行競(jìng)爭(zhēng)。將可變數(shù)據(jù)監(jiān)禁在單一線程內(nèi)部,避免競(jìng)爭(zhēng)
不要讓任何其他線程直接讀取或?qū)懭霐?shù)據(jù)。不允許任何線程直接讀寫該數(shù)據(jù)
由于共享可變數(shù)據(jù)是競(jìng)爭(zhēng)條件的根本原因,監(jiān)禁通過(guò)不共享可變數(shù)據(jù)來(lái)解決。核心思想:線程之間不共享可變數(shù)據(jù)類型
局部變量總是受到線程監(jiān)禁。局部變量存儲(chǔ)在堆棧中,每個(gè)線程都有自己的堆棧。一次運(yùn)行的方法可能會(huì)有多個(gè)調(diào)用,但每個(gè)調(diào)用都有自己的變量專用副本,因此變量本身受到監(jiān)禁。
如果局部變量是對(duì)象引用,則需要檢查它指向的對(duì)象。 如果對(duì)象是可變的,那么我們要檢查對(duì)象是否被監(jiān)禁 - 不能引用它,它可以從任何其他線程訪問(wèn)(而不是別名)。
避免全局變量
這個(gè)類在getInstance()方法中有一個(gè)競(jìng)爭(zhēng)
兩個(gè)線程可以同時(shí)調(diào)用它并最終創(chuàng)建PinballSimulator對(duì)象的兩個(gè)副本,這違反了代表不變量。
假設(shè)兩個(gè)線程正在運(yùn)行g(shù)etInstance()。
對(duì)于兩個(gè)線程正在執(zhí)行的每對(duì)可能的行號(hào),是否有可能違反不變量?
全局靜態(tài)變量不會(huì)自動(dòng)受到線程監(jiān)禁。
如果你的程序中有靜態(tài)變量,那么你必須提出一個(gè)論點(diǎn),即只有一個(gè)線程會(huì)使用它們,并且你必須清楚地記錄這個(gè)事實(shí)。 [在代碼中記錄 - 第4章]
更好的是,你應(yīng)該完全消除靜態(tài)變量。
isPrime()方法從多個(gè)線程調(diào)用并不安全,其客戶端甚至可能不會(huì)意識(shí)到它。
原因是靜態(tài)變量緩存引用的HashMap被所有對(duì)isPrime()的調(diào)用共享,并且HashMap不是線程安全的。
如果多個(gè)線程同時(shí)通過(guò)調(diào)用cache.put()來(lái)改變地圖,那么地圖可能會(huì)以與上一次讀數(shù)中的銀行賬戶損壞相同的方式被破壞。
如果幸運(yùn)的話,破壞可能會(huì)導(dǎo)致哈希映射深處發(fā)生異常,如NullPointerException或IndexOutOfBoundsException。
但它也可能會(huì)悄悄地給出錯(cuò)誤的答案。
策略2:不可變性實(shí)現(xiàn)線程安全的第二種方法是使用不可變引用和數(shù)據(jù)類型。使用不可變數(shù)據(jù)類型和不可變引用,避免多線程之間的競(jìng)爭(zhēng)條件
不變性解決競(jìng)爭(zhēng)條件的共享可變數(shù)據(jù)原因,并簡(jiǎn)單地通過(guò)使共享數(shù)據(jù)不可變來(lái)解決它。
final變量是不可變的引用,所以聲明為final的變量可以安全地從多個(gè)線程訪問(wèn)。
你只能讀取變量,而不能寫入變量。
因?yàn)檫@種安全性只適用于變量本身,我們?nèi)匀槐仨殸?zhēng)辯變量指向的對(duì)象是不可變的。
不可變對(duì)象通常也是線程安全的。不可變數(shù)據(jù)通常是線程安全的
我們說(shuō)“通常”,因?yàn)椴豢勺冃缘漠?dāng)前定義對(duì)于并發(fā)編程而言過(guò)于松散。
如果一個(gè)類型的對(duì)象在整個(gè)生命周期中始終表示相同的抽象值,則類型是不可變的。
但實(shí)際上,只要這些突變對(duì)于客戶是不可見的,例如有益的突變(參見3.3章節(jié)),實(shí)際上允許類型自由地改變其代表。
如緩存,延遲計(jì)算和數(shù)據(jù)結(jié)構(gòu)重新平衡
對(duì)于并發(fā)性,這種隱藏的變異是不安全的。
使用有益突變的不可變數(shù)據(jù)類型必須使用鎖使自己線程安全。如果ADT中使用了有益突變,必須要通過(guò)“加鎖”機(jī)制來(lái)保證線程安全
更強(qiáng)的不變性定義
為了確信一個(gè)不可變的數(shù)據(jù)類型是沒(méi)有鎖的線程安全的,我們需要更強(qiáng)的不變性定義:
沒(méi)有變值器方法
所有字段都是私人的和最終的
沒(méi)有表示風(fēng)險(xiǎn)
表示中的可變對(duì)象沒(méi)有任何突變
甚至不能是有益的突變
如果你遵循這些規(guī)則,那么你可以確信你的不可變類型也是線程安全的。
不要提供“setter”方法 - 修改字段引用的字段或?qū)ο蟮姆椒ā?br>使所有字段最終和私有。
不要讓子類重寫方法。
最簡(jiǎn)單的方法是將類聲明為final。
更復(fù)雜的方法是使構(gòu)造函數(shù)保持私有狀態(tài),并使用工廠方法構(gòu)造實(shí)例。
如果實(shí)例字段包含對(duì)可變對(duì)象的引用,請(qǐng)不要允許更改這些對(duì)象:
不要提供修改可變對(duì)象的方法。
不要共享對(duì)可變對(duì)象的引用。
不要存儲(chǔ)對(duì)傳遞給構(gòu)造函數(shù)的外部可變對(duì)象的引用;如有必要,創(chuàng)建副本,并存儲(chǔ)對(duì)副本的引用。
同樣,必要時(shí)創(chuàng)建內(nèi)部可變對(duì)象的副本,以避免在方法中返回原件。
策略3:使用線程安全數(shù)據(jù)類型實(shí)現(xiàn)線程安全的第三個(gè)主要策略是將共享可變數(shù)據(jù)存儲(chǔ)在現(xiàn)有的線程數(shù)據(jù)類型中。 如果必須要用mutable的數(shù)據(jù)類型在多線程之間共享數(shù)據(jù),則要使用線程安全的數(shù)據(jù)類型。
當(dāng)Java庫(kù)中的數(shù)據(jù)類型是線程安全的時(shí),其文檔將明確說(shuō)明這一事實(shí)。在JDK中的類,文檔中明確指明了是否線程
一般來(lái)說(shuō),JDK同時(shí)提供兩個(gè)相同功能的類,一個(gè)是線程安全的,另一個(gè)不是。線程安全的類一般性能上受影響
原因是這個(gè)報(bào)價(jià)表明:與不安全類型相比,線程安全數(shù)據(jù)類型通常會(huì)導(dǎo)致性能損失。
線程安全集合
Java中的集合接口
列表,設(shè)置,地圖
具有不是線程安全的基本實(shí)現(xiàn)。集合類都是線程不安全的
ArrayList,HashMap和HashSet的實(shí)現(xiàn)不能從多個(gè)線程安全地使用。
Collections API提供了一組包裝器方法來(lái)使集合線程安全,同時(shí)仍然可變。 Java API提供了進(jìn)一步的裝飾器
這些包裝器有效地使集合的每個(gè)方法相對(duì)于其他方法是原子的。
原子動(dòng)作一次有效地發(fā)生
它不會(huì)將其內(nèi)部操作與其他操作的內(nèi)部操作交錯(cuò),并且在整個(gè)操作完成之前,操作的任何效果都不會(huì)被其他線程看到,因此它從未部分完成。
線程安全包裝
public static
public static
public static
public static
public static
public static
包裝實(shí)現(xiàn)將他們所有的實(shí)際工作委托給指定的集合,但在集合提供的基礎(chǔ)上添加額外的功能。
這是裝飾者模式的一個(gè)例子(參見5-3節(jié))
這些實(shí)現(xiàn)是匿名的;該庫(kù)不提供公共類,而是提供靜態(tài)工廠方法。
所有這些實(shí)現(xiàn)都可以在Collections類中找到,該類僅由靜態(tài)方法組成。
同步包裝將自動(dòng)同步(線程安全)添加到任意集合。
不要繞開包裝
確保拋棄對(duì)底層非線程安全集合的引用,并僅通過(guò)同步包裝來(lái)訪問(wèn)它。
新的HashMap只傳遞給synchronizedMap(),并且永遠(yuǎn)不會(huì)存儲(chǔ)在其他地方。
底層的集合仍然是可變的,引用它的代碼可以規(guī)避不變性。
在使用synchronizedMap(hashMap)之后,不要再參數(shù)hashMap共享給其他線程,不要保留別名,一定要徹底銷毀
迭代器仍然不是線程安全的
盡管方法調(diào)用集合本身(get(),put(),add()等)現(xiàn)在是線程安全的,但從集合創(chuàng)建的迭代器仍然不是線程安全的。 即使在線程安全的集合類上,使用迭代器也是不安全的
此迭代問(wèn)題的解決方案將是在需要迭代它時(shí)獲取集合的鎖。除非使用鎖機(jī)制
原子操作不足以阻止競(jìng)爭(zhēng)
您使用同步收集的方式仍可能存在競(jìng)爭(zhēng)條件。
考慮這個(gè)代碼,它檢查列表是否至少有一個(gè)元素,然后獲取該元素:
即使您將list放入同步列表中,此代碼仍可能存在競(jìng)爭(zhēng)條件,因?yàn)榱硪粋€(gè)線程可能會(huì)刪除isEmpty()調(diào)用和get()調(diào)用之間的元素。
同步映射確保containsKey(),get()和put()現(xiàn)在是原子的,所以從多個(gè)線程使用它們不會(huì)損害映射的rep不變量。
但是這三個(gè)操作現(xiàn)在可以以任意方式相互交織,這可能會(huì)破壞緩存中需要的不變量:如果緩存將整數(shù)x映射到值f,那么當(dāng)且僅當(dāng)f為真時(shí)x是素?cái)?shù)。
如果緩存永遠(yuǎn)失敗這個(gè)不變量,那么我們可能會(huì)返回錯(cuò)誤的結(jié)果。
注意
我們必須爭(zhēng)論containsKey(),get()和put()之間的競(jìng)爭(zhēng)不會(huì)威脅到這個(gè)不變量。
containsKey()和get()之間的競(jìng)爭(zhēng)是無(wú)害的,因?yàn)槲覀儚牟粡木彺嬷袆h除項(xiàng)目 - 一旦它包含x的結(jié)果,它將繼續(xù)這樣做。
containsKey()和put()之間存在競(jìng)爭(zhēng)。 因此,最終可能會(huì)有兩個(gè)線程同時(shí)測(cè)試同一個(gè)x的初始值,并且兩個(gè)線程都會(huì)調(diào)用put()與答案。 但是他們都應(yīng)該用相同的答案來(lái)調(diào)用put(),所以無(wú)論哪個(gè)人贏得比賽并不重要 - 結(jié)果將是相同的。
......在注釋中自證線程
需要對(duì)安全性進(jìn)行這種仔細(xì)的論證 - 即使在使用線程安全數(shù)據(jù)類型時(shí) - 也是并發(fā)性很難的主要原因。
一個(gè)簡(jiǎn)短的總結(jié)
通過(guò)共享可變數(shù)據(jù)的競(jìng)爭(zhēng)條件實(shí)現(xiàn)安全的三種主要方式:
禁閉:不共享數(shù)據(jù)。
不變性:共享,但保持?jǐn)?shù)據(jù)不變。
線程安全數(shù)據(jù)類型:將共享的可變數(shù)據(jù)存儲(chǔ)在單個(gè)線程安全數(shù)據(jù)類型中。
減少錯(cuò)誤保證安全。
我們正試圖消除一大類并發(fā)錯(cuò)誤,競(jìng)爭(zhēng)條件,并通過(guò)設(shè)計(jì)消除它們,而不僅僅是意外的時(shí)間。
容易明白。
應(yīng)用這些通用的,簡(jiǎn)單的設(shè)計(jì)模式比關(guān)于哪種線程交叉是可能的而哪些不可行的復(fù)雜論證更容易理解。
準(zhǔn)備好改變。
我們?cè)谝粋€(gè)線程安全參數(shù)中明確地寫下這些理由,以便維護(hù)程序員知道代碼依賴于線程安全。
Strategy 4: Locks and Synchronization最復(fù)雜也最有價(jià)值的threadsafe策略
回顧
數(shù)據(jù)類型或函數(shù)的線程安全性:在從多個(gè)線程使用時(shí)行為正確,無(wú)論這些線程如何執(zhí)行,無(wú)需額外協(xié)調(diào)。線程安全不應(yīng)依賴于偶然
原理:并發(fā)程序的正確性不應(yīng)該依賴于時(shí)間事件。
有四種策略可以使代碼安全并發(fā):
監(jiān)禁:不要在線程之間共享數(shù)據(jù)。
不變性:使共享數(shù)據(jù)不可變。
使用現(xiàn)有的線程安全數(shù)據(jù)類型:使用為您協(xié)調(diào)的數(shù)據(jù)類型。
前三種策略的核心思想:
避免共享→即使共享,也只能讀/不可寫(immutable)→即使可寫(mutable),共享的可寫數(shù)據(jù)應(yīng)該自己具備在多線程之間協(xié)調(diào)的能力,即“使用線程安全的mutable ADT”
同步和鎖定
由于共享可變數(shù)據(jù)的并發(fā)操作導(dǎo)致的競(jìng)爭(zhēng)條件是災(zāi)難性的錯(cuò)誤 - 難以發(fā)現(xiàn),重現(xiàn)和調(diào)試 - 我們需要一種共享內(nèi)存的并發(fā)模塊以實(shí)現(xiàn)彼此同步的方式。
很多時(shí)候,無(wú)法滿足上述三個(gè)條件...
使代碼安全并發(fā)的第四個(gè)策略是:
同步和鎖:防止線程同時(shí)訪問(wèn)共享數(shù)據(jù)。
程序員來(lái)負(fù)責(zé)多線程之間對(duì)可變數(shù)據(jù)的共享操作,通過(guò)“同步”策略,避免多線程同時(shí)訪問(wèn)數(shù)據(jù)
鎖是一種同步技術(shù)。
鎖是一種抽象,最多允許一個(gè)線程擁有它。保持鎖定是一條線程告訴其他現(xiàn)成:“我正在改變這個(gè)東西,現(xiàn)在不要觸摸它。”
使用鎖機(jī)制,獲得對(duì)數(shù)據(jù)的獨(dú)家改變權(quán),其他線程被阻塞,不得訪問(wèn)
使用鎖可以告訴編譯器和處理器你正在同時(shí)使用共享內(nèi)存,所以寄存器和緩存將被刷新到共享存儲(chǔ),確保鎖的所有者始終查看最新的數(shù)據(jù)。
阻塞一般意味著一個(gè)線程等待(不再繼續(xù)工作)直到事件發(fā)生。
兩種鎖定操作
acquire允許線程獲取鎖的所有權(quán)。
如果一個(gè)線程試圖獲取當(dāng)前由另一個(gè)線程擁有的鎖,它會(huì)阻塞,直到另一個(gè)線程釋放該鎖。
在這一點(diǎn)上,它將與任何其他嘗試獲取鎖的線程競(jìng)爭(zhēng)。
一次只能有一個(gè)線程擁有該鎖。
release放棄鎖的所有權(quán),允許另一個(gè)線程獲得它的所有權(quán)。
如果另一個(gè)線程(如線程2)持有鎖l,線程1上的獲取(l)將會(huì)阻塞。它等待的事件是線程2執(zhí)行釋放(l)。
此時(shí),如果線程1可以獲取l,則它繼續(xù)運(yùn)行其代碼,并擁有鎖的所有權(quán)。
另一個(gè)線程(如線程3)也可能在獲?。╨)時(shí)被阻塞。線程1或3將采取鎖定并繼續(xù)。另一個(gè)將繼續(xù)阻塞,再次等待釋放(l)。
(1)同步塊和方法
鎖定
鎖是如此常用以至于Java將它們作為內(nèi)置語(yǔ)言功能提供。鎖是Java的語(yǔ)言提供的內(nèi)嵌機(jī)制
每個(gè)對(duì)象都有一個(gè)隱式關(guān)聯(lián)的鎖 - 一個(gè)String,一個(gè)數(shù)組,一個(gè)ArrayList,每個(gè)類及其所有實(shí)例都有一個(gè)鎖。
即使是一個(gè)不起眼的Object也有一個(gè)鎖,因此裸露的Object通常用于顯式鎖定:
但是,您不能在Java的內(nèi)部鎖上調(diào)用acquire和release。 而是使用synchronized語(yǔ)句來(lái)獲取語(yǔ)句塊持續(xù)時(shí)間內(nèi)的鎖定:
像這樣的同步區(qū)域提供互斥性:一次只能有一個(gè)線程處于由給定對(duì)象的鎖保護(hù)的同步區(qū)域中。
換句話說(shuō),你回到了順序編程世界,一次只運(yùn)行一個(gè)線程,至少就其他同步區(qū)域而言,它們指向同一個(gè)對(duì)象。
鎖定保護(hù)對(duì)數(shù)據(jù)的訪問(wèn)
鎖用于保護(hù)共享數(shù)據(jù)變量。鎖保護(hù)共享數(shù)據(jù)
如果所有對(duì)數(shù)據(jù)變量的訪問(wèn)都被相同的鎖對(duì)象保護(hù)(被同步塊包圍),那么這些訪問(wèn)將被保證為原子 - 不被其他線程中斷。
使用以下命令獲取與對(duì)象obj關(guān)聯(lián)的鎖定:
synchronized(obj){...}
它阻止其他線程進(jìn)入synchronized(obj)直到線程t完成其同步塊為止。
鎖只與其他獲取相同鎖的線程相互排斥。 所有對(duì)數(shù)據(jù)變量的訪問(wèn)必須由相同的鎖保護(hù)。 注意:要互斥,必須使用同一個(gè)鎖進(jìn)行保護(hù)
你可以在單個(gè)鎖后面保護(hù)整個(gè)變量集合,但是所有模塊必須同意他們將獲得并釋放哪個(gè)鎖。
監(jiān)視器模式
在編寫類的方法時(shí),最方便的鎖是對(duì)象實(shí)例本身,即this。用ADT自己做鎖
作為一種簡(jiǎn)單的方法,我們可以通過(guò)在synchronized(this)內(nèi)包裝所有對(duì)rep的訪問(wèn)來(lái)守護(hù)整個(gè)類的表示。
監(jiān)視器模式:監(jiān)視器是一個(gè)類,它們的方法是互斥的,所以一次只能有一個(gè)線程在類的實(shí)例中。
每一個(gè)觸及表示的方法都必須用鎖來(lái)保護(hù),甚至像length()和toString()這樣的顯而易見的小代碼。
這是因?yàn)楸仨毐Wo(hù)讀取以及寫入 - 如果讀取未被保留,則他們可能能夠看到處于部分修改狀態(tài)的rep。
如果將關(guān)鍵字synchronized添加到方法簽名中,Java將像您在方法主體周圍編寫synchronized(this)一樣操作。
同步方法
同一對(duì)象上的同步方法的兩次調(diào)用不可能交錯(cuò)。對(duì)同步的方法,多個(gè)線程執(zhí)行它時(shí)不允許交錯(cuò),也就是說(shuō)“按原子的串行方式執(zhí)行”
當(dāng)一個(gè)線程正在為一個(gè)對(duì)象執(zhí)行一個(gè)同步方法時(shí),所有其他調(diào)用同一對(duì)象的同步方法的線程將阻塞(暫停執(zhí)行),直到第一個(gè)線程完成對(duì)象。
當(dāng)一個(gè)同步方法退出時(shí),它會(huì)自動(dòng)建立與同一對(duì)象的同步方法的任何后續(xù)調(diào)用之間的發(fā)生前關(guān)系。
這保證對(duì)所有線程都可見對(duì)象狀態(tài)的更改。
同步語(yǔ)句/塊
同步方法和同步(this)塊之間有什么區(qū)別?
與synchronized方法不同,synchronized語(yǔ)句必須指定提供內(nèi)部鎖的對(duì)象。
同步語(yǔ)句對(duì)于通過(guò)細(xì)粒度同步來(lái)提高并發(fā)性非常有用。
二者有何區(qū)別?
后者需要顯式的給出鎖,且不一定非要是this
后者可提供更細(xì)粒度的并發(fā)控制
鎖定規(guī)則
鎖定規(guī)則是確保同步代碼是線程安全的策略。
我們必須滿足兩個(gè)條件:
每個(gè)共享的可變變量必須由某個(gè)鎖保護(hù)。除了在獲取該鎖的同步塊內(nèi),數(shù)據(jù)可能不會(huì)被讀取或?qū)懭?。任何共享的可變變?對(duì)象必須被鎖所保護(hù)
如果一個(gè)不變量涉及多個(gè)共享的可變變量(它甚至可能在不同的對(duì)象中),那么涉及的所有變量都必須由相同的鎖保護(hù)。一旦線程獲得鎖定,必須在釋放鎖定之前重新建立不變量。涉及到多個(gè)mutable變量的時(shí)候,它們必須被一個(gè)鎖所保護(hù)
這里使用的監(jiān)視器模式滿足這兩個(gè)規(guī)則。代表中所有共享的可變數(shù)據(jù) - 代表不變量依賴于 - 都被相同的鎖保護(hù)。
發(fā)生-前關(guān)系
這種發(fā)生-前關(guān)系,只是保證多個(gè)線程共享的對(duì)象通過(guò)一個(gè)特定語(yǔ)句寫入的內(nèi)容對(duì)另一個(gè)讀取同一對(duì)象的特定語(yǔ)句是可見的。
這是為了確保內(nèi)存一致性。
發(fā)生-前關(guān)系(a→ b)是兩個(gè)事件的結(jié)果之間的關(guān)系,因此如果在事件發(fā)生之前發(fā)生一個(gè)事件,那么結(jié)果必須反映出,即使這些事件實(shí)際上是無(wú)序執(zhí)行的。
這涉及基于并發(fā)系統(tǒng)中的事件對(duì)的潛在因果關(guān)系對(duì)事件進(jìn)行排序。
它由Leslie Lamport制定。
正式定義為事件中最不嚴(yán)格的部分順序,以便:
如果事件a和b在同一個(gè)過(guò)程中發(fā)生,如果在事件b發(fā)生之前發(fā)生了事件a則a→b;
如果事件a是發(fā)送消息,并且事件b是在事件a中發(fā)送的消息的接收,則a→b。
像所有嚴(yán)格的偏序一樣,發(fā)生-前關(guān)系是傳遞的,非自反的和反對(duì)稱的。
原子數(shù)據(jù)訪問(wèn)的關(guān)鍵字volatile
使用volatile(不穩(wěn)定)變量可降低內(nèi)存一致性錯(cuò)誤的風(fēng)險(xiǎn),因?yàn)槿魏螌?duì)volatile變量的寫入都會(huì)在后續(xù)讀取該變量的同時(shí)建立happen-before關(guān)系。
這意味著對(duì)其他線程總是可見的對(duì)volatile變量的更改。
更重要的是,這也意味著當(dāng)一個(gè)線程讀取一個(gè)volatile變量時(shí),它不僅會(huì)看到volatile的最新變化,還會(huì)看到導(dǎo)致變化的代碼的副作用。
這是一個(gè)輕量級(jí)同步機(jī)制。
使用簡(jiǎn)單的原子變量訪問(wèn)比通過(guò)同步代碼訪問(wèn)這些變量更有效,但需要程序員更多的關(guān)注以避免內(nèi)存一致性錯(cuò)誤。
(3)到處使用同步?
那么線程安全是否只需將synchronized關(guān)鍵字放在程序中的每個(gè)方法上?
不幸的是,
首先,你實(shí)際上并不想同步方法。
同步對(duì)您的程序造成很大的損失。 同步機(jī)制給性能帶來(lái)極大影響
由于需要獲取鎖(并刷新高速緩存并與其他處理器通信),因此進(jìn)行同步方法調(diào)用可能需要更長(zhǎng)的時(shí)間。
由于這些性能原因,Java會(huì)將許多可變數(shù)據(jù)類型默認(rèn)為不同步。當(dāng)你不需要同步時(shí),不要使用它。除非必要,否則不要用.Java中很多mutable的類型都不是threadsafe就是這個(gè)原因
另一個(gè)以更慎重的方式使用同步的理由是,它最大限度地減少了訪問(wèn)鎖的范圍。盡可能減小鎖的范圍
為每個(gè)方法添加同步意味著你的鎖是對(duì)象本身,并且每個(gè)引用了你的對(duì)象的客戶端都會(huì)自動(dòng)引用你的鎖,它可以隨意獲取和釋放。
您的線程安全機(jī)制因此是公開的,可能會(huì)受到客戶的干擾。
與使用作為表示內(nèi)部對(duì)象的鎖并使用synchronized()塊適當(dāng)并節(jié)省地獲取相比。
最后,到處使用同步并不夠?qū)嶋H。
在沒(méi)有思考的情況下同步到一個(gè)方法上意味著你正在獲取一個(gè)鎖,而不考慮它是哪個(gè)鎖,或者是否它是保護(hù)你將要執(zhí)行的共享數(shù)據(jù)訪問(wèn)的正確鎖。
假設(shè)我們?cè)噲D通過(guò)簡(jiǎn)單地將synchronized同步到它的聲明來(lái)解決findReplace的同步問(wèn)題:
public static synchronized boolean findReplace(EditBuffer buf, ...)
它確實(shí)會(huì)獲得一個(gè)鎖 - 因?yàn)閒indReplace是一個(gè)靜態(tài)方法,它將獲取findReplace恰好處于的整個(gè)類的靜態(tài)鎖定,而不是實(shí)例對(duì)象鎖定。
結(jié)果,一次只有一個(gè)線程可以調(diào)用findReplace - 即使其他線程想要在不同的緩沖區(qū)上運(yùn)行,這些緩沖區(qū)應(yīng)該是安全的,它們?nèi)匀粫?huì)被阻塞,直到單個(gè)鎖被釋放。所以我們會(huì)遭受重大的性能損失。
synchronized關(guān)鍵字不是萬(wàn)能的。
線程安全需要一個(gè)規(guī)范 - 使用監(jiān)禁,不變性或鎖來(lái)保護(hù)共享數(shù)據(jù)。
這個(gè)紀(jì)律需要被寫下來(lái),否則維護(hù)人員不會(huì)知道它是什么。
Synchronized不是靈丹妙藥,你的程序需要嚴(yán)格遵守設(shè)計(jì)原則,先試試其他辦法,實(shí)在做不到再考慮lock。
所有關(guān)于線程的設(shè)計(jì)決策也都要在ADT中記錄下來(lái)。
(4)活性:死鎖,饑餓和活鎖
活性
并發(fā)應(yīng)用程序的及時(shí)執(zhí)行能力被稱為活躍性。
三個(gè)子度量標(biāo)準(zhǔn):
死鎖
饑餓
活鎖
(1)死鎖
如果使用得當(dāng),小心,鎖可以防止競(jìng)爭(zhēng)狀況。
但是接下來(lái)的另一個(gè)問(wèn)題就是丑陋的頭腦。
由于使用鎖需要線程等待(當(dāng)另一個(gè)線程持有鎖時(shí)獲取塊),因此可能會(huì)陷入兩個(gè)線程正在等待對(duì)方的情況 - 因此都無(wú)法取得進(jìn)展。
死鎖描述了兩個(gè)或更多線程永遠(yuǎn)被阻塞的情況,等待對(duì)方。
死鎖:多個(gè)線程競(jìng)爭(zhēng)鎖,相互等待對(duì)方釋放鎖
當(dāng)并發(fā)模塊卡住等待對(duì)方執(zhí)行某些操作時(shí)發(fā)生死鎖。
死鎖可能涉及兩個(gè)以上的模塊:死鎖的信號(hào)特征是依賴關(guān)系的一個(gè)循環(huán),例如, A正在等待B正在等待C正在等待A,它們都沒(méi)有取得進(jìn)展。
死鎖的丑陋之處在于它
線程安全的鎖定方法非常強(qiáng)大,但是(與監(jiān)禁和不可變性不同)它將阻塞引入程序。
線程必須等待其他線程退出同步區(qū)域才能繼續(xù)。
在鎖定的情況下,當(dāng)線程同時(shí)獲取多個(gè)鎖時(shí)會(huì)發(fā)生死鎖,并且兩個(gè)線程最終被阻塞,同時(shí)持有鎖,每個(gè)鎖都等待另一個(gè)鎖釋放。
不幸的是,監(jiān)視器模式使得這很容易做到。
死鎖:
線程A獲取harry鎖(因?yàn)閒riend方法是同步的)。
然后線程B獲取snape上的鎖(出于同樣的原因)。
他們都獨(dú)立地更新他們各自的代表,然后嘗試在另一個(gè)對(duì)象上調(diào)用friend() - 這要求他們獲取另一個(gè)對(duì)象上的鎖。
所以A正在拿著哈利等著斯內(nèi)普,而B正拿著斯內(nèi)普等著哈利。
兩個(gè)線程都卡在friend()中,所以都不會(huì)管理退出同步區(qū)域并將鎖釋放到另一個(gè)區(qū)域。
這是一個(gè)經(jīng)典的致命的擁抱。 該程序停止。
問(wèn)題的實(shí)質(zhì)是獲取多個(gè)鎖,并在等待另一個(gè)鎖釋放時(shí)持有某些鎖。
死鎖解決方案1:鎖定順序
對(duì)需要同時(shí)獲取的鎖定進(jìn)行排序,并確保所有代碼按照該順序獲取鎖定。
在示例中,我們可能總是按照向?qū)У拿Q按字母順序獲取向?qū)?duì)象上的鎖定。
雖然鎖定順序很有用(特別是在操作系統(tǒng)內(nèi)核等代碼中),但它在實(shí)踐中有許多缺點(diǎn)。
首先,它不是模塊化的 - 代碼必須知道系統(tǒng)中的所有鎖,或者至少在其子系統(tǒng)中。
其次,代碼在獲取第一個(gè)鎖之前可能很難或不可能確切知道它需要哪些鎖。 它可能需要做一些計(jì)算來(lái)弄清楚。
例如,想一想在社交網(wǎng)絡(luò)圖上進(jìn)行深度優(yōu)先搜索,在你開始尋找它們之前,你怎么知道哪些節(jié)點(diǎn)需要被鎖定?
死鎖解決方案2:粗略鎖定
要使用粗略鎖定 - 使用單個(gè)鎖來(lái)防止許多對(duì)象實(shí)例,甚至是程序的整個(gè)子系統(tǒng)。
例如,我們可能對(duì)整個(gè)社交網(wǎng)絡(luò)擁有一個(gè)鎖,并且對(duì)其任何組成部分的所有操作都在該鎖上進(jìn)行同步。
在代碼中,所有的巫師都屬于一個(gè)城堡,我們只是使用該Castle對(duì)象的鎖來(lái)進(jìn)行同步。
但是,它有明顯的性能損失。
如果你用一個(gè)鎖保護(hù)大量可變數(shù)據(jù),那么你就放棄了同時(shí)訪問(wèn)任何數(shù)據(jù)的能力。
在最糟糕的情況下,使用單個(gè)鎖來(lái)保護(hù)所有內(nèi)容,您的程序可能基本上是順序的。
(2)饑餓
饑餓描述了線程無(wú)法獲得對(duì)共享資源的定期訪問(wèn)并且無(wú)法取得進(jìn)展的情況。
當(dāng)共享資源被“貪婪”線程長(zhǎng)時(shí)間停用時(shí),會(huì)發(fā)生這種情況。
例如,假設(shè)一個(gè)對(duì)象提供了一個(gè)經(jīng)常需要很長(zhǎng)時(shí)間才能返回的同步方法。
如果一個(gè)線程頻繁地調(diào)用此方法,那么其他線程也需要經(jīng)常同步訪問(wèn)同一對(duì)象。
因?yàn)槠渌€程鎖時(shí)間太長(zhǎng),一個(gè)線程長(zhǎng)時(shí)間無(wú)法獲取其所需的資源訪問(wèn)權(quán)(鎖),導(dǎo)致無(wú)法往下進(jìn)行。
(3)活鎖
線程通常會(huì)響應(yīng)另一個(gè)線程的動(dòng)作而行動(dòng)。
如果另一個(gè)線程的動(dòng)作也是對(duì)另一個(gè)線程動(dòng)作的響應(yīng),則可能導(dǎo)致活鎖。
與死鎖一樣,活鎖線程無(wú)法取得進(jìn)一步進(jìn)展。
但是,線程并未被阻止 - 他們只是忙于響應(yīng)對(duì)方恢復(fù)工作。
這與兩個(gè)試圖在走廊上相互傳遞的人相當(dāng):
阿爾方塞向左移動(dòng)讓加斯頓通過(guò),而加斯東向右移動(dòng)讓阿爾方塞通過(guò)。
看到他們?nèi)匀换ハ嘧钄r,阿爾方塞向右移動(dòng),而加斯東向左移動(dòng)。他們?nèi)匀换ハ嘧钄r,所以......
(5)wait(),notify()和notifyAll()
保護(hù)塊
防護(hù)區(qū)塊:這樣的區(qū)塊首先輪詢一個(gè)必須為真的條件才能繼續(xù)。
假設(shè),例如guardedJoy是一種方法,除非另一個(gè)線程設(shè)置了共享變量joy,否則該方法不能繼續(xù)。
這種方法可以簡(jiǎn)單地循環(huán)直到滿足條件,但是該循環(huán)是浪費(fèi)的,因?yàn)樗诘却龝r(shí)連續(xù)執(zhí)行。 某些條件未得到滿足,所以一直在空循環(huán)檢測(cè),直到條件被滿足。這是極大浪費(fèi)。
wait(),notify()和notifyAll()
以下是針對(duì)任意Java對(duì)象o定義的:
o.wait():釋放o上的鎖,進(jìn)入o的等待隊(duì)列并等待
o.notify():?jiǎn)拘裲的等待隊(duì)列中的一個(gè)線程
o.notifyAll():?jiǎn)拘裲的等待隊(duì)列中的所有線程
Object.wait()
Object.wait()會(huì)導(dǎo)致當(dāng)前線程等待,直到另一個(gè)線程調(diào)用此對(duì)象的notify()方法或notifyAll()方法。換句話說(shuō),這個(gè)方法的行為就好像它只是執(zhí)行調(diào)用wait(0)一樣。該操作使對(duì)象所處的阻塞/等待狀態(tài),直到其他線程調(diào)用該對(duì)象的notify()操作
Object.notify()/ notifyAll()
Object.notify()喚醒正在等待該對(duì)象監(jiān)視器的單個(gè)線程。如果任何線程正在等待這個(gè)對(duì)象,則選擇其中一個(gè)線程來(lái)喚醒。隨機(jī)選擇一個(gè)在該對(duì)象上調(diào)用等方法的線程,解除其阻塞狀態(tài)
線程通過(guò)調(diào)用其中一個(gè)等待方法在對(duì)象的監(jiān)視器上等待。
在當(dāng)前線程放棄對(duì)該對(duì)象的鎖定之前,喚醒的線程將無(wú)法繼續(xù)。
喚醒的線程將以通常的方式與其他可能正在主動(dòng)競(jìng)爭(zhēng)的線程競(jìng)爭(zhēng)對(duì)該對(duì)象進(jìn)行同步;例如,被喚醒的線程在作為下一個(gè)線程來(lái)鎖定這個(gè)對(duì)象時(shí)沒(méi)有可靠的特權(quán)或缺點(diǎn)。
此方法只應(yīng)由作為此對(duì)象監(jiān)視器所有者的線程調(diào)用。
線程以三種方式之一成為對(duì)象監(jiān)視器的所有者:
通過(guò)執(zhí)行該對(duì)象的同步實(shí)例方法。
通過(guò)執(zhí)行同步對(duì)象的同步語(yǔ)句的主體。
對(duì)于Class類型的對(duì)象,通過(guò)執(zhí)行該類的同步靜態(tài)方法。
在守衛(wèi)塊中使用wait()
wait()的調(diào)用不會(huì)返回,直到另一個(gè)線程發(fā)出某個(gè)特殊事件可能發(fā)生的通知 - 盡管不一定是該線程正在等待的事件。
Object.wait()會(huì)導(dǎo)致當(dāng)前線程等待,直到另一個(gè)線程調(diào)用此對(duì)象的notify()方法或notifyAll()方法。
當(dāng)wait()被調(diào)用時(shí),線程釋放鎖并暫停執(zhí)行。
在將來(lái)的某個(gè)時(shí)間,另一個(gè)線程將獲得相同的鎖并調(diào)用Object.notifyAll(),通知所有等待該鎖的線程發(fā)生重要事件:
第二個(gè)線程釋放鎖定一段時(shí)間后,第一個(gè)線程重新獲取鎖定,并通過(guò)從等待的調(diào)用返回來(lái)恢復(fù)。
wait(),notify()和notifyAll()
調(diào)用對(duì)象o的方法的線程通常必須預(yù)先鎖定o:
回想一下:開發(fā)ADT的步驟
指定:定義操作(方法簽名和規(guī)約)。
測(cè)試:開發(fā)操作的測(cè)試用例。測(cè)試套件包含基于對(duì)操作的參數(shù)空間進(jìn)行分區(qū)的測(cè)試策略。
代表:選擇一個(gè)代表。
首先實(shí)現(xiàn)一個(gè)簡(jiǎn)單的,強(qiáng)大的代表。
寫下rep不變和抽象函數(shù),并實(shí)現(xiàn)checkRep(),它在每個(gè)構(gòu)造函數(shù),生成器和增量器方法的末尾聲明了rep不變量。
+++同步
說(shuō)出你的代表是線程安全的。
在你的類中作為注釋明確地寫下來(lái),直接用rep不變量表示,以便維護(hù)者知道你是如何為類設(shè)計(jì)線程安全性的。
做一個(gè)安全論證
并發(fā)性很難測(cè)試和調(diào)試!
所以如果你想讓自己和別人相信你的并發(fā)程序是正確的,最好的方法是明確地說(shuō)明它沒(méi)有競(jìng)爭(zhēng),并且記下來(lái)。在代碼中注釋的形式增加說(shuō)明:該ADT采取了什么設(shè)計(jì)決策來(lái)保證線程安全
安全性參數(shù)需要對(duì)模塊或程序中存在的所有線程及其使用的數(shù)據(jù)進(jìn)行編目,并針對(duì)您使用的四種技術(shù)中的哪一種來(lái)防止每個(gè)數(shù)據(jù)對(duì)象或變量的競(jìng)爭(zhēng):監(jiān)禁,不可變性,線程安全數(shù)據(jù)類型或同步。采取了四種方法中的哪一種?
當(dāng)你使用最后兩個(gè)時(shí),你還需要爭(zhēng)辯說(shuō),對(duì)數(shù)據(jù)的所有訪問(wèn)都是適當(dāng)?shù)脑?/p>
也就是說(shuō),你所依賴的不變量不受交織威脅。如果是后兩種,還需考慮對(duì)數(shù)據(jù)的訪問(wèn)都是原子的,不存在交錯(cuò)
用于監(jiān)禁的線程安全論證
因?yàn)槟仨氈老到y(tǒng)中存在哪些線程以及他們有權(quán)訪問(wèn)哪些對(duì)象,因此在我們僅就數(shù)據(jù)類型進(jìn)行爭(zhēng)論時(shí),監(jiān)禁通常不是一種選擇。 除非你知道線程訪問(wèn)的所有數(shù)據(jù),否則Confinement無(wú)法徹底保證線程安全
如果數(shù)據(jù)類型創(chuàng)建了自己的一組線程,那么您可以討論關(guān)于這些線程的監(jiān)禁。
否則,線程從外部進(jìn)入,攜帶客戶端調(diào)用,并且數(shù)據(jù)類型可能無(wú)法保證哪些線程具有對(duì)什么的引用。
因此,在這種情況下,Confinement不是一個(gè)有用的論證。
通常我們?cè)诟邔哟问褂眉s束,討論整個(gè)系統(tǒng),并論證為什么我們不需要線程安全的某些模塊或數(shù)據(jù)類型,因?yàn)樗鼈儾粫?huì)通過(guò)設(shè)計(jì)在線程間共享。除非是在ADT內(nèi)部創(chuàng)建的線程,可以清楚得知訪問(wèn)數(shù)據(jù)有哪些
總結(jié)并發(fā)程序設(shè)計(jì)的目標(biāo)
并發(fā)程序是否可以避免bug?
我們關(guān)心三個(gè)屬性:
安全。 并發(fā)程序是否滿足其不變量和規(guī)約? 訪問(wèn)可變數(shù)據(jù)的競(jìng)爭(zhēng)會(huì)威脅到安全。 安全問(wèn)題:你能證明一些不好的事情從未發(fā)生過(guò)?
活性。 程序是否繼續(xù)運(yùn)行,并最終做你想做的事情,還是會(huì)陷入永遠(yuǎn)等待事件永遠(yuǎn)不會(huì)發(fā)生的地方? 你能證明最終會(huì)發(fā)生什么好事嗎? 死鎖威脅到活性。
公平。 并發(fā)模塊具有處理能力以在計(jì)算上取得進(jìn)展。 公平主要是OS線程調(diào)度器的問(wèn)題,但是你可以通過(guò)設(shè)置線程優(yōu)先級(jí)來(lái)影響它。
實(shí)踐中的并發(fā)
在真正的項(xiàng)目中通常采用什么策略?
庫(kù)數(shù)據(jù)結(jié)構(gòu)不使用同步(為單線程客戶端提供高性能,同時(shí)讓多線程客戶端在頂層添加鎖定)或監(jiān)視器模式。
具有許多部分的可變數(shù)據(jù)結(jié)構(gòu)通常使用粗粒鎖定或線程約束。大多數(shù)圖形用戶界面工具包遵循以下方法之一,因?yàn)閳D形用戶界面基本上是一個(gè)可變對(duì)象的大型可變樹。 Java Swing,圖形用戶界面工具包,使用線程約束。只有一個(gè)專用線程被允許訪問(wèn)Swing的樹。其他線程必須將消息傳遞到該專用線程才能訪問(wèn)該樹。
安全失敗帶來(lái)虛假的安全感。生存失敗迫使你面對(duì)錯(cuò)誤。有利于活躍而不是安全的誘惑。
搜索通常使用不可變的數(shù)據(jù)類型。多線程很容易,因?yàn)樯婕暗乃袛?shù)據(jù)類型都是不可變的。不會(huì)有競(jìng)爭(zhēng)或死鎖的風(fēng)險(xiǎn)。
操作系統(tǒng)通常使用細(xì)粒度的鎖來(lái)獲得高性能,并使用鎖定順序來(lái)處理死鎖問(wèn)題。
數(shù)據(jù)庫(kù)使用與同步區(qū)域類似的事務(wù)來(lái)避免競(jìng)爭(zhēng)條件,因?yàn)樗鼈兊挠绊懯窃拥?,但它們不必獲取鎖定,盡管事務(wù)可能會(huì)失敗并在事件發(fā)生時(shí)被回滾。數(shù)據(jù)庫(kù)還可以管理鎖,并自動(dòng)處理鎖定順序。將在數(shù)據(jù)庫(kù)系統(tǒng)課程中介紹。
總結(jié)
生成一個(gè)安全無(wú)漏洞,易于理解和可以隨時(shí)更改的并發(fā)程序需要仔細(xì)思考。
只要你嘗試將它們固定下來(lái),Heisenbugs就會(huì)消失,所以調(diào)試根本不是實(shí)現(xiàn)正確線程安全代碼的有效方法。
線程可以以許多不同的方式交錯(cuò)操作,即使是所有可能執(zhí)行的一小部分,也永遠(yuǎn)無(wú)法測(cè)試。
創(chuàng)建關(guān)于數(shù)據(jù)類型的線程安全參數(shù),并在代碼中記錄它們。
獲取一個(gè)鎖允許一個(gè)線程獨(dú)占訪問(wèn)該鎖保護(hù)的數(shù)據(jù),強(qiáng)制其他線程阻塞 - 只要這些線程也試圖獲取同一個(gè)鎖。
監(jiān)視器使用通過(guò)每種方法獲取的單個(gè)鎖來(lái)引用數(shù)據(jù)類型的代表。
獲取多個(gè)鎖造成的阻塞會(huì)造成死鎖的可能性。
什么是并發(fā)編程?
進(jìn)程,線程和時(shí)間片
交織和競(jìng)爭(zhēng)條件
線程安全
戰(zhàn)略1:監(jiān)禁
策略2:不可變性
策略3:使用線程安全數(shù)據(jù)類型
策略4:鎖定和同步
如何做安全論證
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/71343.html
摘要:是需要我們?nèi)ヌ幚砗芏嗍虑?,為了防止多線程給我們帶來(lái)的安全和性能的問(wèn)題下面就來(lái)簡(jiǎn)單總結(jié)一下我們需要哪些知識(shí)點(diǎn)來(lái)解決多線程遇到的問(wèn)題。 前言 不小心就鴿了幾天沒(méi)有更新了,這個(gè)星期回家咯。在學(xué)校的日子要努力一點(diǎn)才行! 只有光頭才能變強(qiáng) 回顧前面: 多線程三分鐘就可以入個(gè)門了! Thread源碼剖析 本文章的知識(shí)主要參考《Java并發(fā)編程實(shí)戰(zhàn)》這本書的前4章,這本書的前4章都是講解并發(fā)的基...
摘要:線程的啟動(dòng)與銷毀都與本地線程同步。操作系統(tǒng)會(huì)調(diào)度所有線程并將它們分配給可用的??蚣艿某蓡T主要成員線程池接口接口接口以及工具類。創(chuàng)建單個(gè)線程的接口與其實(shí)現(xiàn)類用于表示異步計(jì)算的結(jié)果。參考書籍并發(fā)編程的藝術(shù)方騰飛魏鵬程曉明著 在java中,直接使用線程來(lái)異步的執(zhí)行任務(wù),線程的每次創(chuàng)建與銷毀需要一定的計(jì)算機(jī)資源開銷。每個(gè)任務(wù)創(chuàng)建一個(gè)線程的話,當(dāng)任務(wù)數(shù)量多的時(shí)候,則對(duì)應(yīng)的創(chuàng)建銷毀開銷會(huì)消耗大量...
摘要:線程切換帶來(lái)的原子性問(wèn)題我們把一個(gè)或者多個(gè)操作在執(zhí)行的過(guò)程中不被中斷的特性稱為原子性。編譯優(yōu)化帶來(lái)的有序性問(wèn)題顧名思義,有序性指的是程序按照代碼的先后順序執(zhí)行。 緩存導(dǎo)致的可見性問(wèn)題 一個(gè)線程對(duì)共享變量的修改,另外一個(gè)線程能夠立刻看到,稱為可見性 在多核下,多個(gè)線程同時(shí)修改一個(gè)共享變量時(shí),如++操作,每個(gè)線程操作的CPU緩存寫入內(nèi)存的時(shí)機(jī)是不確定的。除非你調(diào)用CPU相關(guān)指令強(qiáng)刷。 sh...
摘要:本章中的大部分內(nèi)容適用于構(gòu)造函數(shù)和方法。第項(xiàng)其他方法優(yōu)先于序列化第項(xiàng)謹(jǐn)慎地實(shí)現(xiàn)接口第項(xiàng)考慮使用自定義的序列化形式第項(xiàng)保護(hù)性地編寫方法第項(xiàng)對(duì)于實(shí)例控制,枚舉類型優(yōu)先于第項(xiàng)考慮用序列化代理代替序列化實(shí)例附錄與第版中項(xiàng)目的對(duì)應(yīng)關(guān)系參考文獻(xiàn) effective-java-third-edition 介紹 Effective Java 第三版全文翻譯,純屬個(gè)人業(yè)余翻譯,不合理的地方,望指正,感激...
摘要:線程允許同一個(gè)進(jìn)程中同時(shí)存在多個(gè)程序控制流。線程也被稱為輕量級(jí)進(jìn)程?,F(xiàn)代操作系統(tǒng)中,都是以線程為基本的調(diào)度單位,而不是進(jìn)程。 并發(fā)簡(jiǎn)史 在早期的計(jì)算機(jī)中不包含操作系統(tǒng),從頭至尾都只執(zhí)行一個(gè)程序,并且這個(gè)程序能訪問(wèn)計(jì)算機(jī)所有資源。操作系統(tǒng)的出現(xiàn)使得計(jì)算機(jī)每次能運(yùn)行多個(gè)程序,并且不同的程序都在單獨(dú)的進(jìn)程中運(yùn)行:操作系統(tǒng)為各個(gè)獨(dú)立執(zhí)行的進(jìn)程分配內(nèi)存、文件句柄、安全證書等。不同進(jìn)程之間通過(guò)一些...
閱讀 2055·2021-11-15 11:39
閱讀 3237·2021-10-09 09:41
閱讀 1501·2019-08-30 14:20
閱讀 3274·2019-08-30 13:53
閱讀 3334·2019-08-29 16:32
閱讀 3395·2019-08-29 11:20
閱讀 3032·2019-08-26 13:53
閱讀 783·2019-08-26 12:18