JAVA多線程死鎖解決方案
什么是java多線程中的死鎖?
通過一個簡單的小故事你可以更容易地理解它:
桌上放著一副刀叉,兩份牛排,小強和小明只有同時拿到刀和叉才能吃牛排。一開始,兩人都去競爭餐具,小強拿到了刀,小明拿到了叉,然后小強拿著刀等待小明占有的叉,小明拿著叉等待小強占有的刀,如此他們陷入了彼此等待死鎖,誰也得不到還差的餐具,吃不了牛排。
比較概括性的解釋:多個線程同時被阻塞,其中一個或者全部都在等待某個資源被釋放。由于線程被無限地阻塞,因此程序不可能正常終止。
上鎖同步模擬死鎖:
import java.text.SimpleDateFormat;
import java.util.Date;
public class DeadlockTest {
public static String th1 = "Dao";
public static String th2 = "Cha";
public static SimpleDateFormat sdf = new SimpleDateFormat("yyy/MM/dd hh:mm:ss");
public static void main(String[] args) {
XiaoQiang1 XiaoQiang1 = new XiaoQiang1();
new Thread(XiaoQiang1).start();
XiaoMing1 XiaoMing1 = new XiaoMing1();
new Thread(XiaoMing1).start();
}
}
class XiaoQiang1 implements Runnable{
public void run() {
try {
System.out.println(DeadlockTest.sdf.format(new Date()).toString() + " XiaoQiang1 starts asking for Dao and Cha......");
while(true){
synchronized (DeadlockTest.th1) {
System.out.println(DeadlockTest.sdf.format(new Date()).toString() + " XiaoQiang1 gets Dao!");
Thread.sleep(3000); // 此處休眠不釋放鎖(模擬對資源Dao的占用);
synchronized (DeadlockTest.th2) {
System.out.println(DeadlockTest.sdf.format(new Date()).toString() + " XiaoQiang1 gets Cha!");
Thread.sleep(10 * 1000); // 線程休眠不釋放鎖,讓線程占用對象鎖的時間久一點;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class XiaoMing1 implements Runnable{
public void run() {
try {
System.out.println(DeadlockTest.sdf.format(new Date()).toString() + " XiaoMing1 starts asking for Dao and Cha......");
while(true){
synchronized (DeadlockTest.th2) {
System.out.println(DeadlockTest.sdf.format(new Date()).toString() + " XiaoMing1 gets Cha!");
Thread.sleep(3000); // / 此處休眠不釋放鎖(模擬對資源Cha的占用);
synchronized (DeadlockTest.th1) {
System.out.println(DeadlockTest.sdf.format(new Date()).toString() + " XiaoMing1 gets Dao!");
Thread.sleep(10 * 1000); // 線程休眠不釋放鎖,讓線程占用對象鎖的時間久一點;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}</code></pre>
編譯運行之后的結果:
2016/04/29 12:43:54 XiaoQiang1 starts asking for Dao and Cha......
2016/04/29 12:43:54 XiaoMing1 starts asking for Dao and Cha......
2016/04/29 12:43:54 XiaoQiang1 gets Dao!
2016/04/29 12:43:54 XiaoMing1 gets Cha!
(陷入死鎖......)
此處把小強和小明看做兩個線程,刀叉是資源。總結死鎖的四個特點(或者說產生死鎖的四個必要條件):
1.互斥使用,即當資源被一個線程使用(占有)時,別的線程不能使用。
2.不可搶占,資源請求者不能強制從資源占有者手中奪取資源,資源只能由資源占有者主動釋放。
3.請求和保持,即當資源請求者在請求其他的資源的同時保持對原有資源的占有。
4.循環等待,即存在一個等待隊列:P1占有P2的資源,P2占有P3的資源,P3占有P1的資源。這樣就形成了一個等待環路。
如何解決死鎖問題?
只要打破產生死鎖的四個必要條件之一,死鎖就會消失。
這個實際問題的解決方法:小強和小明在獲取完整餐具失敗或者用完之后都先釋放餐具的使用權一段時間(通過信號量來控制),這樣就會存在資源使用權空閑時間段可以供另外一個人去獲取餐具,不至于陷入無限循環等待的窘境。
Semaphore類介紹
這就要用到java中的Semaphore類(一個計數信號量。從概念上講,信號量維護了一個許可集)。Semaphore不使用實際的許可對象,只對可用許可的號碼進行計數,并采取相應的行動。所謂的許可數目就是用于限制可以訪問某些資源(物理或邏輯的)的線程數目
信號量可以控制資源能被多少線程訪問,這里我們指定只能被一個線程訪問,就做到了類似鎖住。而信號量可以指定去獲取的超時時間,我們可以根據這個超時時間,去做一個額外處理。
-
構造函數 public Semaphore(int permits)
permits 初始的可用許可數目。此值可能為負數,在這種情況下,必須在授予任何獲取前進行釋放。這里設置permits為1,因為這里只有一副刀叉:Semaphore s1 = new Semaphore(1)。
-
public boolean tryAcquire(long timeout,TimeUnit unit)
如果在給定的等待時間內,此信號量有可用的許可并且當前線程未被中斷,則從此信號量獲取一個許可。這里設置等待時間為1秒后:tryAcquire(1, TimeUnit.SECONDS)。
-
public void release()
釋放一個許可,將其返回給信號量。釋放一個許可,將可用的許可數增加 1。如果任意線程試圖獲取許可,則選中一個線程并將剛剛釋放的許可給予它。
用信號量控制之后的死鎖避免模擬:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class UnlockTest {
public static String th1 = "Dao";
public static String th2 = "Cha";
public static final Semaphore s1 = new Semaphore(1);
public static final Semaphore s2 = new Semaphore(1);
public static SimpleDateFormat sdf = new SimpleDateFormat("yyy/MM/dd hh:mm:ss");
public static void main(String[] args) {
XiaoQiang xiaoQiang = new XiaoQiang();
new Thread(xiaoQiang).start();
XiaoMing xiaoMing = new XiaoMing();
new Thread(xiaoMing).start();
}
}
class XiaoQiang implements Runnable{
public void run() {
try {
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoQiang starts asking for Dao and Cha......");
while(true){
if(UnlockTest.s1.tryAcquire(1, TimeUnit.SECONDS)) {
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoQiang gets Dao!");
Thread.sleep(1000); // 休眠1秒(模擬等待下一個餐具或者等待1秒再取);
if(UnlockTest.s2.tryAcquire(1, TimeUnit.SECONDS)) {
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoQiang gets Cha!");
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoQiang happy eating!(30 seconds)");
Thread.sleep(30 1000); // 線程休眠30秒(模擬進餐);
} else {
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoMing fails to get Dao!");
}
} else {
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoMing fails to get Cha!");
}
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoQiang releases Dao or Cha which has been obtained!");
UnlockTest.s1.release(); // 釋放一個許可,將可用的許可數加1(模擬交出Dao的占用權);;
UnlockTest.s2.release(); // 釋放一個許可,將可用的許可數加1(模擬交出Cha的占用權);;
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoQiang occupies nothing......waiting 10sec!");
Thread.sleep(5 1000);//休眠5秒(模擬XiaoQiang交出餐具占用權后等待5秒再去獲取餐具)。
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
class XiaoMing implements Runnable{
public void run() {
try {
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoMing starts asking for Dao and Cha......");
while(true){
if(UnlockTest.s2.tryAcquire(1, TimeUnit.SECONDS)) {
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoMing gets Cha!");
Thread.sleep(3000); // 休眠3秒(模擬等待下一個餐具或者等待3秒再取);
if(UnlockTest.s1.tryAcquire(1, TimeUnit.SECONDS)) {
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoMing gets Dao!");
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoMing happy eating!(30 seconds)");
Thread.sleep(30 * 1000); // 線程休眠30秒,模擬進餐;
} else {
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoMing fails to get Dao!");
}
} else {
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoMing fails to get Cha!");
}
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoMing releases Dao or Cha which has been obtained!");
UnlockTest.s2.release(); // 釋放一個許可,將可用的許可數加1(模擬交出Cha的占用權);;
UnlockTest.s1.release(); // 釋放一個許可,將可用的許可數加1(模擬交出Dao的占用權);;
System.out.println(UnlockTest.sdf.format(new Date()).toString() + " XiaoMing occupies nothing......waiting 20sec!");
Thread.sleep(20 * 1000);//休眠20秒(模擬XiaoMing交出餐具占用權后等待20秒再去獲取餐具)。
}
} catch (Exception e) {
e.printStackTrace();
}
}
}</code></pre>
編譯運行之后的結果:
2016/04/28 11:15:10 XiaoQiang starts asking for Dao and Cha......
2016/04/28 11:15:10 XiaoMing starts asking for Dao and Cha......
2016/04/28 11:15:10 XiaoQiang gets Dao!
2016/04/28 11:15:10 XiaoMing gets Cha!
2016/04/28 11:15:12 XiaoMing fails to get Dao!
2016/04/28 11:15:12 XiaoQiang releases Dao or Cha which has been obtained!
2016/04/28 11:15:12 XiaoQiang occupies nothing......waiting 10sec!
2016/04/28 11:15:13 XiaoMing gets Dao!
2016/04/28 11:15:13 XiaoMing happy eating!(30 seconds)
2016/04/28 11:15:18 XiaoMing fails to get Cha!
2016/04/28 11:15:18 XiaoQiang releases Dao or Cha which has been obtained!
2016/04/28 11:15:18 XiaoQiang occupies nothing......waiting 10sec!
2016/04/28 11:15:23 XiaoQiang gets Dao!
2016/04/28 11:15:24 XiaoQiang gets Cha!
2016/04/28 11:15:24 XiaoQiang happy eating!(30 seconds)
2016/04/28 11:15:43 XiaoMing releases Dao or Cha which has been obtained!
2016/04/28 11:15:43 XiaoMing occupies nothing......waiting 20sec!
2016/04/28 11:15:54 XiaoQiang releases Dao or Cha which has been obtained!
2016/04/28 11:15:54 XiaoQiang occupies nothing......waiting 10sec!
2016/04/28 11:15:59 XiaoQiang gets Dao!
2016/04/28 11:16:00 XiaoQiang gets Cha!
2016/04/28 11:16:00 XiaoQiang happy eating!(30 seconds)
2016/04/28 11:16:03 XiaoMing gets Cha!
2016/04/28 11:16:06 XiaoMing gets Dao!
2016/04/28 11:16:06 XiaoMing happy eating!(30 seconds)
......
輸出信息多些更容易看出規律,由此可見:使用信號量控制之后已經成功地避免了死鎖問題。
文/斯云(簡書)