java 在协作对象之间发生的死锁

java 在协作对象之间发生的死锁

一种比较隐蔽的死锁,理解起来有挑战性,修正起来也比“顺序锁死锁”更有难度。

继续《Java并发编程实战》第⑩章的学习,文章内容继承上一篇文章。

 

一、在协作对象之间发生的死锁

(1)经典例子

死锁往往发生在不同对象的同步方法相互调用之时。先来看一个经典例子:

TestPlayer.java

运行结果为:

虽然这段程序中没有上一节中明显的“双重嵌套”,但是死锁问题依然存在。

问题出在Player类的attack()方法:

这段代码是这样调用的:

fromPlayer在调用attack()同步方法时,首先要获得fromPlayer这个对象的对象锁。attack()内部调用player.getHp()时,需要获得toPlayer这个对象的对象锁。这是一种比较隐蔽的“双重嵌套”。

因此,在fromPlayer和toPlayer相互攻击时,就有可能发生死锁。

(2)如何解决该问题

1.开放调用

首先理解一下:

方法调用相当于一种抽象屏障,你无需了解被调用方法中执行的操作。

重新关注一下attack()方法:

也就是说,写attack()方法的时候,我们不会特别留意getHp()和setHp()是怎么作用的,我只想得到这两个方法的结果而已。因此,这种状况很容易出现死锁。

既然很容易中招,我们能不能总结一个规律,减少这种情况的产生呢?要记住一个原则——开放调用。

如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用。

要如何理解这句话?举个简单例子你就明白了:

2.开放调用时如何保证线程安全?

问题来了:一个开放调用的方法不会被synchronized修饰,那就不是一个同步方法,不会引发线程安全问题吗?

做个实验,改写一下Player类,去掉attack()方法:

Player.java

运行结果变成了:

虽然没有出现死锁,但是结果中两个player的hp都不为0,现在的attack()方法肯定不是线程安全的。

那么问题来了,开放调用时如何保证线程安全?我们重新理解一下这句话:

如果在调用某个方法时不需要持有锁,那么这种调用被称为开放调用。

要清楚,“如果在调用某个方法时不需要持有锁”≠“在这个方法内不能获取锁”。如果在attack()方法内获取player(被攻击玩家)的对象锁,不是一样能保证线程安全吗?

改写一下Player类:

Player.java

运行结果为:

并没有出现死锁,结果也是正确的。

我们主要看attack()方法:

主要逻辑操作的都是player对象,所以只要保证player线程安全即可。直接用synchronized锁住整个对象,代替synchronized方法。

(顺带一提,synchronized是可重入锁,只要获得了player的对象锁,所有需要同一对象锁的方法都可以被调用。)

综上,个人认为,解决方案就是:

只使用同步代码块去保护涉及共享状态的操作。

为此,我们应该养成良好的变成习惯:

不要因为简单、语法紧凑等简单理由,在非必须条件下(整个方法不需要通过一个锁来保护)使用同步方法(而不是同步代码块)。

在上面的例子中,这两种写法确实是等价的,没必要刻意用开放调用的写法:

二、总结

敲了下书上的例子,感觉对死锁问题有了更深的理解。

发布者

xie4ever

发表评论

电子邮件地址不会被公开。