任务在访问数据前必须申请得到锁,然后执行计算;最后要释放锁,以便其他任务可以对这
些数据执行别的操作。不幸的是,尽管锁在一定程度上能避免数据竞争,但是它也给现代软
件开发带来了严重问题。
最主要的问题是,锁不具有可组合性。我们不能保证由两部分以锁为基础、能正确运行的代
码合并得到的程序依然正确。而现代软件开发的基础恰恰是将小程序库合并为更大程序的组
装能力;因此,我们无法做到不考察组件的具体实现,就能在它们的基础上组装大软件。
造成组装失败的祸首是死锁。考虑最简单的情况,当两个任务以相反的顺序申请两个锁时,
死锁就出现了:线程T1 获得了锁L1,线程T2 获得了锁L2,然后,T1申请获得锁L2,同时
T2 申请获得L1。此时,两个线程将永久阻塞,死锁的情况就出现了。
死锁发生必须同时满足以下四个条件:
1. 互斥:至少有一个资源必须处于非共享模式;即一次只有一个线程使用。如果另一资源
申请该资源,那么申请线程必须延迟直到该资源释放为止。
2. 占有并等待:一个线程必须占有至少一个资源,并等待另一个资源,而该资源为其他线
程所占用。
3. 非抢占:资源申请者不能强行从资源占有着手中夺取资源,并且所有线程都不放弃已经
占有的资源。
4. 循环等待:有一组线程{T0,T1,…,Tn},T0等待的资源被T1 占用,T1等待的资源被T2
占用,Tn-1等待的资源被Tn占用。
只要破坏以上条件就可以避免死锁。
要避免死锁,最好的方法是复制原本需要互斥访问的资源,这样每一个线程都拥有一个资源
的私有副本。每个线程访问自己的副本,从而可以避免使用锁。如果有需要的话,可以在程
序的最后将各个线程占有的资源副本合并成一个单一的共享资源副本。这种不使用锁的方
法,从根本上消除了死锁存在的可能,并进一步提高了可扩展性和该组件的可组装性。
如果资源无法被复制,那么一般的做法是按照一定的顺序获取资源(锁),保存一致的锁获
取顺序可避免死锁环的出现。
我觉得可以这么理解死锁和饥饿的区别,首先死锁是同步的,饥饿时异步的。也就是说,死锁可以认为是两个线程或进程同时在请求对方占有的资源,饥饿可以认为是一个线程或是进程在无限的等待另外两个或多个线程或进程占有的但是不会往外释放的资源。
介绍“死锁”的例子1:如果线程A锁住了记录R1并等待记录R2,而线程B锁住了记录R2并等待记录R1,这样两个线程A和B就发生了死锁现象。
介绍“死锁”的例子2:两个山羊过一个独木桥,两只羊同时走到桥中间,一个山羊等另一个山羊过去了然后再过桥,另一个山羊等这一个山羊过去,结果两只山羊都堵在中间动弹不得。
介绍饥饿的例子:资源在其中两个或以上线程或进程相互使用,第三方线程或进程始终得不到。想像一下三个人传球,其中两个人传来传去,第三个人始终得不到。
出现以下四种情况会产生死锁:
1,相互排斥。一个线程或进程永远占有共享资源,比如,独占该资源。
2,循环等待。例如,进程A在等待进程B,进程B在等待进程C,而进程C又在等待进程A。
3,部分分配。资源被部分分配,例如,进程A和B都需要访问一个文件,同时需要用到打印机,进程A得到了这个文件资源,进程B得到了打印机资源,但两个进程都不能获得全部的资源了。
4,缺少优先权。一个进程获得了该资源但是一直不释放该资源,即使该进程处于阻塞状态。
按照前面的分析,情况1和情况4是线程或进程无休止的等待另外的几个线程或进程所占据的资源。
针对上面可能出现死锁的几种情况,可以给出预防措施,比如控制线程或进程如果判断不能够获取到所需的全部的资源,那么就释放已经占有的资源等等。
现实中,当遇到一个对象要执行多个动作,而这些动作又穿插在一起时就要使用线程的概念来编写程序,此时就需要特别注意程序执行效率的问题和预防死锁或者饥饿问题的出现。