在Java多線程開發(fā)中死鎖問題并不少見,當(dāng)線程間相互等待資源,而又不釋放自身的資源時就會導(dǎo)致無窮無盡的等待。
舉一個死鎖的例子
public class Account {
private int balance;
// 轉(zhuǎn)賬
void transfer(Account target, int amt) {
// 鎖定轉(zhuǎn)出賬戶
synchronized(this) {
try {
Thread.sleep(10);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"lock:"+this+"=>get:"+target);
// 鎖定轉(zhuǎn)入賬戶
synchronized(target) {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
System.out.println(Thread.currentThread().getName()+"lock:"+target+"=>get:"+this);
}
}
}
public static void main(String[] args){
Account account1 = new Account();
Account account2 = new Account();
new Thread(new Runnable() {
public void run() {
account1.transfer(account2,10);
}
}).start();
new Thread(new Runnable() {
public void run() {
account2.transfer(account1,10);
}
}).start();
}
}
以上是一個轉(zhuǎn)賬的例子,兩個賬戶相互轉(zhuǎn)賬,轉(zhuǎn)賬時必須要保護自己賬戶的資源balance和目標(biāo)的資源balance不會被其他線程修改,就做了加鎖。在單線程的情況下時不會有問題的,但是一旦有兩個線程同時操作兩個賬戶轉(zhuǎn)賬就會出現(xiàn)死鎖的問題。兩個線程都在等對方先釋放資源,會永久地等下去。
如何解決死鎖地問題
并發(fā)程序出現(xiàn)死鎖問題并沒什么好地解決辦法,一般情況下只能重啟應(yīng)用。因此解決死鎖地問題最好的辦法就是規(guī)避死鎖。如何規(guī)避呢?有一個叫Coffman的牛人總結(jié)出來,只有以下四個條件都發(fā)生的時候才會出現(xiàn)死鎖。
- 互斥,共享資源 X 和 Y 只能被一個線程占用;
- 占有且等待,線程 T1 已經(jīng)取得共享資源 X,在等待共享資源 Y 的時候,不釋放共享資源 X;
- 不可搶占,其他線程不能強行搶占線程 T1 占有的資源;
- 循環(huán)等待,線程 T1 等待線程 T2 占有的資源,線程 T2 等待線程 T1 占有的資源,就是循環(huán)等待。
那么也就是說這四個條件只要破壞其中一個就不會發(fā)生死鎖。首先第一個條件互斥是無法被破壞的,因為我們在多線程環(huán)境里加鎖就是為了互斥。其他三個條件都是可以被破壞的。
破壞占有且等待條件
要破壞這個條件通常的做法是讓一個線程一次性申請所有的資源,在上面的例子上我們可以再建一個單例類Allocator,Allocator來一次性申請兩個賬戶的資源,轉(zhuǎn)賬完成后就一起釋放資源。
public class Allocator {
private Allocator(){};
private static Allocator instance = new Allocator();
private List<Object> als = new ArrayList<>();
// 一次性申請所有資源
synchronized boolean apply(Object from, Object to){
if(als.contains(from) || als.contains(to)){
return false;
} else {
als.add(from);
als.add(to);
}
return true;
}
// 歸還資源
synchronized void free(Object from, Object to){
als.remove(from);
als.remove(to);
}
public static Allocator getInstance(){
return instance;
}
}
class Account {
// actr 應(yīng)該為單例
private Allocator actr = Allocator.getInstance();
private int balance;
// 轉(zhuǎn)賬
void transfer(Account target, int amt){
// 一次性申請轉(zhuǎn)出賬戶和轉(zhuǎn)入賬戶,直到成功
while(!actr.apply(this, target))
try{
// 鎖定轉(zhuǎn)出賬戶
synchronized(this){
// 鎖定轉(zhuǎn)入賬戶
synchronized(target){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
} finally {
actr.free(this, target);
}
}
public static void main(String[] args) {
Account account1 = new Account();
Account account2 = new Account();
new Thread(new Runnable() {
public void run() {
account1.transfer(account2,10);
}
}).start();
new Thread(new Runnable() {
public void run() {
account2.transfer(account1,10);
}
}).start();
}
}
破壞不可搶占條件
破壞不可強制資源其實就是線程能夠主動釋放它占有的資源,這一點 synchronized是做不到的。原因是 synchronized 申請資源的時候,如果申請不到,線程直接進入阻塞狀態(tài)了,而線程進入阻塞狀態(tài),啥都干不了,也釋放不了線程已經(jīng)占有的資源。但是可以使用SDK下的java.util.concurrent 這個包下面提供的 Lock類來解決這個問題。
public class Account {
private final Lock lock = new ReentrantLock();
private int balance;
// 轉(zhuǎn)賬
void transfer(Account target, int amt) throws InterruptedException {
while(true){
if(this.lock.tryLock()){
try {
if(target.lock.tryLock()){
try {
if (this.balance > amt) {
this.balance -= amt;
target.balance += amt;
}
}finally {
target.lock.unlock();
}
}
}finally {
this.lock.unlock();
}
}
}
}
public static void main(String[] args){
Account account1 = new Account();
Account account2 = new Account();
new Thread(new Runnable() {
public void run() {
try {
account1.transfer(account2,10);
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
public void run() {
try {
account2.transfer(account1,10);
}catch (Exception e){
e.printStackTrace();
}
}
}).start();
}
}
破壞循環(huán)等待條件
破壞這個條件,需要對資源進行排序,然后按序申請資源。以上賬號的例子我們假設(shè)每一個賬戶都有一個id字段,我們用id字段作為排序條件。申請的時候,我們可以按照從小到大的順序來申請,這樣無論有多少個線程進來都會先去拿賬戶id小的賬戶資源,這樣就不會出現(xiàn)爭搶的問題。
public class Account {
private int id;
private int balance;
public Account(int id) {
this.id = id;
}
// 轉(zhuǎn)賬
void transfer(Account target, int amt){
Account left = this;
Account right = target;
if (this.id > target.id) {
left = target;
right = this;
}
// 鎖定序號小的賬戶
synchronized(left){
// 鎖定序號大的賬戶
synchronized(right){
if (this.balance > amt){
this.balance -= amt;
target.balance += amt;
}
}
}
}
public static void main(String[] args){
Account account1 = new Account(1);
Account account2 = new Account(2);
new Thread(new Runnable() {
public void run() {
account1.transfer(account2,10);
}
}).start();
new Thread(new Runnable() {
public void run() {
account2.transfer(account1,10);
}
}).start();
}
}
有了以上這三個方案后你可能會有一個疑問,那個方案好呢。從性能上看破壞占有且等待條件這個方案性能最低,它需要同時獲得兩把鎖的使用權(quán)才能執(zhí)行下去,不然就會一直while下去。破壞不可搶占條件這個性能次之,它不用同時獲得兩個資源的鎖,一個資源一個資源的拿,一旦拿不到就會主動釋放,但它也會一直while的嘗試下去。破壞循環(huán)等待條件這個性能最佳,它會提前把資源順序排序好,避免了發(fā)生掙搶的問題。但是破壞循環(huán)等待條件的做法對代碼產(chǎn)生侵入性,增加了額外的邏輯。所以最優(yōu)的要看具體的業(yè)務(wù)性能要求,通常的話破壞不可搶占條件這個方案是一個常用的選擇。
線上如何排查死鎖的問題
線上死鎖問題總是不經(jīng)意間產(chǎn)生的,跑在tomcat上的應(yīng)用一旦出現(xiàn)死鎖問題就會照成大部分線程阻塞,進而tomcat就會出現(xiàn)假死狀態(tài)不能正常的服務(wù)。所以排查死鎖問題是java程序員必備的一個技能。
我們可以使用jconsole這類的工具對java進程進行監(jiān)控來找到死鎖的線程,也可以使用jstack命令來排查。
首先可以用jps來找到當(dāng)前java的進程號
>jps
14804 Account //查詢出來account這個進程的進程號
17900 Jps
使用jstack命令查詢線程運行狀態(tài)
>jstack 14804 //查看進程下所有線程狀態(tài)
2020-04-20 19:35:07
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b15 mixed mode):
"DestroyJavaVM" #16 prio=5 os_prio=0 tid=0x0000000002fe9000 nid=0x1b2c waiting on condition [0x0000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread-1" #15 prio=5 os_prio=0 tid=0x0000000020ebb000 nid=0x1e40 waiting for monitor entry [0x0000000021eff000]
java.lang.Thread.State: BLOCKED (on object monitor)
at locktest.Account1.transfer(Account1.java:18)
- waiting to lock <0x000000076b613c20> (a locktest.Account1)
- locked <0x000000076b613c30> (a locktest.Account1)
at locktest.Account1$2.run(Account1.java:39)
at java.lang.Thread.run(Thread.java:745)
"Thread-0" #14 prio=5 os_prio=0 tid=0x00000000206ab000 nid=0x1578 waiting for monitor entry [0x0000000021dff000]
java.lang.Thread.State: BLOCKED (on object monitor)
at locktest.Account1.transfer(Account1.java:18)
- waiting to lock <0x000000076b613c30> (a locktest.Account1)
- locked <0x000000076b613c20> (a locktest.Account1)
at locktest.Account1$1.run(Account1.java:34)
at java.lang.Thread.run(Thread.java:745)
"Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001e673800 nid=0x24c8 in Object.wait() [0x000000001f9cf000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076af08ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:143)
- locked <0x000000076af08ee0> (a java.lang.ref.ReferenceQueue$Lock)
at java.lang.ref.ReferenceQueue.remove(ReferenceQueue.java:164)
at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209)
"Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x000000001cf90000 nid=0x2acc in Object.wait() [0x000000001f8cf000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076af06b50> (a java.lang.ref.Reference$Lock)
at java.lang.Object.wait(Object.java:502)
at java.lang.ref.Reference.tryHandlePending(Reference.java:191)
- locked <0x000000076af06b50> (a java.lang.ref.Reference$Lock)
at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153)
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x000000001cf927d8 (object 0x000000076b613c20, a locktest.Account1),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x000000001cf93e88 (object 0x000000076b613c30, a locktest.Account1),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at locktest.Account1.transfer(Account1.java:18)
- waiting to lock <0x000000076b613c20> (a locktest.Account1)
- locked <0x000000076b613c30> (a locktest.Account1)
at locktest.Account1$2.run(Account1.java:39)
at java.lang.Thread.run(Thread.java:745)
"Thread-0":
at locktest.Account1.transfer(Account1.java:18)
- waiting to lock <0x000000076b613c30> (a locktest.Account1)
- locked <0x000000076b613c20> (a locktest.Account1)
at locktest.Account1$1.run(Account1.java:34)
at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.
可以看到jstack直接就幫我們找到了deadlock。