在JAVA并发编程中,我们使用锁来确保可变共享变量的安全性。要注意的是,不正确的使用锁很容易导致死锁。
死锁的4个必要条件:
1. 互斥访问
2. 非抢占
3. 持有并等待
4. 循环等待
在JAVA编程中,有3种典型的死锁类型:
静态的锁顺序死锁,动态的锁顺序死锁,协作对象之间发生的死锁。
静态的锁顺序死锁:
a和b两个方法都需要获得A锁和B锁。一个线程执行a方法且已经获得了A锁,在等待B锁;另一个线程执行了b方法且已经获得了B锁,在等待A锁。这种状态,就是发生了静态的锁顺序死锁。
//可能发生静态锁顺序死锁的代码
classStaticLockOrderDeadLock{
privatefinalObject lockA=newObject();
privatefinalObject lockB=newObject();
publicvoida(){
synchronized(lockA) {
synchronized(lockB) {
System.out.println("function a");
}
}
}
publicvoidb(){
synchronized(lockB) {
synchronized(lockA) {
System.out.println("function b");
}
}
}
}
解决静态的锁顺序死锁的方法就是:所有需要多个锁的线程,都要以相同的顺序来获得锁。
//正确的代码
classStaticLockOrderDeadLock{
privatefinalObject lockA=newObject();
privatefinalObject lockB=newObject();
publicvoida(){
synchronized(lockA) {
synchronized(lockB) {
System.out.println("function a");
}
}
}
publicvoidb(){
synchronized(lockA) {
synchronized(lockB) {
System.out.println("function b");
}
}
}
}
动态的锁顺序死锁:
动态的锁顺序死锁是指两个线程调用同一个方法时,传入的参数颠倒造成的死锁。如下代码,一个线程调用了transferMoney方法并传入参数accountA,accountB;另一个线程调用了transferMoney方法并传入参数accountB,accountA。此时就可能发生在静态的锁顺序死锁中存在的问题,即:第一个线程获得了accountA锁并等待accountB锁,第二个线程获得了accountB锁并等待accountA锁。
//可能发生动态锁顺序死锁的代码
classDynamicLockOrderDeadLock{
publicvoidtransefMoney(Account fromAccount,Account toAccount,Double amount){
synchronized(fromAccount) {
synchronized(toAccount) {
//...
fromAccount.minus(amount);
toAccount.add(amount);
//...
}
}
}
}
动态的锁顺序死锁解决方案如下:使用System.identifyHashCode来定义锁的顺序。确保所有的线程都以相同的顺序获得锁
//正确的代码
classDynamicLockOrderDeadLock{
privatefinalObject myLock=newObject();
publicvoidtransefMoney(finalAccount fromAccount,finalAccount toAccount,finalDouble amount){
classHelper{
publicvoidtransfer(){
//...
fromAccount.minus(amount);
toAccount.add(amount);
//...
}
}
intfromHash=System.identityHashCode(fromAccount);
inttoHash=System.identityHashCode(toAccount);
if(fromHash
synchronized(fromAccount) {
synchronized(toAccount) {
newHelper().transfer();
}
}
}elseif(fromHash>toHash){
synchronized(toAccount) {
synchronized(fromAccount) {
newHelper().transfer();
}
}
}else{
synchronized(myLock) {
synchronized(fromAccount) {
synchronized(toAccount) {
newHelper().transfer();
}
}
}
}
}
}
协作对象之间发生的死锁:
有时,死锁并不会那么明显,比如两个相互协作的类之间的死锁,比如下面的代码:一个线程调用了Taxi对象的setLocation方法,另一个线程调用了Dispatcher对象的getImage方法。此时可能会发生,第一个线程持有Taxi对象锁并等待Dispatcher对象锁,另一个线程持有Dispatcher对象锁并等待Taxi对象锁。
//可能发生死锁
classTaxi{
privatePoint location,destination;
privatefinalDispatcher dispatcher;
publicTaxi(Dispatcher dispatcher) {
this.dispatcher=dispatcher;
}
publicsynchronizedPoint getLocation(){
returnlocation;
}
publicsynchronizedvoidsetLocation(Point location){
this.location=location;
if(location.equals(destination))
dispatcher.notifyAvailable(this);//外部调用方法,可能等待Dispatcher对象锁
}
}
classDispatcher{
privatefinalSet taxis;
privatefinalSet availableTaxis;
publicDispatcher(){
taxis=newHashSet();
availableTaxis=newHashSet();
}
publicsynchronizedvoidnotifyAvailable(Taxi taxi){
availableTaxis.add(taxi);
}
publicsynchronizedImage getImage(){
Image image=newImage();
for(Taxi t:taxis)
image.drawMarker(t.getLocation());//外部调用方法,可能等待Taxi对象锁
returnimage;
}
}
上面的代码中,我们在持有锁的情况下调用了外部的方法,这是非常危险的(可能发生死锁)。为了避免这种危险的情况发生,我们使用开放调用。如果调用某个外部方法时不需要持有锁,我们称之为开放调用。
解决协作对象之间发生的死锁:需要使用开放调用,即避免在持有锁的情况下调用外部的方法。
//正确的代码
classTaxi{
privatePoint location,destination;
privatefinalDispatcher dispatcher;
publicTaxi(Dispatcher dispatcher) {
this.dispatcher=dispatcher;
}
publicsynchronizedPoint getLocation(){
returnlocation;
}
publicvoidsetLocation(Point location){
booleanflag=false;
synchronized(this) {
this.location=location;
flag=location.equals(destination);
}
if(flag)
dispatcher.notifyAvailable(this);//使用开放调用
}
}
classDispatcher{
privatefinalSet taxis;
privatefinalSet availableTaxis;
publicDispatcher(){
taxis=newHashSet();
availableTaxis=newHashSet();
}
publicsynchronizedvoidnotifyAvailable(Taxi taxi){
availableTaxis.add(taxi);
}
publicImage getImage(){
Set copy;
synchronized(this) {
copy=newHashSet(taxis);
}
Image image=newImage();
for(Taxi t:copy)
image.drawMarker(t.getLocation());//使用开放调用
returnimage;
}
}
综上,是常见的3种死锁的类型。即:静态的锁顺序死锁,动态的锁顺序死锁,协作对象之间的死锁。在写代码时,要确保线程在获取多个锁时采用一致的顺序。同时,要避免在持有锁的情况下调用外部方法。