1. 死鎖的產(chǎn)生條件
計算機(jī)系統(tǒng)中同時具備下面四個必要條件時谴麦,那么會發(fā)生死鎖</br>
- 互斥條件缀皱。即某個資源在一段時間內(nèi)只能由一個進(jìn)程占有,不能同時被兩個或兩個以上的進(jìn)程占有。這種獨占資源如CD-ROM驅(qū)動器洲拇,打印機(jī)等等奈揍,必須在占有該資源的進(jìn)程主動釋放它之后,其它進(jìn)程才能占有該資源赋续。這是由資源本身的屬性所決定的打月。</br>
- 不可搶占條件。進(jìn)程所獲得的資源在未使用完畢之前蚕捉,資源申請者不能強(qiáng)行地從資源占有者手中奪取資源奏篙,而只能由該資源的占有者進(jìn)程自行釋放。</br>
- 占有且申請條件迫淹。進(jìn)程至少已經(jīng)占有一個資源秘通,但又申請新的資源;由于該資源已被另外進(jìn)程占有敛熬,此時該進(jìn)程阻塞肺稀;但是,它在等待新資源之時应民,仍繼續(xù)占用已占有的資源话原。</br>
- 循環(huán)等待條件。存在一個進(jìn)程等待序列{P1诲锹,P2繁仁,...,Pn}归园,其中P1等待P2所占有的某一資源黄虱,P2等待P3所占有的某一源,......庸诱,而Pn等待P1所占有的的某一資源捻浦,形成一個進(jìn)程循環(huán)等待環(huán)。</br>
當(dāng)程序存在競爭條件時桥爽,需要同步朱灿,避免出現(xiàn)不合預(yù)期的運行結(jié)果。同步實現(xiàn)的兩個工具:鎖和條件狀態(tài)钠四。</br>
以銀行存取款為例盗扒,如果沒有采取同步操作</br>
Code1
public class Bank {
private final double[] accounts;
public Bank(int n, double initialBalance){
accounts = new double[n];
for (int i = 0; i < accounts.length; i++){
accounts[i] = initialBalance;
}
}
public void transfer(int from, int to , double amount){
if (accounts[from] < amount ) {
return;
}
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" 10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance();
}
public double getTotalBalance(){
double sum = 0;
for (double a : accounts){
sum += a;
}
return sum;
}
}
public int size(){
return accounts.length;
}
}
如果存在兩個線程同時執(zhí)行指令
accounts[to] += amount;
由于指令不是原子操作,該指令可能被處理為
- 將accounts[to]加載到寄存器</br>
- 增加amount </br>
- 將結(jié)果寫回account[to]</br>
在線程1執(zhí)行完步驟1形导,2還沒有執(zhí)行步驟3的時候环疼,即只是在寄存器中增加了amount,線程1被剝奪了運行權(quán)限朵耕,處理器將運行權(quán)限交給了線程2,線程2執(zhí)行步驟1淋叶,2阎曹,還沒有執(zhí)行步驟3,即線程2獲取和線程1擁有一樣的初始值,并且只是在寄存器中增加了amount值处嫌,這時候處理器又將時間片給了線程1栅贴,線程1將計算后的值寫入內(nèi)存,而當(dāng)時間片繼續(xù)轉(zhuǎn)給線程2的時候熏迹,仍然是在和線程一樣的初始值上增加amount檐薯,這種情況下,則擦去了線程2所做的更新注暗。
2. ReentrantLock可重入鎖
可重入鎖:是一種特殊的互斥鎖坛缕,可以被同一個線程多次獲取,而不會產(chǎn)生死鎖捆昏。具有兩個特點:</br>
1.是互斥的赚楚,任意時刻,只有一個線程鎖骗卜,假設(shè)A線程已經(jīng)獲取了鎖宠页,在A線程釋放這個鎖之前,B線程無法獲取到寇仓。</br>
2.它可以被同一線程多次持有举户,即假設(shè)A線程已經(jīng)獲取了這個鎖,如果A線程在釋放這個鎖前又一次請求獲取這個鎖遍烦,能夠獲取成功</br>
鎖持有一個計數(shù)器敛摘,來跟蹤lock方法的嵌套調(diào)用。如下代碼乳愉,transfer調(diào)用getTotalBalance方法兄淫,也會封鎖bankLock對象,此時bankLock對象的持有計數(shù)為2蔓姚。當(dāng)getTotalBalance方法退出時捕虽,持有計數(shù)變回1。當(dāng)transfer退出時坡脐,持有計數(shù)變?yōu)?泄私。線程鎖釋放。</br>
Code2
public class Bank {
private final double[] accounts;
private Lock bankLock = new ReentrantLock();
public Bank(int n, double initialBalance){
accounts = new double[n];
for (int i = 0; i < accounts.length; i++){
accounts[i] = initialBalance;
}
}
public void transfer(int from, int to , double amount){
bankLock.lock();
try {
if (accounts[from] < amount ) {
return;
}
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" 10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
} finally {
bankLock.unlock();
}
}
public double getTotalBalance(){
bankLock.lock();
try {
double sum = 0;
for (double a : accounts){
sum += a;
}
return sum;
} finally {
bankLock.unlock();
}
}
public int size(){
return accounts.length;
}
}
package java.util.concurrent.locks.Lock
//獲取這個鎖备闲,如果鎖同時被另一個線程擁有則發(fā)生阻塞
void lock():
//釋放這個鎖
void unlock();
package java.util.concurrent.locks.ReentrantLock
//構(gòu)建一個可以被用來保護(hù)臨界區(qū)的可重入鎖
ReentrantLock();
//構(gòu)建一個帶有公平策略的鎖晌端。一個公平鎖偏愛等待時間最長的線程。但是恬砂,這一公平的保證將大大降低性能咧纠,所以默認(rèn)情況下,鎖沒有被強(qiáng)制為公平的泻骤。
ReentrantLock(boolean fair);
3 條件對象(條件變量)
使用場景:線程進(jìn)入臨界區(qū)漆羔,卻發(fā)現(xiàn)在某一條件滿足之后它才能執(zhí)行梧奢。要使用一個條件對象對象來管理那些已經(jīng)獲得了一個鎖卻不能做有用工作的線程。</br>
銀行賬戶需要轉(zhuǎn)賬演痒,賬戶內(nèi)只有500元卻需要轉(zhuǎn)600元亲轨,即賬戶中沒有足夠的余額,應(yīng)該怎么辦呢鸟顺?現(xiàn)實情況下惦蚊,銀行柜員會告訴你賬戶余額不足,無法辦理讯嫂,直接退出蹦锋。或者端姚,我們可以等待另一個線程賬戶注入100元及以上的金額晕粪。</br>
當(dāng)transfer方法寫成如下
Code3
public void transfer(int from, int to, int amount){
banklock.lock();
try{
while(accounts[from] < amount){
//wait... 這里采取等待,而不是立即返回
}
//transfer funds...
}finally{
banklock.unlock();
}
}
可以看出這個線程剛剛獲得了對banklock的排他性訪問渐裸,因此別的線程沒有進(jìn)行存取操作的機(jī)會巫湘。所以這是需要條件對象的原因。</br>
一個鎖對象可以有一個或多個相關(guān)的條件對象昏鹃,可以用newCondition方法獲得一個條件對象尚氛。習(xí)慣上給每一個條件對象命名為可以反映它所表達(dá)的條件的名字。</br>
Code4
public class Bank {
private final double[] accounts;
private Lock bankLock;
private Condition sufficientFunds;
public Bank(int n, double initialBalance) {
accounts = new double[n];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = initialBalance;
}
//一個bank對象擁有一個ReentrantLock
bankLock = new ReentrantLock();
sufficientFunds = bankLock.newCondition();
}
public void transfer(int from, int to, double amount) {
bankLock.lock();
try {
while (accounts[from] < amount) {
//當(dāng)前線程被阻塞洞渤,并且放棄了鎖阅嘶,并且該線程進(jìn)入該條件的等待集
sufficientFunds.await();
}
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" 10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
//重新激活因為這一條件而等待的所有線程。這些線程中等待集中移出時载迄,它們再次成為可運行的讯柔,調(diào)度器將再次激活它們
//同時,它們將試圖重新進(jìn)入該對象
//一旦鎖成為可用的护昧,它們中的某一個將從await調(diào)用返回魂迄,獲得該鎖并且從被阻塞的地方繼續(xù)執(zhí)行
//采用循環(huán)while表明此時線程應(yīng)該再次檢測該條件。由于無法確保該條件被滿足惋耙,signalAll方法僅僅是通知正在等待的線程
//siganlAll語義可以理解為:此時有可能已經(jīng)滿足條件捣炬,值得再次去檢測條件
sufficientFunds.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bankLock.unlock();
}
}
public double getTotalBalance() {
bankLock.lock();
try {
double sum = 0;
for (double a : accounts) {
sum += a;
}
return sum;
} finally {
bankLock.unlock();
}
}
public int size() {
return accounts.length;
}
}
Condition.signal():是隨機(jī)解除等待集中某個線程的阻塞狀態(tài)。這比解除所有線程的等待狀態(tài)效率要高绽榛,但是存在危險湿酸。如果隨機(jī)選擇的線程發(fā)現(xiàn)自己仍然不能運行,那么它再次被阻塞灭美。如果沒有其他線程再次調(diào)用signal推溃,那么系統(tǒng)就死鎖了。
package java.util.concurrent.locks.Lock
//返回一個與該鎖相關(guān)的條件對象
Condition new Condition();
e.g. Condition sufficientFunds = bankLock.newCondition();
package java.util.concurrent.locks.Condition
//將線程放到條件的等待集合中
void await();
//解除該條件的等待集中的所有線程的阻塞狀態(tài)
void signalAll();
//從該條件的等待集中隨機(jī)地選擇一個線程冲粤,解除其阻塞狀態(tài)
void Signal();
4 鎖與條件對象的關(guān)鍵之處
*. 鎖可以用來保護(hù)代碼片段美莫,任何時刻只能有一個線程執(zhí)行被保護(hù)的代碼页眯。</br>
*. 鎖可以管理試圖進(jìn)入被保護(hù)代碼段的線程
*. 鎖可以擁有一個或多個相關(guān)的條件對象</br>
*. 每個條件對象管理那些已經(jīng)進(jìn)入被保護(hù)的代碼段但還不能運行的線程
5 synchronized 關(guān)鍵字
每一對象有一個內(nèi)部鎖梯捕,并且該鎖有一個內(nèi)部條件厢呵。由鎖來管理那些試圖進(jìn)入synchronized方法的線程,由條件來管理那些調(diào)用wait的線程傀顾。</br>
Code5
public class Bank {
private double[] accounts;
public Bank(int n, double initialBalance) {
accounts = new double[n];
for (int i = 0; i < accounts.length; i++) {
accounts[i] = initialBalance;
}
}
public synchronized void transfer(int from, int to, double amount) {
try {
while (accounts[from] < amount) {
//將線程添加到一個線程等待集中襟铭,該方法只能在一個同步方法中調(diào)用方法中調(diào)用
wait();
}
System.out.println(Thread.currentThread());
accounts[from] -= amount;
System.out.printf(" 10.2f from %d to %d", amount, from, to);
accounts[to] += amount;
System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
//notifyAll/notify方法解除等待線程的阻塞狀態(tài),該方法只能在同步方法或者同步塊中調(diào)用
notifyAll();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public synchronized double getTotalBalance() {
double sum = 0;
for (double a : accounts) {
sum += a;
}
return sum;
}
public int size() {
return accounts.length;
}
}
將靜態(tài)方法聲明為synchronized也是合法的短曾,如果調(diào)用這種方法寒砖,該方法獲得相關(guān)的類對象的內(nèi)部鎖。如果Bank類有一個靜態(tài)同步的方法嫉拐,那么當(dāng)該方法被調(diào)用時哩都,Bank.class對象的鎖被鎖住。因此婉徘,沒有其他線程可以調(diào)用同一個類的這個或者任何其他的同步靜態(tài)方法漠嵌。
pulic class Bank
{
private double[] accounts;
private Object lock = new Object();
}
6 鎖和條件對象的局限
*. 不能中斷一個正在試圖獲得鎖的線程</br>
*. 試圖獲得鎖時不能設(shè)定超時
*. 每個鎖僅有單一的條件,可能是不夠的</br>
*. 最好既不是用Locl/Condition也不使用synchronized關(guān)鍵字
*. 如果synchronized關(guān)鍵字適合程序盖呼,那么請盡量使用它儒鹿,這樣可以減少編寫的代碼數(shù)量,減少出錯的幾率几晤。</br>
*. 如果特別需要Lock/Condition結(jié)構(gòu)提供的獨有特性時约炎,才使用Lock/Condition</br>
7 Volatile域
有時,僅僅為了讀寫一個或兩個實例域就使用同步蟹瘾,開銷過大圾浅。可以采用volatile關(guān)鍵字聲明域憾朴,該修飾詞告訴編譯器和虛擬機(jī)該域是可能被另一個線程并發(fā)更新的狸捕。它為實例域的同步訪問提供了一個種免鎖機(jī)制。
8 final變量
將域聲明為final伊脓,可以安全的訪問一個共享域府寒。
final Map<String, Double> accounts = new HashMap<>();
其他線程會在構(gòu)造函數(shù)完成構(gòu)造之后才看到這個accounts變量。如果不適用final报腔,就不能保證其他線程看到的是account更新后的值株搔,他們可能都只是看到null,而不是新構(gòu)造的HashMap纯蛾。當(dāng)然纤房,對這個映射表的操作不是線程安全的,如果多個線程在讀寫這個映射表翻诉,仍然需要進(jìn)行的炮姨。
學(xué)習(xí)資料:《Java核心技術(shù)卷一》