第10章:併發和分佈式編程 10.1併發性和線程安全性

大綱

什麼是併發編程?
進程,線程和時間片
交織和競爭條件
線程安全前端

  • 策略1:監禁
  • 策略2:不可變性
  • 策略3:使用線程安全數據類型
  • 策略4:鎖定和同步

如何作安全論證
總結java

什麼是併發編程?

併發
併發性:多個計算同時發生。程序員

在現代編程中無處不在:數據庫

  • 網絡上的多臺計算機中的多臺計算機
  • 在一臺計算機上運行的多個應用程序一臺計算機上的多個應用程序
  • 計算機中的多個處理器(今天,一般是單個芯片上的多個處理器內核)一個CPU上的多核處理器

併發在現代編程中相當重要:編程

  • 網站必須處理多個同時使用的用戶。多用戶併發請求服務器的計算資源
  • 移動應用程序須要在雲中執行一些處理。 App在手機端和在雲端都有計算
  • 圖形用戶界面幾乎老是須要不中斷用戶的後臺工做。例如,Eclipse在編輯它時編譯你的Java代碼。 GUI的前端用戶操做和後臺的計算

爲何要「併發」?設計模式

處理器時鐘速度再也不增長。摩爾定律失效了
相反,咱們每一個新一代芯片都會得到更多內核。 「核」變得愈來愈多
爲了讓計算更快運行,咱們必須將計算分解爲併發塊。爲了充分利用多核和多處理器,須要將程序轉化爲並行執行數組

並行編程的兩種模型瀏覽器

共享內存:併發模塊經過在內存中讀寫共享對象進行交互。
共享內存:在內存中讀寫共享數據
消息傳遞:併發模塊經過通訊通道相互發送消息進行交互。模塊發送消息,並將傳入的消息發送到每一個模塊以便處理。
消息傳遞:經過信道(channel)交換消息緩存

共享內存安全

共享內存模型的示例:

  • A和B多是同一臺計算機中的兩個處理器(或處理器核),共享相同的物理內存。

兩個處理器,共享內存

  • A和B多是在同一臺計算機上運行的兩個程序,它們共享一個通用文件系統及其可讀取和寫入的文件。

同一臺機器上的兩個程序,共享文件系統

  • A和B多是同一個Java程序中的兩個線程,共享相同的Java對象。

同一個Java的程序內的兩個線程,共享的Java對象

消息傳遞

消息傳遞的例子:

  • A和B多是網絡中的兩臺計算機,經過網絡鏈接進行通訊。

網絡上的兩臺計算機,經過網絡鏈接通信

  • A和B多是一個Web瀏覽器和一個Web服務器
  • A打開與B的鏈接並請求網頁,B將網頁數據發送回A.

瀏覽器和Web服務器,A請求頁面,B發送頁面數據給A

  • A和B多是即時消息客戶端和服務器。

即時通信軟件的客戶端和服務器

  • A和B多是在同一臺計算機上運行的兩個程序,其輸入和輸出已經過管道鏈接,如鍵入命令提示符中的ls | grep。

同一臺計算機上的兩個程序,經過管道鏈接進行通信

進程,線程,時間片

進程和線程

消息傳遞和共享內存模型是關於併發模塊如何通訊的。
併發模塊自己有兩種不一樣的類型:進程和線程,兩個基本的執行單元。
併發模塊的類型:進程和線程

  • 進程是正在運行的程序的一個實例,與同一臺計算機上的其餘進程隔離。 特別是,它有本身的機器內存專用部分。進程:私有空間,彼此隔離
  • 線程是正在運行的程序中的一個控制軌跡。 把它看做是正在運行的程序中的一個地方,再加上致使那個地方的方法調用堆棧(因此當線程到達返回語句時能夠返回堆棧)。線程:程序內部的控制機制

(1)進程

進程抽象是一個虛擬計算機(一個獨立的執行環境,具備一套完整的私有基本運行時資源,尤爲是內存)。進程:擁有整臺計算機的資源

  • 它使程序感受本身擁有整臺機器
  • 就像一臺全新的計算機同樣,建立了新的內存,只是爲了運行該程序。

就像鏈接網絡的計算機同樣,進程一般在它們之間不共享內存。多進程之間不共享內存

  • 進程沒法訪問另外一個進程的內存或對象。
  • 相比之下,新的流程自動準備好傳遞消息,由於它是使用標準輸入輸出流建立的,這些流是您在Java中使用的System.out和System.in流。進程之間經過消息傳遞進行協做

進程一般被視爲與程序或應用程序的同義詞。通常來講,進程==程序==應用

  • 可是,用戶將其視爲單一應用程序實際上多是一組協做過程。但一個應用中可能包含多個進程

爲了促進進程之間的通訊,大多數操做系統都支持進程間通訊(IPC)資源,例如管道和套接字。 OS支持的IPC機制(pipe / socket)支持進程間通訊

  • IPC不只用於同一系統上的進程之間的通訊,還用於不一樣系統上的進程。不只是本機的多個進程之間,也能夠是不一樣機器的多個進程之間。

Java虛擬機的大多數實現都是做爲單個進程運行的。可是Java應用程序可使用ProcessBuilder對象建立其餘進程。 JVM一般運行單一進程,但也能夠建立新的進程。

(2)線程

線程和多線程編程
就像一個進程表明一個虛擬計算機同樣,線程抽象表明一個虛擬處理器,線程有時稱爲輕量級進程 進程=虛擬機;線程=虛擬CPU

  • 製做一個新的線程模擬在由該進程表示的虛擬計算機內部製造新的處理器。
  • 這個新的虛擬處理器運行相同的程序,並與進程中的其餘線程共享相同的資源(內存,打開的文件等),即「線程存在於進程中」。程序共享,資源共享,都隸屬於進程

線程自動準備好共享內存,由於線程共享進程中的全部內存。共享內存

  • 須要特殊的努力才能得到專用於單個線程的「線程本地」內存。很難得到線程私有的內存空間(線程堆棧怎麼樣?)
  • 經過建立和使用隊列數據結構,還須要顯式設置消息傳遞。經過建立消息隊列在線程之間進行消息傳遞

線程與進程

線程是輕量級的 進程是重量級的
線程共享內存空間 進程有本身的
線程須要同步(當調用可變對象時線程保有鎖) 進程不須要
殺死線程是不安全的 殺死進程是安全的

多線程執行是Java平臺的基本功能。

  • 每一個應用程序至少有一個線程。每一個應用至少有一個線程
  • 從應用程序員的角度來看,你只從一個叫作主線程的線程開始。這個線程有能力建立額外的線程。主線程,能夠建立其餘的線程

兩種建立線程的方法:

  • (不多使用)子類化線程。從Thread類派生子類
  • (更經常使用)實現Runnable接口並使用new Thread(..)構造函數。從Runnable接口構造線程對象

如何建立一個線程:子類Thread

子類Thread

  • Thread類自己實現了Runnable,儘管它的run方法什麼都不作。應用程序能夠繼承Thread,提供本身的run()實現。

調用Thread.start()以啓動新線程。

建立線程的方法:提供一個Runnable對象

提供一個Runnable對象

  • Runnable接口定義了一個方法run(),意在包含在線程中執行的代碼。
  • Runnable對象被傳遞給Thread構造函數。

如何建立線程

一個很是常見的習慣用法是用一個匿名的Runnable啓動一個線程,它消除了命名的類:
Runnable接口表示要由線程完成的工做。

爲何使用線程?

面對阻塞活動的表現

  • 考慮一個Web服務器

在多處理器上的性能
乾淨地處理天然併發

在Java中,線程是生活中的事實

  • 示例:垃圾收集器在其本身的線程中運行(回憶:第8-1節)

咱們都是併發程序員......

爲了利用咱們的多核處理器,咱們必須編寫多線程代碼
好消息:它不少都是爲你寫的

  • 存在優秀的庫(java.util.concurrent)

壞消息:你仍然必須瞭解基本面

  • 有效地使用庫
  • 調試使用它們的程序

Interleaving and Race Condition

交錯和競爭

(1) 時間分片(Time slicing)

在具備單個執行核心的計算機系統中,在任何給定時刻只有一個線程正在執行。雖然有多線程,但只有一個核,每一個時刻只能執行一個線程

  • 單個內核的處理時間經過稱爲時間分片的操做系統功能在進程和線程間共享。經過時間分片,在多個進程/線程之間共享處理器

今天的計算機系統具備多個處理器或具備多個執行核心的處理器。那麼,個人計算機中只有一個或兩個處理器的多個併發線程如何處理?即便是多核CPU,進程/線程的數目也每每大於核的數目

  • 當線程數多於處理器時,併發性經過時間分片模擬,這意味着處理器在線程之間切換。時間分片

時間分片的一個例子

三個線程T1,T2和T3可能在具備兩個實際處理器的機器上進行時間分割。

  • 首先一個處理器運行線程T1,另外一個運行線程T2,而後第二個處理器切換到運行線程T3。
  • 線程T2只是暫停,直到下一個時間片在同一個處理器或另外一個處理器上。

在大多數系統中,時間片發生不可預知的和非肯定性的,這意味着線程可能隨時暫停或恢復。時間分片是由操做系統自動調度的

(2) 線程間的共享內存

共享內存示例

線程之間的共享內存可能會致使微妙的錯誤!
例如:一家銀行擁有使用共享內存模式的取款機,所以全部取款機均可以在內存中讀取和寫入相同的帳戶對象。
將銀行簡化爲一個帳戶,在餘額變量中存儲美圓餘額,以及兩個操做存款和取款,只需添加或刪除美圓便可:
客戶使用現金機器進行以下交易:
每筆交易只是一美圓存款,而後是基礎提款,因此它應該保持帳戶餘額不變。

  • 在整個一天中,咱們網絡中的每臺自動提款機正在處理一系列存款/提款交易。

在這一天結束時,不管有多少現鈔機在運行,或者咱們處理了多少交易,咱們都應該預期賬戶餘額仍然爲0.按理說,餘額應該始終爲0

  • 可是若是咱們運行這個代碼,咱們常常發如今一天結束時的餘額不是0.若是多個cashMachine()調用同時運行
  • 例如,在同一臺計算機上的不一樣處理器上
  • 那麼在一天結束時餘額可能不會爲零。爲何不?

交錯

假設兩臺取款機A和B同時在存款上工做。
如下是deposit()步驟一般如何分解爲低級處理器指令的方法:
當A和B同時運行時,這些低級指令彼此交錯...

(3) 競爭條件(Race Condition)

餘額如今是1

  • A的美圓丟失了!
  • A和B同時讀取餘額,計算單獨的最終餘額,而後進行存儲以返回新的餘額
  • 沒有考慮到對方的存款。

競爭條件:程序的正確性(後置條件和不變量的知足)取決於併發計算A和B中事件的相對時間。發生這種狀況時,咱們說「A與B競爭」。
事件的一些交織多是能夠的,由於它們與單個非併發進程會產生什麼一致,可是其餘交織會產生錯誤的答案 - 違反後置條件或不變量。

調整代碼將無濟於事

全部這些版本的銀行帳戶代碼都具備相同的競爭條件!
你不能僅僅從Java代碼中看出處理器將如何執行它。
你不能說出原子操做是什麼。

  • 它不是原子,由於它只是一行Java。
  • 僅僅由於平衡標識符只在一行中出現一次才平衡一次。單行,單條語句都未必是原子的

Java編譯器不會對您的代碼生成的低級操做作出任何承諾。是否原子,由JVM肯定

  • 一個典型的現代Java編譯器爲這三個版本生成徹底相同的代碼!

競爭條件
關鍵的教訓是,你沒法經過觀察一個表達來判斷它是否會在競爭條件下安全。
競爭條件也被稱爲「線程干擾」

(4) 消息傳遞示例

如今不只是自動取款機模塊,並且帳戶也是模塊。
模塊經過相互發送消息進行交互。

  • 傳入的請求被放入一個隊列中,一次處理一個請求。
  • 發件人在等待對其請求的回答時不中止工做。它處理來自其本身隊列的更多請求。對其請求的回覆最終會做爲另外一條消息返回。

消息傳遞可否解決競爭條件?

不幸的是,消息傳遞並不能消除競爭條件的可能性。消息傳遞機制也沒法解決競爭條件問題

  • 假設每一個帳戶都支持收支平衡和撤銷操做,並帶有相應的消息。
  • 兩臺A和B取款機的用戶都試圖從同一帳戶中提取一美圓。
  • 他們首先檢查餘額,以確保他們永遠不會超過帳戶餘額,由於透支會觸發大銀行的處罰。

問題是再次交錯,可是此次將消息交給銀行帳戶,而不是A和B所執行的指令。仍然存在消息傳遞時間上的交錯
若是帳戶以一美圓開始,那麼什麼交錯的信息會欺騙A和B,使他們認爲他們既能夠提取一美圓,從而透支帳戶?

(5) 併發性很難測試和調試

使用測試發現競爭條件很是困難。很難測試和調試由於競爭條件致使的錯誤

  • 即便一次測試發現了一個錯誤,也可能很難將其本地化到引起該錯誤的程序部分。 - - 爲何?

併發性錯誤表現出不好的重現性。由於交錯的存在,致使很難復現錯誤

  • 很難讓它們以一樣的方式發生兩次。
  • 指令或消息的交織取決於受環境強烈影響的事件的相對時間。
  • 延遲是由其餘正在運行的程序,其餘網絡流量,操做系統調度決策,處理器時鐘速度的變化等引發的。
  • 每次運行包含競爭條件的程序時,您均可能獲得不一樣的行爲。

Heisenbugs和Bohrbugs

一個heisenbug是一個軟件錯誤,當一我的試圖研究它時,它彷佛消失或改變了它的行爲。
順序編程中幾乎全部的錯誤都是bohrbugs。

併發性很難測試和調試!

當您嘗試用println或調試器查看heisenbug時,甚至可能會消失!增長打印語句甚至致使這種錯誤消失!〜

  • 緣由是打印和調試比其餘操做慢得多,一般慢100-1000倍,因此它們會顯着改變操做的時間和交錯。神奇的緣由

所以,將一個簡單的打印語句插入到cashMachine()中:
...忽然間,平衡老是0,而且錯誤彷佛消失了。但它只是被掩蓋了,並無真正固定。

3.5干擾線程自動交錯的一些操做

Thread.sleep()方法
使用Thread.sleep(time)暫停執行:致使當前線程暫停指定時間段的執行。線程的休眠

  • 這是使處理器時間可用於其餘線程或可能在同一臺計算機上運行的其餘應用程序的有效方法。將某個線程休眠,意味着其餘線程獲得更多的執行機會
  • 線程睡眠不會丟失當前線程獲取的任何監視器或鎖。進入休眠的線程不會失去對現有監視器或鎖的全部權

Thread.interrupt()方法
一個線程經過調用Thread對象上的中斷來發送一箇中斷,以便使用interrupt()方法中斷的線程 向線程發出中斷信號

  • t.interrupt()在其餘線程裏向t發出中斷信號

要檢查線程是否中斷,請使用isInterrupted()方法。檢查線程是否被中斷

  • t.isInterrupted()檢查t是否已在中斷狀態中

中斷表示線程應該中止正在執行的操做並執行其餘操做。 當某個線程被中斷後,通常來講應中止其run()中的執行,取決於程序員在run()中處理

  • 由程序員決定線程是如何響應中斷的,但線程終止是很是常見的。 通常來講,線程在收到中斷信號時應該中斷,直接終止

可是,線程收到其餘線程發來的中斷信號,並不意味着必定要「中止」...

Thread.yield()方法
這種靜態方法主要用於通知系統當前線程願意「放棄CPU」一段時間。使用該方法,線程告知調度器:我能夠放棄CPU的佔用權,從而可能引發調度器喚醒其餘線程。

  • 整體思路是:線程調度器將選擇一個不一樣的線程來運行而不是當前的線程。

這是線程編程中不多使用的方法,由於調度應該由調度程序負責。儘可能避免在代碼中使用

Thread.join()方法
join()方法用於保存當前正在運行的線程的執行,直到指定的線程死亡(執行完畢)。讓當前線程保持執行,直到其執行結束

  • 在正常狀況下,咱們一般擁有多個線程,線程調度程序調度線程,但不保證線程執行的順序。通常不須要這種顯式指定線程執行次序
  • 經過使用join()方法,咱們可讓一個線程等待另外一個線程。

(6) 總結

併發性:同時運行多個計算
共享內存和消息傳遞參數

進程和線程

  • 進程就像一臺虛擬計算機;線程就像一個虛擬處理器

競爭條件

  • 結果的正確性(後置條件和不變量)取決於事件的相對時間
  • 多個線程共享相同的可變變量,但不協調他們正在作的事情。
  • 這是不安全的,由於程序的正確性可能取決於其低級操做的時間安排事故。

這些想法主要以糟糕的方式與咱們的優秀軟件的關鍵屬性相關聯。

併發是必要的,但它會致使嚴重的正確性問題:

  • 從錯誤安全。併發性錯誤是找到並修復最難的錯誤之一,須要仔細設計才能避免。
  • 容易明白。預測併發代碼如何與其餘併發代碼交錯對於程序員來講很是困難。最好以這樣的方式設計代碼,程序員根本沒必要考慮交錯。

線程安全

競爭條件:多個線程共享相同的可變變量,但不協調他們正在作的事情。

這是不安全的,由於程序的正確性可能取決於其低級操做時間的事故。
線程之間的「競爭條件」:做用於同一個可變數據上的多線程,彼此之間存在對該數據的訪問競爭並致使交錯,致使postcondition可能被違反,這是不安全的。

線程安全的意思

若是數據類型或靜態方法在從多個線程使用時行爲正確,則不管這些線程如何執行,都無需線程安全,也不須要調用代碼進行額外協調。線程安全:ADT或方法在多線程中要執行正確

如何捕捉這個想法?

  • 「正確行爲」是指知足其規範並保留其不變性;不違反規範,保持RI
  • 「無論線程如何執行」意味着線程可能在多個處理器上或在同一個處理器上進行時間片化;與多少處理器,如何調度線程,均無關
  • 「沒有額外的協調」意味着數據類型不能在與定時有關的調用方上設置先決條件,如「在set()進行時不能調用get()」。不須要在spec中強制要求客戶端知足某種「線程安全」的義務

還記得迭代器嗎?這不是線程安全的。

迭代器的規範說,你不能在迭代它的同時修改一個集合。
這是一個與調用程序相關的與時間有關的前提條件,若是違反它,Iterator不保證行爲正確。

線程安全意味着什麼:remove()的規範

做爲這種非本地契約現象的一個症狀,考慮Java集合類,這些類一般記錄在客戶端和實現者之間的很是明確的契約中。

  • 嘗試找到它在客戶端記錄關鍵要求的位置,以便在迭代時沒法修改集合。

線程安全的四種方法

監禁數據共享。不要在線程之間共享變量。

共享不可變數據。使共享數據不可變。

線程安全數據類型共享線程安全的可變數據。將共享數據封裝在爲您協調的現有線程安全數據類型中。

同步 同步機制共享共享線程不安全的可變數據,對外即爲線程安全的ADT。使用同步來防止線程同時訪問變量。同步是您構建本身的線程安全數據類型所需的。

不要共享:在單獨的線程中隔離可變狀態
不要改變:只共享不可變的狀態
若是必須共享可變狀態,請使用線程安全數據類型或同步

策略1:監禁

線程監禁是一個簡單的想法:

  • 經過將數據監禁在單個線程中,避免在可變數據上進行競爭。將可變數據監禁在單一線程內部,避免競爭
  • 不要讓任何其餘線程直接讀取或寫入數據。不容許任何線程直接讀寫該數據

因爲共享可變數據是競爭條件的根本緣由,監禁經過不共享可變數據來解決。核心思想:線程之間不共享可變數據類型

局部變量老是受到線程監禁。局部變量存儲在堆棧中,每一個線程都有本身的堆棧。一次運行的方法可能會有多個調用,但每一個調用都有本身的變量專用副本,所以變量自己受到監禁。

  • 若是局部變量是對象引用,則須要檢查它指向的對象。 若是對象是可變的,那麼咱們要檢查對象是否被監禁 - 不能引用它,它能夠從任何其餘線程訪問(而不是別名)。

避免全局變量

這個類在getInstance()方法中有一個競爭

  • 兩個線程能夠同時調用它並最終建立PinballSimulator對象的兩個副本,這違反了表明不變量。

假設兩個線程正在運行getInstance()。
對於兩個線程正在執行的每對可能的行號,是否有可能違反不變量?
全局靜態變量不會自動受到線程監禁。

  • 若是你的程序中有靜態變量,那麼你必須提出一個論點,即只有一個線程會使用它們,而且你必須清楚地記錄這個事實。 [在代碼中記錄 - 第4章]

更好的是,你應該徹底消除靜態變量。

isPrime()方法從多個線程調用並不安全,其客戶端甚至可能不會意識到它。

  • 緣由是靜態變量緩存引用的HashMap被全部對isPrime()的調用共享,而且HashMap不是線程安全的。
  • 若是多個線程同時經過調用cache.put()來改變地圖,那麼地圖可能會以與上一次讀數中的銀行帳戶損壞相同的方式被破壞。
  • 若是幸運的話,破壞可能會致使哈希映射深處發生異常,如NullPointerException或IndexOutOfBoundsException。
  • 但它也可能會悄悄地給出錯誤的答案。

策略2:不可變性

實現線程安全的第二種方法是使用不可變引用和數據類型。使用不可變數據類型和不可變引用,避免多線程之間的競爭條件

  • 不變性解決競爭條件的共享可變數據緣由,並簡單地經過使共享數據不可變來解決它。

final變量是不可變的引用,因此聲明爲final的變量能夠安全地從多個線程訪問。

  • 你只能讀取變量,而不能寫入變量。
  • 由於這種安全性只適用於變量自己,咱們仍然必須爭辯變量指向的對象是不可變的。

不可變對象一般也是線程安全的。不可變數據一般是線程安全的
咱們說「一般」,由於不可變性的當前定義對於併發編程而言過於鬆散。

  • 若是一個類型的對象在整個生命週期中始終表示相同的抽象值,則類型是不可變的。
  • 但實際上,只要這些突變對於客戶是不可見的,例若有益的突變(參見3.3章節),實際上容許類型自由地改變其表明。
  • 如緩存,延遲計算和數據結構從新平衡

對於併發性,這種隱藏的變異是不安全的。

  • 使用有益突變的不可變數據類型必須使用鎖使本身線程安全。若是ADT中使用了有益突變,必需要經過「加鎖」機制來保證線程安全

更強的不變性定義

爲了確信一個不可變的數據類型是沒有鎖的線程安全的,咱們須要更強的不變性定義:

  • 沒有變值器方法
  • 全部字段都是私人的和最終的
  • 沒有表示風險
  • 表示中的可變對象沒有任何突變
  • 甚至不能是有益的突變

若是你遵循這些規則,那麼你能夠確信你的不可變類型也是線程安全的。
不要提供「setter」方法 - 修改字段引用的字段或對象的方法。
使全部字段最終和私有。
不要讓子類重寫方法。

  • 最簡單的方法是將類聲明爲final。
  • 更復雜的方法是使構造函數保持私有狀態,並使用工廠方法構造實例。

若是實例字段包含對可變對象的引用,請不要容許更改這些對象:

  • 不要提供修改可變對象的方法。
  • 不要共享對可變對象的引用。
  • 不要存儲對傳遞給構造函數的外部可變對象的引用;若有必要,建立副本,並存儲對副本的引用。
  • 一樣,必要時建立內部可變對象的副本,以免在方法中返回原件。

策略3:使用線程安全數據類型

實現線程安全的第三個主要策略是將共享可變數據存儲在現有的線程數據類型中。 若是必需要用mutable的數據類型在多線程之間共享數據,則要使用線程安全的數據類型。

  • 當Java庫中的數據類型是線程安全的時,其文檔將明確說明這一事實。在JDK中的類,文檔中明確指明瞭是否線程

通常來講,JDK同時提供兩個相同功能的類,一個是線程安全的,另外一個不是。線程安全的類通常性能上受影響

  • 緣由是這個報價代表:與不安全類型相比,線程安全數據類型一般會致使性能損失。

線程安全集合

Java中的集合接口

  • 列表,設置,地圖
  • 具備不是線程安全的基本實現。集合類都是線程不安全的
  • ArrayList,HashMap和HashSet的實現不能從多個線程安全地使用。

Collections API提供了一組包裝器方法來使集合線程安全,同時仍然可變。 Java API提供了進一步的裝飾器

  • 這些包裝器有效地使集合的每一個方法相對於其餘方法是原子的。
  • 原子動做一次有效地發生
  • 它不會將其內部操做與其餘操做的內部操做交錯,而且在整個操做完成以前,操做的任何效果都不會被其餘線程看到,所以它從未部分完成。

線程安全包裝

public static <T> Collection<T> synchronizedCollection(Collection<T> c);
public static <T> Set<T> synchronizedSet(Set<T> s);
public static <T> List<T> synchronizedList(List<T> list);
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m);
public static <T> SortedSet<T> synchronizedSortedSet(SortedSet<T> s);
public static <K,V> SortedMap<K,V> synchronizedSortedMap(SortedMap<K,V> m);

包裝實現將他們全部的實際工做委託給指定的集合,但在集合提供的基礎上添加額外的功能。
這是裝飾者模式的一個例子(參見5-3節)
這些實現是匿名的;該庫不提供公共類,而是提供靜態工廠方法。
全部這些實現均可以在Collections類中找到,該類僅由靜態方法組成。
同步包裝將自動同步(線程安全)添加到任意集合。

不要繞開包裝

確保拋棄對底層非線程安全集合的引用,並僅經過同步包裝來訪問它。
新的HashMap只傳遞給synchronizedMap(),而且永遠不會存儲在其餘地方。
底層的集合仍然是可變的,引用它的代碼能夠規避不變性。
在使用synchronizedMap(hashMap)以後,不要再參數hashMap共享給其餘線程,不要保留別名,必定要完全銷燬

迭代器仍然不是線程安全的

儘管方法調用集合自己(get(),put(),add()等)如今是線程安全的,但從集合建立的迭代器仍然不是線程安全的。 即便在線程安全的集合類上,使用迭代器也是不安全的
此迭代問題的解決方案將是在須要迭代它時獲取集合的鎖。除非使用鎖機制

原子操做不足以阻止競爭

您使用同步收集的方式仍可能存在競爭條件。
考慮這個代碼,它檢查列表是否至少有一個元素,而後獲取該元素:
即便您將list放入同步列表中,此代碼仍可能存在競爭條件,由於另外一個線程可能會刪除isEmpty()調用和get()調用之間的元素。

同步映射確保containsKey(),get()和put()如今是原子的,因此從多個線程使用它們不會損害映射的rep不變量。
可是這三個操做如今能夠以任意方式相互交織,這可能會破壞緩存中須要的不變量:若是緩存將整數x映射到值f,那麼當且僅當f爲真時x是素數。
若是緩存永遠失敗這個不變量,那麼咱們可能會返回錯誤的結果。

注意

咱們必須爭論containsKey(),get()和put()之間的競爭不會威脅到這個不變量。

  • containsKey()和get()之間的競爭是無害的,由於咱們從不從緩存中刪除項目 - 一旦它包含x的結果,它將繼續這樣作。
  • containsKey()和put()之間存在競爭。 所以,最終可能會有兩個線程同時測試同一個x的初始值,而且兩個線程都會調用put()與答案。 可是他們都應該用相同的答案來調用put(),因此不管哪一個人贏得比賽並不重要 - 結果將是相同的。

......在註釋中自證線程
須要對安全性進行這種仔細的論證 - 即便在使用線程安全數據類型時 - 也是併發性很難的主要緣由。

一個簡短的總結
經過共享可變數據的競爭條件實現安全的三種主要方式:

  • 禁閉:不共享數據。
  • 不變性:共享,但保持數據不變。
  • 線程安全數據類型:將共享的可變數據存儲在單個線程安全數據類型中。

減小錯誤保證安全。

  • 咱們正試圖消除一大類併發錯誤,競爭條件,並經過設計消除它們,而不只僅是意外的時間。

容易明白。

  • 應用這些通用的,簡單的設計模式比關於哪一種線程交叉是可能的而哪些不可行的複雜論證更容易理解。

準備好改變。

  • 咱們在一個線程安全參數中明確地寫下這些理由,以便維護程序員知道代碼依賴於線程安全。

Strategy 4: Locks and Synchronization

最複雜也最有價值的threadsafe策略

回顧
數據類型或函數的線程安全性:在從多個線程使用時行爲正確,不管這些線程如何執行,無需額外協調。線程安全不該依賴於偶然

原理:併發程序的正確性不該該依賴於時間事件。

有四種策略可使代碼安全併發:

  • 監禁:不要在線程之間共享數據。
  • 不變性:使共享數據不可變。
  • 使用現有的線程安全數據類型:使用爲您協調的數據類型。

前三種策略的核心思想:

  • 避免共享→即便共享,也只能讀/不可寫(immutable)→即便可寫(mutable),共享的可寫數據應該本身具有在多線程之間協調的能力,即「使用線程安全的mutable ADT」

同步和鎖定

因爲共享可變數據的併發操做致使的競爭條件是災難性的錯誤 - 難以發現,重現和調試 - 咱們須要一種共享內存的併發模塊以實現彼此同步的方式。
不少時候,沒法知足上述三個條件...
使代碼安全併發的第四個策略是:

  • 同步和鎖:防止線程同時訪問共享數據。

程序員來負責多線程之間對可變數據的共享操做,經過「同步」策略,避免多線程同時訪問數據

鎖是一種同步技術。

  • 鎖是一種抽象,最多容許一個線程擁有它。保持鎖定是一條線程告訴其餘現成:「我正在改變這個東西,如今不要觸摸它。」
  • 使用鎖機制,得到對數據的獨家改變權,其餘線程被阻塞,不得訪問

使用鎖能夠告訴編譯器和處理器你正在同時使用共享內存,因此寄存器和緩存將被刷新到共享存儲,確保鎖的全部者始終查看最新的數據。
阻塞通常意味着一個線程等待(再也不繼續工做)直到事件發生。

兩種鎖定操做

acquire容許線程獲取鎖的全部權。

  • 若是一個線程試圖獲取當前由另外一個線程擁有的鎖,它會阻塞,直到另外一個線程釋放該鎖。
  • 在這一點上,它將與任何其餘嘗試獲取鎖的線程競爭。
  • 一次只能有一個線程擁有該鎖。

release放棄鎖的全部權,容許另外一個線程得到它的全部權。

  • 若是另外一個線程(如線程2)持有鎖l,線程1上的獲取(l)將會阻塞。它等待的事件是線程2執行釋放(l)。
  • 此時,若是線程1能夠獲取l,則它繼續運行其代碼,並擁有鎖的全部權。
  • 另外一個線程(如線程3)也可能在獲取(l)時被阻塞。線程1或3將採起鎖定並繼續。另外一個將繼續阻塞,再次等待釋放(l)。

(1)同步塊和方法

鎖定

鎖是如此經常使用以致於Java將它們做爲內置語言功能提供。鎖是Java的語言提供的內嵌機制

  • 每一個對象都有一個隱式關聯的鎖 - 一個String,一個數組,一個ArrayList,每一個類及其全部實例都有一個鎖。
  • 即便是一個不起眼的Object也有一個鎖,所以裸露的Object一般用於顯式鎖定:

可是,您不能在Java的內部鎖上調用acquire和release。 而是使用synchronized語句來獲取語句塊持續時間內的鎖定:
像這樣的同步區域提供互斥性:一次只能有一個線程處於由給定對象的鎖保護的同步區域中。
換句話說,你回到了順序編程世界,一次只運行一個線程,至少就其餘同步區域而言,它們指向同一個對象。

鎖定保護對數據的訪問

鎖用於保護共享數據變量。鎖保護共享數據

  • 若是全部對數據變量的訪問都被相同的鎖對象保護(被同步塊包圍),那麼這些訪問將被保證爲原子 - 不被其餘線程中斷。

使用如下命令獲取與對象obj關聯的鎖定:
synchronized(obj){...}

  • 它阻止其餘線程進入synchronized(obj)直到線程t完成其同步塊爲止。

鎖只與其餘獲取相同鎖的線程相互排斥。 全部對數據變量的訪問必須由相同的鎖保護。 注意:要互斥,必須使用同一個鎖進行保護

  • 你能夠在單個鎖後面保護整個變量集合,可是全部模塊必須贊成他們將得到並釋放哪一個鎖。

監視器模式

在編寫類的方法時,最方便的鎖是對象實例自己,即this。用ADT本身作鎖
做爲一種簡單的方法,咱們能夠經過在synchronized(this)內包裝全部對rep的訪問來守護整個類的表示。
監視器模式:監視器是一個類,它們的方法是互斥的,因此一次只能有一個線程在類的實例中。
每個觸及表示的方法都必須用鎖來保護,甚至像length()和toString()這樣的顯而易見的小代碼。
這是由於必須保護讀取以及寫入 - 若是讀取未被保留,則他們可能可以看處處於部分修改狀態的rep。
若是將關鍵字synchronized添加到方法簽名中,Java將像您在方法主體周圍編寫synchronized(this)同樣操做。

同步方法

同一對象上的同步方法的兩次調用不可能交錯。對同步的方法,多個線程執行它時不容許交錯,也就是說「按原子的串行方式執行」

  • 當一個線程正在爲一個對象執行一個同步方法時,全部其餘調用同一對象的同步方法的線程將阻塞(暫停執行),直到第一個線程完成對象。
  • 當一個同步方法退出時,它會自動創建與同一對象的同步方法的任何後續調用之間的發生前關係。
  • 這保證對全部線程均可見對象狀態的更改。

同步語句/塊

同步方法和同步(this)塊之間有什麼區別?

  • 與synchronized方法不一樣,synchronized語句必須指定提供內部鎖的對象。
  • 同步語句對於經過細粒度同步來提升併發性很是有用。

兩者有何區別?

  • 後者須要顯式的給出鎖,且不必定非要是this
  • 後者可提供更細粒度的併發控制

鎖定規則

鎖定規則是確保同步代碼是線程安全的策略。
咱們必須知足兩個條件:

  • 每一個共享的可變變量必須由某個鎖保護。除了在獲取該鎖的同步塊內,數據可能不會被讀取或寫入。任何共享的可變變量/對象必須被鎖所保護
  • 若是一個不變量涉及多個共享的可變變量(它甚至可能在不一樣的對象中),那麼涉及的全部變量都必須由相同的鎖保護。一旦線程得到鎖定,必須在釋放鎖定以前從新創建不變量。涉及到多個mutable變量的時候,它們必須被一個鎖所保護

這裏使用的監視器模式知足這兩個規則。表明中全部共享的可變數據 - 表明不變量依賴於 - 都被相同的鎖保護。

發生-前關係

這種發生-前關係,只是保證多個線程共享的對象經過一個特定語句寫入的內容對另外一個讀取同一對象的特定語句是可見的。
這是爲了確保內存一致性。
發生-前關係(a→ b)是兩個事件的結果之間的關係,所以若是在事件發生以前發生一個事件,那麼結果必須反映出,即便這些事件其實是無序執行的。

  • 這涉及基於併發系統中的事件對的潛在因果關係對事件進行排序。
  • 它由Leslie Lamport制定。

正式定義爲事件中最不嚴格的部分順序,以便:

  • 若是事件a和b在同一個過程當中發生,若是在事件b發生以前發生了事件a則a→b;
  • 若是事件a是發送消息,而且事件b是在事件a中發送的消息的接收,則a→b。

像全部嚴格的偏序同樣,發生-前關係是傳遞的,非自反的和反對稱的。

原子數據訪問的關鍵字volatile

使用volatile(不穩定)變量可下降內存一致性錯誤的風險,由於任何對volatile變量的寫入都會在後續讀取該變量的同時創建happen-before關係。
這意味着對其餘線程老是可見的對volatile變量的更改。
更重要的是,這也意味着當一個線程讀取一個volatile變量時,它不只會看到volatile的最新變化,還會看到致使變化的代碼的反作用。
這是一個輕量級同步機制。
使用簡單的原子變量訪問比經過同步代碼訪問這些變量更有效,但須要程序員更多的關注以免內存一致性錯誤。

(3)處處使用同步?

那麼線程安全是否只需將synchronized關鍵字放在程序中的每一個方法上?
不幸的是,
首先,你實際上並不想同步方法。

  • 同步對您的程序形成很大的損失。 同步機制給性能帶來極大影響
  • 因爲須要獲取鎖(並刷新高速緩存並與其餘處理器通訊),所以進行同步方法調用可能須要更長的時間。
  • 因爲這些性能緣由,Java會將許多可變數據類型默認爲不一樣步。當你不須要同步時,不要使用它。除非必要,不然不要用.Java中不少mutable的類型都不是threadsafe就是這個緣由

另外一個以更慎重的方式使用同步的理由是,它最大限度地減小了訪問鎖的範圍。儘量減少鎖的範圍

  • 爲每一個方法添加同步意味着你的鎖是對象自己,而且每一個引用了你的對象的客戶端都會自動引用你的鎖,它能夠隨意獲取和釋放。
  • 您的線程安全機制所以是公開的,可能會受到客戶的干擾。

與使用做爲表示內部對象的鎖並使用synchronized()塊適當並節省地獲取相比。
最後,處處使用同步並不夠實際。

  • 在沒有思考的狀況下同步到一個方法上意味着你正在獲取一個鎖,而不考慮它是哪一個鎖,或者是否它是保護你將要執行的共享數據訪問的正確鎖。

假設咱們試圖經過簡單地將synchronized同步到它的聲明來解決findReplace的同步問題:
public static synchronized boolean findReplace(EditBuffer buf, ...)

  • 它確實會得到一個鎖 - 由於findReplace是一個靜態方法,它將獲取findReplace剛好處於的整個類的靜態鎖定,而不是實例對象鎖定。
  • 結果,一次只有一個線程能夠調用findReplace - 即便其餘線程想要在不一樣的緩衝區上運行,這些緩衝區應該是安全的,它們仍然會被阻塞,直到單個鎖被釋放。因此咱們會遭受重大的性能損失。

synchronized關鍵字不是萬能的。
線程安全須要一個規範 - 使用監禁,不變性或鎖來保護共享數據。
這個紀律須要被寫下來,不然維護人員不會知道它是什麼。

Synchronized不是靈丹妙藥,你的程序須要嚴格遵照設計原則,先試試其餘辦法,實在作不到再考慮lock。
全部關於線程的設計決策也都要在ADT中記錄下來。

(4)活性:死鎖,飢餓和活鎖

活性
併發應用程序的及時執行能力被稱爲活躍性。
三個子度量標準:

  • 死鎖
  • 飢餓
  • 活鎖

(1)死鎖

若是使用得當,當心,鎖能夠防止競爭情況。
可是接下來的另外一個問題就是醜陋的頭腦。
因爲使用鎖須要線程等待(當另外一個線程持有鎖時獲取塊),所以可能會陷入兩個線程正在等待對方的狀況 - 所以都沒法取得進展。
死鎖描述了兩個或更多線程永遠被阻塞的狀況,等待對方。
死鎖:多個線程競爭鎖,相互等待對方釋放鎖
當併發模塊卡住等待對方執行某些操做時發生死鎖。
死鎖可能涉及兩個以上的模塊:死鎖的信號特徵是依賴關係的一個循環,例如, A正在等待B正在等待C正在等待A,它們都沒有取得進展。
死鎖的醜陋之處在於它
線程安全的鎖定方法很是強大,可是(與監禁和不可變性不一樣)它將阻塞引入程序。
線程必須等待其餘線程退出同步區域才能繼續。
在鎖定的狀況下,當線程同時獲取多個鎖時會發生死鎖,而且兩個線程最終被阻塞,同時持有鎖,每一個鎖都等待另外一個鎖釋放。
不幸的是,監視器模式使得這很容易作到。

死鎖:

  • 線程A獲取harry鎖(由於friend方法是同步的)。
  • 而後線程B獲取snape上的鎖(出於一樣的緣由)。
  • 他們都獨立地更新他們各自的表明,而後嘗試在另外一個對象上調用friend() - 這要求他們獲取另外一個對象上的鎖。

因此A正在拿着哈利等着斯內普,而B正拿着斯內普等着哈利。

  • 兩個線程都卡在friend()中,因此都不會管理退出同步區域並將鎖釋放到另外一個區域。
  • 這是一個經典的致命的擁抱。 該程序中止。

問題的實質是獲取多個鎖,並在等待另外一個鎖釋放時持有某些鎖。

死鎖解決方案1:鎖定順序

對須要同時獲取的鎖定進行排序,並確保全部代碼按照該順序獲取鎖定。

  • 在示例中,咱們可能老是按照嚮導的名稱按字母順序獲取嚮導對象上的鎖定。

雖然鎖定順序頗有用(特別是在操做系統內核等代碼中),但它在實踐中有許多缺點。
首先,它不是模塊化的 - 代碼必須知道系統中的全部鎖,或者至少在其子系統中。
其次,代碼在獲取第一個鎖以前可能很難或不可能確切知道它須要哪些鎖。 它可能須要作一些計算來弄清楚。

  • 例如,想想在社交網絡圖上進行深度優先搜索,在你開始尋找它們以前,你怎麼知道哪些節點須要被鎖定?

死鎖解決方案2:粗略鎖定

要使用粗略鎖定 - 使用單個鎖來防止許多對象實例,甚至是程序的整個子系統。

  • 例如,咱們可能對整個社交網絡擁有一個鎖,而且對其任何組成部分的全部操做都在該鎖上進行同步。
  • 在代碼中,全部的巫師都屬於一個城堡,咱們只是使用該Castle對象的鎖來進行同步。

可是,它有明顯的性能損失。

  • 若是你用一個鎖保護大量可變數據,那麼你就放棄了同時訪問任何數據的能力。
  • 在最糟糕的狀況下,使用單個鎖來保護全部內容,您的程序可能基本上是順序的。

(2)飢餓

飢餓描述了線程沒法得到對共享資源的按期訪問而且沒法取得進展的狀況。

  • 當共享資源被「貪婪」線程長時間停用時,會發生這種狀況。

例如,假設一個對象提供了一個常常須要很長時間才能返回的同步方法。

  • 若是一個線程頻繁地調用此方法,那麼其餘線程也須要常常同步訪問同一對象。

由於其餘線程鎖時間太長,一個線程長時間沒法獲取其所需的資源訪問權(鎖),致使沒法往下進行。

(3)活鎖

線程一般會響應另外一個線程的動做而行動。
若是另外一個線程的動做也是對另外一個線程動做的響應,則可能致使活鎖。
與死鎖同樣,活鎖線程沒法取得進一步進展。
可是,線程並未被阻止 - 他們只是忙於響應對方恢復工做。
這與兩個試圖在走廊上相互傳遞的人至關:

  • 阿爾方塞向左移動讓加斯頓經過,而加斯東向右移動讓阿爾方塞經過。
  • 看到他們仍然互相阻攔,阿爾方塞向右移動,而加斯東向左移動。他們仍然互相阻攔,因此......

(5)wait(),notify()和notifyAll()

保護塊

防禦區塊:這樣的區塊首先輪詢一個必須爲真的條件才能繼續。
假設,例如guardedJoy是一種方法,除非另外一個線程設置了共享變量joy,不然該方法不能繼續。

  • 這種方法能夠簡單地循環直到知足條件,可是該循環是浪費的,由於它在等待時連續執行。 某些條件未獲得知足,因此一直在空循環檢測,直到條件被知足。這是極大浪費。

wait(),notify()和notifyAll()

如下是針對任意Java對象o定義的:

  • o.wait():釋放o上的鎖,進入o的等待隊列並等待
  • o.notify():喚醒o的等待隊列中的一個線程
  • o.notifyAll():喚醒o的等待隊列中的全部線程

Object.wait()

Object.wait()會致使當前線程等待,直到另外一個線程調用此對象的notify()方法或notifyAll()方法。換句話說,這個方法的行爲就好像它只是執行調用wait(0)同樣。該操做使對象所處的阻塞/等待狀態,直到其餘線程調用該對象的notify()操做

Object.notify()/ notifyAll()

Object.notify()喚醒正在等待該對象監視器的單個線程。若是任何線程正在等待這個對象,則選擇其中一個線程來喚醒。隨機選擇一個在該對象上調用等方法的線程,解除其阻塞狀態

  • 線程經過調用其中一個等待方法在對象的監視器上等待。
  • 在當前線程放棄對該對象的鎖定以前,喚醒的線程將沒法繼續。
  • 喚醒的線程將以一般的方式與其餘可能正在主動競爭的線程競爭對該對象進行同步;例如,被喚醒的線程在做爲下一個線程來鎖定這個對象時沒有可靠的特權或缺點。

此方法只應由做爲此對象監視器全部者的線程調用。
線程以三種方式之一成爲對象監視器的全部者:

  • 經過執行該對象的同步實例方法。
  • 經過執行同步對象的同步語句的主體。
  • 對於Class類型的對象,經過執行該類的同步靜態方法。

在守衛塊中使用wait()

wait()的調用不會返回,直到另外一個線程發出某個特殊事件可能發生的通知 - 儘管不必定是該線程正在等待的事件。
Object.wait()會致使當前線程等待,直到另外一個線程調用此對象的notify()方法或notifyAll()方法。
當wait()被調用時,線程釋放鎖並暫停執行。
在未來的某個時間,另外一個線程將得到相同的鎖並調用Object.notifyAll(),通知全部等待該鎖的線程發生重要事件:
第二個線程釋放鎖定一段時間後,第一個線程從新獲取鎖定,並經過從等待的調用返回來恢復。

wait(),notify()和notifyAll()
調用對象o的方法的線程一般必須預先鎖定o:

如何制定安全性論據

回想一下:開發ADT的步驟
指定:定義操做(方法簽名和規約)。
測試:開發操做的測試用例。測試套件包含基於對操做的參數空間進行分區的測試策略。
表明:選擇一個表明。

  • 首先實現一個簡單的,強大的表明。
  • 寫下rep不變和抽象函數,並實現checkRep(),它在每一個構造函數,生成器和增量器方法的末尾聲明瞭rep不變量。

+++同步

  • 說出你的表明是線程安全的。
  • 在你的類中做爲註釋明確地寫下來,直接用rep不變量表示,以便維護者知道你是如何爲類設計線程安全性的。

作一個安全論證

併發性很難測試和調試!
因此若是你想讓本身和別人相信你的併發程序是正確的,最好的方法是明確地說明它沒有競爭,而且記下來。在代碼中註釋的形式增長說明:該ADT採起了什麼設計決策來保證線程安全

  • 安全性參數須要對模塊或程序中存在的全部線程及其使用的數據進行編目,並針對您使用的四種技術中的哪種來防止每一個數據對象或變量的競爭:監禁,不可變性,線程安全數據類型或同步。採起了四種方法中的哪種?
  • 當你使用最後兩個時,你還須要爭辯說,對數據的全部訪問都是適當的原子
  • 也就是說,你所依賴的不變量不受交織威脅。若是是後兩種,還需考慮對數據的訪問都是原子的,不存在交錯

用於監禁的線程安全論證

由於您必須知道系統中存在哪些線程以及他們有權訪問哪些對象,所以在咱們僅就數據類型進行爭論時,監禁一般不是一種選擇。 除非你知道線程訪問的全部數據,不然Confinement沒法完全保證線程安全

  • 若是數據類型建立了本身的一組線程,那麼您能夠討論關於這些線程的監禁。
  • 不然,線程從外部進入,攜帶客戶端調用,而且數據類型可能沒法保證哪些線程具備對什麼的引用。

所以,在這種狀況下,Confinement不是一個有用的論證。

  • 一般咱們在更高層次使用約束,討論整個系統,並論證爲何咱們不須要線程安全的某些模塊或數據類型,由於它們不會經過設計在線程間共享。除非是在ADT內部建立的線程,能夠清楚得知訪問數據有哪些

總結

併發程序設計的目標

併發程序是否能夠避免bug?

咱們關心三個屬性:

  • 安全。 併發程序是否知足其不變量和規約? 訪問可變數據的競爭會威脅到安全。 安全問題:你能證實一些很差的事情從未發生過?
  • 活性。 程序是否繼續運行,並最終作你想作的事情,仍是會陷入永遠等待事件永遠不會發生的地方? 你能證實最終會發生什麼好事嗎? 死鎖威脅到活性。
  • 公平。 併發模塊具備處理能力以在計算上取得進展。 公平主要是OS線程調度器的問題,可是你能夠經過設置線程優先級來影響它。

實踐中的併發

在真正的項目中一般採用什麼策略?

  • 庫數據結構不使用同步(爲單線程客戶端提供高性能,同時讓多線程客戶端在頂層添加鎖定)或監視器模式。
  • 具備許多部分的可變數據結構一般使用粗粒鎖定或線程約束。大多數圖形用戶界面工具包遵循如下方法之一,由於圖形用戶界面基本上是一個可變對象的大型可變樹。 Java Swing,圖形用戶界面工具包,使用線程約束。只有一個專用線程被容許訪問Swing的樹。其餘線程必須將消息傳遞到該專用線程才能訪問該樹。

安全失敗帶來虛假的安全感。生存失敗迫使你面對錯誤。有利於活躍而不是安全的誘惑。

  • 搜索一般使用不可變的數據類型。多線程很容易,由於涉及的全部數據類型都是不可變的。不會有競爭或死鎖的風險。
  • 操做系統一般使用細粒度的鎖來得到高性能,並使用鎖定順序來處理死鎖問題。
  • 數據庫使用與同步區域相似的事務來避免競爭條件,由於它們的影響是原子的,但它們沒必要獲取鎖定,儘管事務可能會失敗並在事件發生時被回滾。數據庫還能夠管理鎖,並自動處理鎖定順序。將在數據庫系統課程中介紹。

總結

生成一個安全無漏洞,易於理解和能夠隨時更改的併發程序須要仔細思考。

  • 只要你嘗試將它們固定下來,Heisenbugs就會消失,因此調試根本不是實現正確線程安全代碼的有效方法。
  • 線程能夠以許多不一樣的方式交錯操做,即便是全部可能執行的一小部分,也永遠沒法測試。

建立關於數據類型的線程安全參數,並在代碼中記錄它們。
獲取一個鎖容許一個線程獨佔訪問該鎖保護的數據,強制其餘線程阻塞 - 只要這些線程也試圖獲取同一個鎖。
監視器使用經過每種方法獲取的單個鎖來引用數據類型的表明。
獲取多個鎖形成的阻塞會形成死鎖的可能性。
什麼是併發編程?
進程,線程和時間片
交織和競爭條件
線程安全

  • 戰略1:監禁
  • 策略2:不可變性
  • 策略3:使用線程安全數據類型
  • 策略4:鎖定和同步

如何作安全論證

相關文章
相關標籤/搜索