線程安全性
作者:xcbeyond
瘋狂源自夢(mèng)想,技術(shù)成就輝煌!微信公眾號(hào):《程序猿技術(shù)大咖》號(hào)主,專注后端開發(fā)多年,擁有豐富的研發(fā)經(jīng)驗(yàn),樂于技術(shù)輸出、分享,現(xiàn)階段從事微服務(wù)架構(gòu)項(xiàng)目的研發(fā)工作,涉及架構(gòu)設(shè)計(jì)、技術(shù)選型、業(yè)務(wù)研發(fā)等工作。對(duì)于Java、微服務(wù)、數(shù)據(jù)庫、Docker有深入了解,并有大量的調(diào)優(yōu)經(jīng)驗(yàn)。
線程的使用一直是難以把控掌握的。如果使用得當(dāng),線程可以有效地降低程序的開發(fā)和維護(hù)等成本,同時(shí)提升復(fù)雜應(yīng)用程序的性能。在GUI應(yīng)用程序中,提高用戶界面的響應(yīng)靈敏度,在服務(wù)器應(yīng)用程序中,提升資源利用率以及系統(tǒng)吞吐量。
然而,如果使用不當(dāng),線程將會(huì)帶來一系列不可預(yù)估的風(fēng)險(xiǎn)。Java對(duì)線程的支持其實(shí)就是一把雙刃劍。雖然Java明確是一種跨平臺(tái)(編寫異常,隨處運(yùn)行)的語言,JDK并提供了相應(yīng)的類庫,簡化了程序的開發(fā),但更多的在處理一些復(fù)雜程序時(shí),就需要使用線程,隨之引入的“并發(fā)性”問題(線程安全性),就成為了開發(fā)人員實(shí)現(xiàn)考慮的難點(diǎn)。
在多線程中的各個(gè)操作的順序都是不可預(yù)測(cè)的,有時(shí)其執(zhí)行結(jié)果簡直出乎意料,令人驚訝。例如,通過下面代碼片段的數(shù)值序列生成器,用來生成一個(gè)遞增序列。
package com.xcbeyond.thread;
import net.jcip.annotations.NotThreadSafe;
/**
* 非線程安全的數(shù)值序列生成器
* @author xcbeyond
* 2018-5-6下午03:17:33
*/
public class UnSafeThreadSequence {
private int value;
/**
* 返回一個(gè)唯一的值
* @return
*/
public int getValue() {
return value++;
}
}
如果在單線程中執(zhí)行,沒有任何問題,結(jié)果也正如我們預(yù)期的一樣。而在多線程并發(fā)的場(chǎng)景下操作,將會(huì)出現(xiàn)不同線程之間交替操作,不同線程很可能同時(shí)讀取同一個(gè)值,得到相同值,導(dǎo)致不同線程返回了相同的序列值,這恰恰與我們的預(yù)期截然相反。
這充分說明了一種常見的線程并發(fā)危險(xiǎn):競(jìng)爭(zhēng)條件。因?yàn)榫€程共享了相同的內(nèi)存空間地址,且并發(fā)的執(zhí)行,它們可能訪問或修改其他線程正在使用的變量,將會(huì)出現(xiàn)變量競(jìng)爭(zhēng)情況。
線程安全就是多線程訪問時(shí),采用了加鎖機(jī)制,當(dāng)一個(gè)線程訪問該類的某個(gè)數(shù)據(jù)時(shí),進(jìn)行保護(hù),其他線程不能進(jìn)行訪問直到該線程讀取完,其他線程才可使用。不會(huì)出現(xiàn)數(shù)據(jù)不一致或者數(shù)據(jù)污染。 線程不安全就是不提供數(shù)據(jù)訪問保護(hù),有可能出現(xiàn)多個(gè)線程先后更改數(shù)據(jù)造成所得到的數(shù)據(jù)是臟數(shù)據(jù)。
如何編寫線程安全的代碼呢?
編寫線程安全的代碼,核心在于要對(duì)狀態(tài)訪問操作進(jìn)行管理,特別是對(duì)共享的(Shared)和可變的(Mutable)狀態(tài)的訪問。一個(gè)對(duì)象是否需要時(shí)線程安全的,取決于該對(duì)象是否被多線程訪問。這指的是程序中訪問對(duì)象的方式,而不是對(duì)象要實(shí)現(xiàn)的功能。要使得對(duì)象是線程安全的,要采用同步機(jī)制來協(xié)同對(duì)對(duì)象可變狀態(tài)的訪問。Java常用的同步機(jī)制是Synchronized,還包括 volatile類型的變量,顯示鎖以及原子變量。
原子性:
假定有兩個(gè)操作A和B,如果從執(zhí)行A的線程來看,當(dāng)另一個(gè)線程執(zhí)行B時(shí),要么將B完全執(zhí)行完,要么完全不執(zhí)行B,那么A和B對(duì)彼此來說是原子的。原子操作是指:對(duì)于訪問同一個(gè)狀態(tài)的所有操作(包括該操作本身)來說,這個(gè)操作是一個(gè)以原子方式執(zhí)行的操作。
競(jìng)態(tài)條件(Race Condition):某個(gè)計(jì)算/程序的正確性取決于多個(gè)線程的交替執(zhí)行的時(shí)序。(線程的時(shí)序不同,產(chǎn)生的結(jié)果可能會(huì)不同)
“先檢查后執(zhí)行”,即通過一個(gè)潛在的過渡值來決定下一步的操作。
首先觀察到某個(gè)條件為真,然后開始執(zhí)行相關(guān)的程序,但是在多線程的運(yùn)行環(huán)境中,條件判斷的結(jié)果以及開始執(zhí)行程序中間,觀察結(jié)果可能變得無效(另外一個(gè)線程在此期間執(zhí)行了相關(guān)的動(dòng)作),從而導(dǎo)致無效。
“讀取-修改-寫入”,基于對(duì)象之前的狀態(tài)來定義對(duì)象狀態(tài)的轉(zhuǎn)換。即使是volatile修飾的變量,在多線程的環(huán)境里面進(jìn)行自增操作,同樣會(huì)發(fā)生競(jìng)態(tài)條件,所以volatile不能保證絕對(duì)的線程安全。
加鎖機(jī)制:
在線程安全的定義中,多個(gè)線程間的操作無論采用何種執(zhí)行時(shí)序或交替方式,都要保證不變性條件不被破壞。當(dāng)不變性條件中涉及多個(gè)變量時(shí),各個(gè)變量之間并不是互相獨(dú)立的,一個(gè)變量發(fā)生變化會(huì)對(duì)其他變量的值產(chǎn)生約束。因此,一個(gè)變量發(fā)生改變,在同一個(gè)原子操作里面,其他相關(guān)變量也要更新。
內(nèi)置鎖:同步代碼塊(Synchronized Block)包括兩部分:一個(gè)作為鎖的對(duì)象引用,一個(gè)作為由這個(gè)鎖保護(hù)的代碼塊。關(guān)鍵字Synchronized修飾方法就是一種同步代碼塊,鎖就是方法調(diào)用所在的對(duì)象,靜態(tài)的Synchronized方法以Class對(duì)象作為鎖。內(nèi)置鎖或監(jiān)視鎖就是以對(duì)象作為實(shí)現(xiàn)同步的鎖。
內(nèi)置鎖在Java中扮演了互斥鎖的角色,意味著最多只有一個(gè)線程可以擁有鎖,當(dāng)線程A嘗試請(qǐng)求一個(gè)被線程B占用的鎖時(shí),線程A必須等待或者處于阻塞狀態(tài),知道B釋放為止。如果B不釋放鎖,A將永遠(yuǎn)等待下去。
雖然同步代碼塊解決了線程安全問題,但線程可能出現(xiàn)等待或者阻塞情況,導(dǎo)致響應(yīng)性能非常低下。(解決了線程安全問題,但可能會(huì)出現(xiàn)性能問題)可以盡可能的將每個(gè)同步代碼塊的大小進(jìn)行調(diào)整,即:同步代碼塊“足夠小”,在一定程度上解決了線程安全問題,也解決了性能低下的問題。要判斷同步代碼塊的合理大小,需要在各種設(shè)計(jì)需求之間進(jìn)行權(quán)衡,包括安全性( 必須得到滿足)、簡單性和性能。