什么是線程安全?
線程安全,有兩個(gè)重要的特征說明:“共享”和“可變”憎蛤。
- 共享是指可以被多個(gè)線程同時(shí)訪問;
- 可變是指變量的值在生命周期內(nèi)是可以變化的孽鸡;
如何實(shí)現(xiàn)線程安全
- 一個(gè)對象是否需要線程安全的蹂午,取決于它是否被多個(gè)線程訪問;
- 如何保證一個(gè)對象的線程安全彬碱,則需要采用同步機(jī)制來協(xié)同對對象可變狀態(tài)的訪問豆胸;
- 給線程安全下一個(gè)明確的定義:當(dāng)多個(gè)線程訪問這個(gè)對象或者資源時(shí),如果這個(gè)對象或資源始終都能表現(xiàn)出數(shù)據(jù)的一致性的狀態(tài)巷疼,那么就稱這個(gè)對象或者資源是線程安全的晚胡;
數(shù)據(jù)資源的有無狀態(tài)化
- 無狀態(tài)的對象一定是線程安全的。
- 有狀態(tài)的對象嚼沿,多線程環(huán)境下估盘,多個(gè)線程共享資源,且進(jìn)行的不是原子性操作骡尽,這個(gè)時(shí)候就要考慮線程的安全控制問題
比如:count++遣妥,其實(shí)是不具備原子性的,因?yàn)檫@個(gè)步驟實(shí)際會(huì)被拆分為三個(gè)步驟攀细,即 讀取箫踩、修改和寫入爱态,而這三個(gè)步驟有可能在某個(gè)時(shí)刻因 CPU 時(shí)間片的切換問題,而只執(zhí)行其中一兩個(gè)步驟境钟,這就不具備原子性锦担。
原子化能力支持
在 Java 中,為了解決這個(gè)問題慨削,java.util.concurrent.atomic 包提供了很多的類洞渔,來保證數(shù)據(jù)操作的原子性,比如我們之前的程序可以修改為
- 基本數(shù)據(jù)類型 AtomicInteger
- 數(shù)組類型 AtomicIntegerArray
AtomicInteger integer = new AtomicInteger(0);
integer.incrementAndGet()
內(nèi)部的原理是采用了 CAS 機(jī)制
那么什么是 CAS 機(jī)制缚态?
CAS 有人翻譯為 Compare And Set 或 Compare And Swap 都是正確的磁椒。
多線程并發(fā)執(zhí)行的狀態(tài)下,鎖的狀態(tài)改變猿规,基本都是使用 CAS 原理衷快,它有一個(gè)比較別扭的叫法“CPU 硬件同步原語”,算法是基于 CPU 硬件的姨俩,原子性操作蘸拔,不會(huì)被其他線程打斷。
CAS 的算法环葵,比較當(dāng)前值和期望的值是否相等调窍,如果相等,則將當(dāng)前值賦予一個(gè)新值张遭。
再比如修改一個(gè) Boolean 的類型的變量的值邓萨,我們也可以采用
private AtomicBoolean atomicBoolean = new AtomicBoolean(false);
public void lock(){
//期望是false,如果是false菊卷,則可以修改為true
atomicBoolean.compareAndSet(false, true);
}
同步鎖機(jī)制支持
只要程序中存在“先判斷缔恳,再更新”,那么就要保證這兩個(gè)操作在一個(gè)原子操作里面洁闰,才能保證線程安全歉甚。
public synchronized int getCount(){
return count++;
}
Java 鎖機(jī)制的一些特點(diǎn)
監(jiān)視鎖、互斥鎖扑眉、可重入鎖都是在這個(gè)鎖的特點(diǎn)纸泄。
- 監(jiān)視鎖:java 的每一個(gè)對象都可以用來做監(jiān)視鎖,也就是為什么我們的 wait腰素、notify 方法定義在 Object 類的原因聘裁。
- 互斥鎖:表示最多只有一個(gè)線程可以持有這把鎖。
- 可重入鎖:是指當(dāng)線程 A 請求一個(gè)由線程 B 持有的鎖時(shí)弓千,線程 B 會(huì)進(jìn)入阻塞狀態(tài)衡便;而如果線程 A 如果再訪問另一段代碼,而這個(gè)代碼的鎖是已經(jīng)被線程 A 持有的,這個(gè)時(shí)候請求是可以成功的镣陕,這就叫可重入征唬。
Java 鎖機(jī)制的簡單原理
JVM 為每個(gè)鎖設(shè)置兩個(gè)屬性,獲取計(jì)數(shù)值和所有者線程茁彭,當(dāng)計(jì)數(shù)值為 0 時(shí),這個(gè)鎖就被認(rèn)為是沒有被任何線程持有扶歪,當(dāng)線程請求一個(gè)未被持有的鎖時(shí)理肺,JVM 將記錄鎖的持有者,并且計(jì)數(shù)值+1善镰。
如果同一個(gè)線程再次獲取這個(gè)鎖妹萨,則計(jì)數(shù)值將遞增,而當(dāng)線程退出同步代碼塊時(shí)炫欺,計(jì)數(shù)器會(huì)相應(yīng)遞減乎完,當(dāng)計(jì)數(shù)值為 0,這個(gè)鎖將被釋放品洛。
活躍性問題
承接上面解決安全性的問題分析树姨,鎖機(jī)制會(huì)存在活躍性問題,比如:死鎖桥状,饑餓帽揪,活鎖,這些都是屬于活躍性問題辅斟。
死鎖
多個(gè)線程转晰,各自占對方的資源,都不愿意釋放士飒,從而造成死鎖查邢,A 線程需要等待的鎖被 B 線程占用,而 B 線程需要的等待的鎖被 A 線程占用酵幕,所以相互都不釋放扰藕,于是就陷入了死鎖。
饑餓
多個(gè)線程訪問同一個(gè)同步資源裙盾,有些線程總是沒有機(jī)會(huì)得到互斥鎖实胸,這種就叫做饑餓。
出現(xiàn)饑餓的三種情況
- 高優(yōu)先級的線程吞噬了低優(yōu)先級的線程的 CPU 時(shí)間片
- 理論上來說番官,線程優(yōu)先級高的線程會(huì)比線程優(yōu)先級低的線程獲得更多的執(zhí)行機(jī)會(huì)庐完,但是 java 的線程優(yōu)先級不是絕對出現(xiàn)這樣的效果。
- 一般而言:優(yōu)先級高的出現(xiàn)頻率會(huì)比優(yōu)先級低的高很多
- 不同的操作系統(tǒng)對線程的優(yōu)先級支持是不同的徘熔,規(guī)定是在 1-10 之間门躯,java 通過 3 個(gè)常量來屏蔽這種操作系統(tǒng)的底層差異化。
- 線程被永久阻塞在等待進(jìn)入同步代碼塊的狀態(tài)
- 等待的線程永遠(yuǎn)不被喚醒
建議大家采用公平鎖來代替 synchronized 這種互斥鎖
活鎖
兩個(gè)人在走廊上碰見酷师,大家都互相很有禮貌讶凉,互相禮讓染乌,A 從左到右,B 也從從左轉(zhuǎn)向右懂讯,發(fā)現(xiàn)又擋住了地方荷憋,繼續(xù)轉(zhuǎn)換方向,但又碰到了褐望,反反復(fù)復(fù)勒庄,一直沒有機(jī)會(huì)運(yùn)行下去。