在Java多线程开发中死锁问题并不少见,当线程间相互等待资源,而又不释放自身的资源时就会导致无穷无尽的等待。
举一个死锁的例子
public class Account {
private int balance;
// 转账
void transfer(Account target, int amt) {
// 锁定转出账户
synchronized(this) {
try {
Thread.sleep(10);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"lock:"+this+"=>get:"+target);
// 锁定转入账户
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();
}
}
以上是一个转账的例子,两个账户相互转账,转账时必须要保护自己账户的资源balance
和目标的资源balance
不会被其他线程修改,就做了加锁。在单线程的情况下时不会有问题的,但是一旦有两个线程同时操作两个账户转账就会出现死锁的问题。两个线程都在等对方先释放资源,会永久地等下去。
如何解决死锁地问题
并发程序出现死锁问题并没什么好地解决办法,一般情况下只能重启应用。因此解决死锁地问题最好的办法就是规避死锁。如何规避呢?有一个叫Coffman的牛人总结出来,只有以下四个条件都发生的时候才会出现死锁。
- 互斥,共享资源 X 和 Y 只能被一个线程占用;
- 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
- 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
- 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
那么也就是说这四个条件只要破坏其中一个就不会发生死锁。首先第一个条件互斥是无法被破坏的,因为我们在多线程环境里加锁就是为了互斥。其他三个条件都是可以被破坏的。
破坏占有且等待条件
要破坏这个条件通常的做法是让一个线程一次性申请所有的资源,在上面的例子上我们可以再建一个单例类Allocator
,Allocator
来一次性申请两个账户的资源,转账完成后就一起释放资源。
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 应该为单例
private Allocator actr = Allocator.getInstance();
private int balance;
// 转账
void transfer(Account target, int amt){
// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target))
try{
// 锁定转出账户
synchronized(this){
// 锁定转入账户
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 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。但是可以使用SDK下的java.util.concurrent 这个包下面提供的 Lock类来解决这个问题。
public class Account {
private final Lock lock = new ReentrantLock();
private int balance;
// 转账
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();
}
}
破坏循环等待条件
破坏这个条件,需要对资源进行排序,然后按序申请资源。以上账号的例子我们假设每一个账户都有一个id
字段,我们用id
字段作为排序条件。申请的时候,我们可以按照从小到大的顺序来申请,这样无论有多少个线程进来都会先去拿账户id
小的账户资源,这样就不会出现争抢的问题。
public class Account {
private int id;
private int balance;
public Account(int id) {
this.id = id;
}
// 转账
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();
}
}
有了以上这三个方案后你可能会有一个疑问,那个方案好呢。从性能上看破坏占有且等待条件这个方案性能最低,它需要同时获得两把锁的使用权才能执行下去,不然就会一直while
下去。破坏不可抢占条件这个性能次之,它不用同时获得两个资源的锁,一个资源一个资源的拿,一旦拿不到就会主动释放,但它也会一直while
的尝试下去。破坏循环等待条件这个性能最佳,它会提前把资源顺序排序好,避免了发生挣抢的问题。但是破坏循环等待条件的做法对代码产生侵入性,增加了额外的逻辑。所以最优的要看具体的业务性能要求,通常的话破坏不可抢占条件这个方案是一个常用的选择。
线上如何排查死锁的问题
线上死锁问题总是不经意间产生的,跑在tomcat上
的应用一旦出现死锁问题就会照成大部分线程阻塞,进而tomcat
就会出现假死状态不能正常的服务。所以排查死锁问题是java程序员必备的一个技能。
我们可以使用jconsole
这类的工具对java进程进行监控来找到死锁的线程,也可以使用jstack
命令来排查。
首先可以用jps来找到当前java的进程号
>jps
14804 Account //查询出来account这个进程的进程号
17900 Jps
使用jstack命令查询线程运行状态
>jstack 14804 //查看进程下所有线程状态
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
。