涉及到多线程并发编程,总会涉及到“死锁”的问题,这里说件比较尴尬的事,笔者有一次去面试,被面试官给绕晕了,结果人家一句:“你这不就死锁了吗?”把我吓出一身汗来···
今天,我们先来手写一段死锁的代码:
public class MyObject {
private final Object A=new Object();
private final Object B=new Object();
public void a(){
synchronized (A){
System.out.println(Thread.currentThread().getName()+" 持有A对象锁!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B){
System.out.println("a已完成!");
}
}
}
public void b() {
synchronized (B){
System.out.println(Thread.currentThread().getName()+" 持有B对象锁!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (A){
System.out.println("b已完成!");
}
}
}
}
如上,我们定义了一个类,其中有两个成员对象,同时定义了两个方法,每个方法里都有锁的嵌套,接着,我们来进行调用:
public static void main(String[] args) {
final MyObject myObject=new MyObject();
Thread threadA=new Thread(){
@Override
public void run() {
super.run();
myObject.a();
}
};
Thread threadB=new Thread(){
@Override
public void run() {
super.run();
myObject.b();
}
};
threadA.start();
threadB.start();
}
好啦,就这么简单,这样就是一个最简单的死锁形式了,我们来运行一下:
Thread-0持有A对象锁!
Thread-1持有B对象锁!
结果如上,我们永远都等不到“a已完成!”和“b已完成!”
其实,整个过程并不复杂,threadA持有myObject中A对象锁,threadB持有myObject中B对象锁,而a方法要完成需要拿到myObject中B对象锁,b方法要完成需要拿到myObject中A对象锁,很明显,如果a方法不执行完毕,其不会释放myObject中A对象锁,而如果b方法不执行完毕,其不会释放myObject中B对象锁,就这样,大家都互相等下去耗下去——死锁。
如果我们将调用的代码改为如下:
public static void main(String[] args) {
final MyObject myObjectA=new MyObject();
final MyObject myObjectB=new MyObject();
Thread threadA=new Thread(){
@Override
public void run() {
super.run();
myObjectA.a();
}
};
Thread threadB=new Thread(){
@Override
public void run() {
super.run();
myObjectB.b();
}
};
threadA.start();
threadB.start();
}
运行结果:
Thread-1持有B对象锁!
Thread-0持有A对象锁!
a已完成!
b已完成!
这个也没什么可说的,因为是不同的对象,不存在死锁的问题。
那么,如果是同一对象,我们有什么办法来解决上面死锁的问题呢?
办法当然是有的,我们可以用Lock类中的tryLock()方法去获取锁,该方法会返回获取锁的结果,也支持设置获取锁的超时时限,如果获取不到锁,我们可以进行相应的处理,而不至于造成死锁,下面我们对例子进行更改:
public class MyObject2 {
class ObjectA {
public Lock lock = new ReentrantLock();
}
class ObjectB {
public Lock lock = new ReentrantLock();
}
private final ObjectA A=new ObjectA();
private final ObjectB B=new ObjectB();
public void a(){
if(A.lock.tryLock()){
System.out.println(Thread.currentThread().getName()+"持有A对象锁!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(B.lock.tryLock()){
System.out.println(Thread.currentThread().getName()+"持有B对象锁!");
B.lock.unlock();//释放B对象锁
System.out.println("a已完成!");
}else {
System.out.println(Thread.currentThread().getName()+"获取B对象锁失败!");
}
A.lock.unlock();//释放A对象锁
}else {
System.out.println(Thread.currentThread().getName()+"获取A对象锁失败!");
}
}
public void b() {
if(B.lock.tryLock()){
System.out.println(Thread.currentThread().getName()+"持有B对象锁!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(A.lock.tryLock()){
System.out.println(Thread.currentThread().getName()+"持有A对象锁!");
A.lock.unlock();//释放A对象锁
System.out.println("b已完成!");
}else {
System.out.println(Thread.currentThread().getName()+"获取A对象锁失败!");
}
B.lock.unlock();//释放B对象锁
}else {
System.out.println(Thread.currentThread().getName()+"获取B对象锁失败!");
}
}
}
调用代码同上面,只需把MyObject更换为MyObject2即可,这里就不再展示,直接看看运行结果:
Thread-0持有A对象锁!
Thread-1持有B对象锁!
Thread-1获取A对象锁失败!
Thread-0获取B对象锁失败!
恩,这样总算不至于死锁了,起码程序可以正常执行下去,如果你将a()方法中获取B对象的锁代码更改为:B.lock.tryLock(1,TimeUnit.SECONDS),那么运行结果为:
Thread-0持有A对象锁!
Thread-1持有B对象锁!
Thread-1获取A对象锁失败!
Thread-0持有B对象锁!
a已完成!
很显然,因为a()获取B对象锁设置了超时时间为1秒,而在b()方法中释放lock后,a()得以获取到B对象锁,所以可以执行完毕。
总结来说,Lock和synchronized有以下几点不同:(参考该文)
1)Lock是一个接口,而synchronized是Java中的关键字,synchronized是内置的语言实现;
2)synchronized在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生;而Lock在发生异常时,如果没有主动通过unLock()去释放锁,则很可能造成死锁现象,因此使用Lock时需要在finally块中释放锁;
3)Lock可以让等待锁的线程响应中断,而synchronized却不行,使用synchronized时,等待的线程会一直等待下去,不能够响应中断;
4)通过Lock可以知道有没有成功获取锁,而synchronized却无法办到。
5)Lock可以提高多个线程进行读操作的效率。
在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。
网友评论