前言
在構(gòu)建并發(fā)程序時(shí),必須正確的使用線程和鎖。編寫(xiě)線程安全的代碼的核心在于要對(duì)狀態(tài)訪問(wèn)操作進(jìn)行管理,特別是對(duì)共享和可變狀態(tài)的訪問(wèn)。
共享:變量可以由多個(gè)線程同時(shí)訪問(wèn)
可變:變量的值在其生命周期內(nèi)可以發(fā)生變化
初探
一個(gè)對(duì)象是否需要是線程安全的,取決于它是否會(huì)被多個(gè)線程訪問(wèn)。注意,這指得是程序中訪問(wèn)對(duì)象的方式,而不是對(duì)象要實(shí)現(xiàn)的功能。
線程安全的對(duì)象
有三種方式可以讓一個(gè)可變狀態(tài)的的變量成為一個(gè)線程安全的對(duì)象:
- 不在線程之間共享該變量
- 將這個(gè)變量修改為不可變的變量
- 在訪問(wèn)變量時(shí)使用同步
同步機(jī)制
采用同步機(jī)制來(lái)協(xié)同對(duì)對(duì)象可變狀態(tài)的訪問(wèn)可以讓該對(duì)象變的線程安java中的提供的實(shí)現(xiàn)同步機(jī)制的辦法包括:
- synchronizend,一種獨(dú)占鎖的方式;
- volatile類(lèi)型的變量;
- 顯示鎖(Explicit Lock);
- 原子變量;
線程安全性
當(dāng)多個(gè)線程訪問(wèn)某個(gè)類(lèi)時(shí),不管運(yùn)行時(shí)環(huán)境采用何種調(diào)度方式或者這些線程將如何交替執(zhí)行,并且在主調(diào)試代碼中不需要任何額外的同步或者協(xié)同,這個(gè)類(lèi)都能表現(xiàn)出正確的行為,那么就稱(chēng)這個(gè)類(lèi)是線程安全的。
在定義中,最核心的概念就是正確性。其含義為:<u>類(lèi)的行為與其規(guī)范的完全一致</u>
讓我們看一個(gè)不正確的典范,為售票處設(shè)計(jì)一個(gè)買(mǎi)票系統(tǒng),五個(gè)窗口賣(mài)五張票:
public class Ticket extends Thread {
private int count = 5;
@Override
public void run() {
super.run();
count--;
System.out.println(this.currentThread().getName() + ":" + count);
}
}
public class Sale {
public static void main(String[] args){
Ticket ticket = new Ticket();
Thread a = new Thread(ticket,"A");
Thread b = new Thread(ticket,"B");
Thread c = new Thread(ticket,"C");
Thread d = new Thread(ticket,"D");
Thread e = new Thread(ticket,"E");
a.start();
b.start();
c.start();
d.start();
e.start();
}
}
這段代碼的輸出結(jié)果是不確定的,輸出的結(jié)果可能是下面這樣的:
這就是一個(gè)
不安全的典范,這段代碼所產(chǎn)生的行為跟我們預(yù)期設(shè)計(jì)的完全不一致。我們稍微改變一下需求,將五個(gè)窗口賣(mài)五張票改為五個(gè)窗口各賣(mài)五張票。修改代碼為:
public class TicketNoState extends Thread {
//private int count = 5;
@Override
public void run() {
super.run();
int count = 5;
do {
count--;
System.out.println(this.currentThread().getName() + ":" + count);
} while (count > 0);
}
}
這段代碼不會(huì)產(chǎn)生前面代碼所犯的錯(cuò)誤,TicketNoState是無(wú)狀態(tài)的:
- 不包含任何域
- 不包含任何對(duì)其他類(lèi)中域的引用
計(jì)算過(guò)程中的臨時(shí)狀態(tài)僅存在于線程棧上的局部變量中,并且只能由正在執(zhí)行的線程訪問(wèn)。訪問(wèn)TicketNoState的線程不會(huì)影響另一個(gè)線程的計(jì)算過(guò)程和結(jié)果,因?yàn)檫@兩個(gè)線程并沒(méi)有共享狀態(tài),就好像他們?cè)谠L問(wèn)不同的實(shí)例。
<u>無(wú)狀態(tài)對(duì)象一定是線程安全的</u>
原子性
一個(gè)原子操作是一個(gè)不能分割的整體,沒(méi)有其他線程能夠中斷或檢查正處于原子操作中的變量。原子操作在沒(méi)有鎖的情況下可以做到線程安全。
回憶上文中那個(gè)不安全的賣(mài)票代碼中的count--操作,緊湊的語(yǔ)法使得它看上去只是一個(gè)操作,但它確實(shí)非原子的。實(shí)際上它包含了三個(gè)獨(dú)立的操作:讀取count的值,將其減1,然后將計(jì)算結(jié)果寫(xiě)入count。這是一個(gè)“讀取-計(jì)算-寫(xiě)入”的過(guò)程,上文中錯(cuò)誤的輸出就是由這段操作產(chǎn)生的。
使用原子類(lèi)可以實(shí)現(xiàn)對(duì)count的原子操作:
public class Ticket extends Thread {
private AtomicInteger count = new AtomicInteger(5);
@Override
public void run() {
super.run();
System.out.println(this.currentThread().getName() + "賣(mài)出去了一張:" + count.getAndDecrement());
}
}
注意:原子類(lèi)在具有有邏輯性的情況下也是不安全的。因?yàn)榉椒ㄖg的調(diào)用不是原子的。
在原子操作中,對(duì)于訪問(wèn)同一個(gè)狀態(tài)的所有操作來(lái)說(shuō),這個(gè)操作是以原子方式執(zhí)行的操作。假如存在兩個(gè)操作A和B,如果從執(zhí)行A的線程來(lái)看,當(dāng)另一個(gè)線程執(zhí)行B的時(shí)候,要么將B全部執(zhí)行完,要么完全不執(zhí)行B,那么A和B彼此是原子的。與原子操作對(duì)應(yīng)的就是復(fù)合操作:包含了一組必須以原子方式執(zhí)行的操作以確保線程安全性的操作。
競(jìng)態(tài)條件
程序的結(jié)果取決于運(yùn)氣!
如果程序運(yùn)行順序的改變會(huì)影響最終結(jié)果,這就是一個(gè)競(jìng)態(tài)條件
也就是說(shuō)當(dāng)某個(gè)計(jì)算的正確性取決于多個(gè)線程的交替執(zhí)行序時(shí),那么就會(huì)發(fā)生競(jìng)態(tài)條件。
要想理解這個(gè)定義,首先要知道程序運(yùn)行不一定是線性的。不按順序執(zhí)行的典范還是多線程程序。例如上文中的代碼創(chuàng)建的五個(gè)線程,雖然它們是依次啟動(dòng),但他們內(nèi)部的代碼誰(shuí)先執(zhí)行就不得而知了。
如果一段程序運(yùn)行多次的結(jié)果不一致,那這就可能是競(jìng)態(tài)條件的體現(xiàn)。最常見(jiàn)的競(jìng)態(tài)條件類(lèi)型是“先檢查后執(zhí)行”,通過(guò)觀測(cè)一個(gè)結(jié)果來(lái)決定程序下一步的走向,而這個(gè)結(jié)果可能是已經(jīng)失效了的。例如上文中五個(gè)窗口售賣(mài)五張車(chē)票的代碼,多次執(zhí)行會(huì)有不同結(jié)果。
另一個(gè)常見(jiàn)的類(lèi)型就是延遲初始化中的競(jìng)態(tài)條件,觀察下面實(shí)現(xiàn)單例的代碼:
public class LazyInit {
private static LazyInit mLazyInit = null;
public static LazyInit getLazyInit() {
if (mLazyInit == null) {
mLazyInit = new LazyInit();
}
return mLazyInit;
}
}
LazyInit中就包含了一個(gè)競(jìng)態(tài)條件,它可能會(huì)破壞這個(gè)類(lèi)的正確性。假定線程A和線程B同時(shí)執(zhí)行getLazyInit,當(dāng)A先進(jìn)入是,A看到mLazyInit為空,變?nèi)バ陆ㄒ粋€(gè)實(shí)例。當(dāng)A尚未創(chuàng)建成功時(shí),此時(shí)mLazyInit依舊為空,而此時(shí),如果B進(jìn)入,發(fā)現(xiàn)mLazyInit為空,也會(huì)去進(jìn)行初始化的操作。這樣getLazyInit就會(huì)返回兩個(gè)不同的實(shí)例。
加鎖機(jī)制
<u>加鎖機(jī)制可以實(shí)現(xiàn)操作的原子性。</u>
同步代碼塊:Java提供了一種內(nèi)置的鎖機(jī)制來(lái)支持原子性。它包含兩個(gè)部分:
一個(gè)作為鎖的對(duì)象引用 一個(gè)由這個(gè)鎖保護(hù)的代碼塊
例如:
synchronized (lock){
//被保護(hù)的代碼塊
}
內(nèi)置鎖
每個(gè)Java對(duì)象都可以用來(lái)作為一個(gè)同步鎖,即內(nèi)置鎖(監(jiān)視器鎖)。線程在進(jìn)入同步代碼塊之前會(huì)自動(dòng)獲取鎖,并且在退出的時(shí)候釋放鎖。獲得鎖唯一的方法就是進(jìn)入由這個(gè)鎖保護(hù)的同步代碼塊或者方法。
用關(guān)鍵字synchronized修飾整方法就是一種橫跨整個(gè)方法的同步代碼塊
synchronized public static LazyInit getLazyInit() {
if (mLazyInit == null) {
mLazyInit = new LazyInit();
}
return mLazyInit;
}
其中的鎖就是方法調(diào)用所在的對(duì)象。靜態(tài)的synchronized方法以Class對(duì)象作為鎖:
synchronized (LazyInit.class){
//被保護(hù)的代碼塊
}
<u>Java的內(nèi)置鎖是一種互斥鎖,同一時(shí)間內(nèi)最多只有有一個(gè)線程能持有這種鎖,每次只能由一個(gè)線程執(zhí)行鎖保護(hù)起來(lái)的代碼塊,于是這些代碼塊得以以原子的方式執(zhí)行</u>。
重入
內(nèi)置鎖是可以重如的:當(dāng)A線程請(qǐng)求一個(gè)由B線程持有的鎖時(shí),A線程會(huì)被阻塞,如果B線程視圖獲得一個(gè)已經(jīng)由它自己持有的鎖,請(qǐng)求則會(huì)成功,不會(huì)發(fā)生阻塞。(自己可以再次獲取自己持有的鎖)
例如:當(dāng)存在類(lèi)繼承關(guān)系,子類(lèi)可以同過(guò)鎖可重如的特性調(diào)用父類(lèi)的同步方法:
public class Human {
public synchronized void method(){
}
}
public class Student extends Human {
@Override
public synchronized void method() {
//調(diào)用父類(lèi)的同步方法
super.method();
}
}
需要注意的是:簡(jiǎn)單的且粗粒度的使用同步方法確實(shí)能確保線程安全性,但付出的代價(jià)卻極高————被同步的方法每次只能由一個(gè)線程執(zhí)行,無(wú)法同時(shí)處理多個(gè)請(qǐng)求。
總結(jié)
該文為Java并發(fā)系列博客之一,為基礎(chǔ)篇。該系列博客是我學(xué)習(xí)過(guò)程中的總結(jié),希望持續(xù)關(guān)注Java并發(fā)系列博客,積極討論,一起學(xué)習(xí)成長(zhǎng)。