很久以前学习Java的多线程机制时,一直没有搞懂Java的锁机制,今天花费了一整天的时间研究了一下。在学习的过程中发现一项非常有趣的问题,那就是死锁现象,下面是对死锁现象的完整描述:
当A线程等待由B线程持有的锁时,而B线程正在等待A线程持有的锁,随即发生死锁现象,
JVM不会检测也不试图避免这种情况,完全需要靠程序员自己注意。
要避免死锁现象,我们首先需要搞清楚什么是死锁现象,然后才能找到有效的避免方法,接下来我们将探索什么事死锁现象。
一、死锁现象
阅读上面对死锁现象的定义,我们可以断定,死锁现象至少需要两个线程,这里我们假设为Thread A和Thread B。还有,我们知道,每个对象的同步锁同一时刻只能被一个线程持有,所以,根据上面定义,我们还可以断定,这里必须包含两个带锁的对象,我们将这两个带锁的对象命名为Lock X和Lock Y。下面我们来创建代码,观察死锁现象的具体表现形式:
1) 首先创建两个带锁的对象类
// Lock X
static class LockX {
synchronized public void get(LockY lock) {
System.out.println("Before sleep of lock X");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Waiting for lock Y");
lock.samething();
}
synchronized public void something() {
System.out.println("Lock X somthing method");
}
}
static class LockY {
synchronized public void get(LockX lock) {
System.out.println("Before sleep of lock Y");
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Waiting for lock X");
lock.something();
}
synchronized public void samething() {
System.out.println("Lock Y somthing method");
}
}
上面代码看起来可能很奇怪,难以理解。现在先不要着急去理解每个方法的用意。最后我们会详细讲解每个方法的具体调用过程。
2) 创建两个线程类
// Thread A
static class ThreadA extends Thread {
private LockX lockX;
private LockY lockY;
public ThreadA(LockX lockx, LockY locky) {
this.lockX = lockx;
this.lockY = locky;
}
@Override
public void run() {
System.out.println("Start run thread A");
lockX.get(lockY);
}
}
// Thread B
static class ThreadB extends Thread {
private LockY lockY;
private LockX lockX;
public ThreadB(LockX lockx, LockY locky) {
this.lockX = lockx;
this.lockY = locky;
}
@Override
public void run() {
System.out.println("Start run thread B");
lockY.get(lockX);
}
}
3)创建主方法
public static void main(String[] args) {
LockX x = new LockX();
LockY y = new LockY();
new ThreadA(x, y).start();
new ThreadB(x, y).start();
}
首先运行一次程序,观察是否真的发生死锁现象。下面截图展示了控制台的运行结果:
注意上面红色圆圈,表明程序并未结束,而且控制台也没有输出"Lock X somthing method"和"Lock X someting method"两条语句。接下来我们来分析上述代码的执行过程:
首先,我们在main方法中启动了ThreadA和ThreadB两个线程,并且分别为这两个线程传递了两个共享的资源对象x和y,x和y属于不同类的对象,但他们都包含get和something方法,而且这两个方法都是同步方法。
从控制台的输出我们可以确定,首先启动的是ThreadA线程。当ThreadA线程启动后,首先输出"Start run thread A"一行文字,这也是控制台输出的第一行文字。接着ThreadA线程调用x对象的get方法,由于get属于同步方法,所以此时由ThreadA持有该对象的同步锁,直到ThreadA执行完get方法之前,其他线程将不能访问该对象的任何同步方法,这就是同步锁的排斥性。当ThreadA线程进入get方法之后,首先输出一行"Before sleep of lock X"文字,然后进入10毫秒的睡眠状态。
我们知道此时还有一个ThreadB线程已经启动了,当ThreadA线程在(x的)get方法中睡眠时,ThreadB等不及了,调度器此时将CPU让给ThreadB开始执行代码(这里一定要清楚,同步锁并不能保证CPU一次性执行完成同步代码块或同步方法内的所有代码)。
ThreadB开始执行代码,原理与ThreadA线程的开始过程完全相同,先是输出"Start run thread B",接着通过调用y对象的get方法获得y的同步锁,然后输出"Before sleep of lock Y"文字,紧接着也进入10毫秒的睡眠状态。
当ThreadB进入睡眠状态后,调度器又会将CPU的执行权还给ThreadA线程,此时ThreadA线程的睡眠时间已经结束,开始继续执行下面代码。所以ThreadA线程此时应该输出"Waiting for lock Y"一行文字,然后继续执行lock.samething()的调用,lock引用的是y对象,由于此时y对象的同步锁是由线程ThreadB持有的,所以ThreadA不能进入y的something()方法,只能等待ThreadB释放同步锁。
当ThreadA在等待的时候,CPU又会进入ThreadB线程,此时ThreadB线程输出"Waiting for lock X"一行文字,然后也调用lock.something()方法,但是此时的lock属于x对象,而x对象的同步锁此时还被ThreadA持有着,因为ThreadA并没有退出x的get方法,所有ThreadB只能等待ThreadA释放同步锁。
到这个时候就已经产生死锁现象了,因为此后,无论CPU切换到哪一个线程都无法继续执行代码。
二 如何避免死锁现象
避免死锁其实就一句话:当多个线程都要访问共享的资源A,B,C时,保证每一个线程都按照相同的顺序去访问他们,比如都先访问A,接着B,最后C。
根据上面这句话我们就可以修改上面锁死的例子。首先,我们不应该为LockX和LockY的get方法设置参数对象,其次在ThreadA和ThreadB线程中,应该该着相同的顺序访问资源对象x和y。