死锁

因为线程可以阻塞,并且对象可以具有同步控制方法,用以防止别的线程在锁还没有释放
的时候就访问这个对象。所以就可能出现这种情况:某个线程在等待另一个线程,而后者
又等待别的线程,这样一直下去,直到这个链条上的线程又在等待第一个线程释放锁。你
将得到一个线程之间相互等待的连续循环,没有哪个线程能继续。这被称之为“死锁”
(deadlock)。
如果你运行一个程序,而它马上就死锁了,你当时就能知道出了问题,并且可以跟踪下去。
真正的问题在于,你的程序可能看起来工作良好,但是具有潜在的死锁危险。这时,死锁
可能发生,而事先却没有任何征兆,所以它会潜伏在你的程序里,直到客户发现它出乎意
料地发生(并且你可能很难重现这个问题)。因此,在编写并发程序的时候,进行仔细的
程序设计以防止死锁是一个关键部分。


让我们看一下经典的死锁现象,它是由Dijkstra提出的:哲学家就餐问题。其内容是指定
五个哲学家(不过这里的例子中将允许任意数目)。这些哲学家将用部分的时间思考,部
分的时间就餐。当他们思考的时候,不需要任何共享资源,但当他们就餐的时候,他们坐
在桌子旁并且只有有限数量的餐具。在问题的原始描述中,餐具是叉子,要吃到桌子中央
盘子里的意大利面条需要用两把叉子,不过把餐具看成是筷子更合理;很明显,哲学家要
就餐就需要两根筷子。


问题中引入的难点是:作为哲学家,他们很穷,所以他们只能买的起五根筷子。他们围坐
在桌子周围,每人之间放一根筷子。当一个哲学家要就餐的时候,他/她必须同时得到左边
和右边的筷子。如果一个哲学家左边或右边已经有人在使用筷子了,那么这个哲学家就必
须等待。


注意,这个问题之所以有趣,其原因是它显示了一个程序可能看起来运行正确,但确实存
在死锁的危险。要演示这一点,命令行参数允许你调整哲学家的数量以及一个影响哲学家
思考时间的因子。如果你有许多哲学家,并且/或者他们花很多时间去思考,那么尽管存在

死锁可能,你可能永远也看不到死锁。默认的命令行参数是倾向于使死锁尽快发生

静态字段ponder表示哲学家是否花费时间进行思考。如果值是非零的,那么在think( )
方法里线程将休眠一段由这个值随机产生的时间。通过这种方法,就可以展示,线程(哲
学家)在其它任务(思考)上花费的时越多,那么它们在请求共享资源(筷子)的时候发
生冲突的可能性就越低,这样你就能确信程序没有死锁,尽管事实上可能是有的。


在eat( )里,哲学家通过同步控制获取左边的筷子。如果筷子不可用,那么哲学家将等待
(阻塞)。在获取了左边的筷子之后,再用同样的方法获取右边的筷子。在就餐完毕之后,
先释放右边的筷子,然后释放左边的筷子。


在run( )中,每个哲学家不断重复思考和就餐的动作。


main( )方法至少需要三个参数,如果不满足的话,就打印一条使用信息。第三个参数可
以是字符串“deadlock”,在这种情况下,使用会发生死锁的程序。如果是其它字符串就
使用不产生死锁的程序。最后一个参数(可选的)是一个到期因子,表示一段时间之后将
退出程序(无论是否发生了死锁)。作为本书所附代码的测试程序的一部分,并且是自动
运行的程序,设置一个过期时间是很有必要的。


在创建了Philosopher数组并且设置了ponder值之后,建立了两个Chopstick对象,并
且将书组的第一个元素存储在first变量里供以后使用。除最后一个元素之外,数组里的
每个引用都是通过创建一个新的Philosopher对象,并传递给它左筷子和右筷子对象来进
行初始化的。在每次初始化之后,左边的筷子放到右边,右边的给一个新的Chopstick对
象,供下一个Philosopher使用。


在死锁版本的程序里,最后一个Philosopher被给予了左边的筷子和前面存储的第一个筷
子。这是因为最后一个哲学家就坐在第一个哲学家的旁边,他们共享第一根筷子。在这种
安排下,在某个时间点上就可能出现,所有的哲学家都准备就餐,并且在等待旁边的哲学
家放下他们手中的筷子,此时程序死锁。


试着用不同的命令行参数运行程序,以观察程序的行为,尤其要注意那些使程序看起来可
以正常运行,没有死锁的方式。


要修正死锁问题,你必须明白,当以下四个条件同时满足时,就会发生死锁:


1.互斥条件:线程使用的资源中至少有一个是不能共享的。这里,一根筷子一
次就只能被一个哲学家使用。


2.至少有一个进程持有一个资源,并且它在等待获取一个当前被别的进程持有
的资源。也就是说,要发生死锁,哲学家必须拿着一根筷子并且等待另一根。


3.资源不能被进程抢占。所有的进程必须把资源释放作为普通事件。哲学家很
有礼貌,他们不会从其他哲学家那里抢筷子。


4.必须有循环等待,这时,一个进程等待其它进程持有的资源,后者又在等待
另一个进程持有的资源,这样一直下去,直到有一个进程在等待第一个进程持有
的资源,使得大家都被锁住。在这里,因为每个哲学家都试图先得到左边的筷子,
然后得到右边的筷子,所以发生了循环等待。在上面的例子中,针对最后一个哲
学家,通过交换构造器中的初始化顺序,打破了死锁条件,使得最后一个哲学家
先拿右边的筷子,后拿左边的筷子。


因为要发生死锁的话,所有这些条件必须全部满足,所以你要防止死锁的话,只需破坏其
中一个即可。在程序中,防止死锁最容易的方法是破坏条件 4。有这个条件的原因是每个哲
学家都试图用特定的顺序拿筷子:先左后右。正因为如此,就可能会发生“每个人都拿着左
边的筷子,并等待右边的筷子”的情况,这就是循环等待条件。然而,如果最后一个哲学家
被初始化成先拿右边的筷子,后拿左边的筷子,那么这个哲学家将永远不会阻止其左边的
哲学家拿起他/她右边的筷子,这就打破了循环等待。这只是问题的解决方法之一,你也可
以通过破坏其它条件来防止死锁(具体细节请参考更高级的线程书籍)。


Java对死锁并没有提供语言层面上的支持;能否通过小心的程序设计以避免死锁,取决于
你自己。对于正在试图调试一个有死锁的程序的程序员来说,没有什么更好消息。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值