产生的原因: 每个线程拥有其他线程需要的资源,同时又等待其他线程已经拥有的资源,并且所有线程在获得所有需要的资源之前都不会放弃已经拥有的资源。
检查死锁的技巧:
- 不同的线程会用到两个相同的锁;
- 并且不同线程获取锁的顺序不同。
产生原因: 两个线程试图以不同的顺序来获得相同的两个及以上的锁。
静态顺序死锁:
private final Object left = new Object();
private final Object right = new Object();
new Thread() {
public void run() {
synchronized (left) {
synchronized (right) {
doSomething();
}
}
}
}.start();
new Thread() {
public void run() {
synchronized (right) {
synchronized (left) {
doSomething();
}
}
}
}.start();
动态顺序死锁:
public static void transferMoney(Account fromAccount,
Account toAccount,
double money) {
synchronized (fromAccount) {
synchronized (toAccount) {
// 转账
}
}
}
Account account1 = new Account();
Account account2 = new Account();
new Thread() {
public void run() {
transferMoney(account1, account2, 100.0);
}
}.start();
new Thread() {
public void run() {
transferMoney(account2, account1, 100.0);
}
}.start();
- 两个线程持有对方需要的资源并不放开自己拿到的资源。
- 例子:单线程 Executor,等待的资源在工作队列中。
- 避免一个线程同时获取多个锁;
- 避免一个线程在锁内占有多个资源,尽量保证每个锁只占有一个资源;
- 使用定时锁,即
lock.tryLock(timeout)
,这样拿不到锁就放弃,不会发生死锁一直卡在那里; - 对于数据库锁,加锁和解锁必须在同一个数据库连接中,否则可能会解锁失败。
- 线程由于无法访问它所需要的资源而不能继续执行。
- 最常见资源就是 CPU 时钟周期 。如果在 Java 应用程序中对线程的优先级使用不当,或者在持有锁时执行一些无法结束的结构(例如无限循环,或者无限制地等待某个资源),那么也可能导致饥饿,因为其他需要这个锁的线程将无法得到它。
- 尽量不要改变线程的优先级 。只要改变了线程的优先级,程序的行为就将与平台相关,并且会导致发生饥饿问题的风险。你经常能发现某个程序会在一些奇怪的地方调用 Thread.sleep 或 Thread.yield,这是因为该程序试图克服优先级调整问题或响应性问题,试图让低优先级的线程执行更多的时间。
- 当多个相互协作的线程都对彼此进行响应从而修改各自的状态,并使得任何一个线程都无法继续执行时,就发生了活锁。这就像两个过于礼貌的人在半路上面对面地相遇:他们彼此都让出对方的路,然而又在另一条路上相遇了,因此他们就这样反复地避让下去。
- 解决方法: 在重试机制中引入随机性。
资源限制指在进行并发编程时,程序的执行速度受限于计算机硬件资源。比如服务器的带宽只有 2 MB/s,某个资源的下载速度是 1 MB/s,那么开 10 个线程下载,下载速度也不会变成 10 MB/s。
并发编程中,将代码执行速度加快的原则是将代码串行执行的部分变成并发执行,但是如果本该并发执行的部分,由于资源限制的原因,仍然在串行执行,此时,由于多个线程间竞争的缘故,增加了上下文切换次数以及资源调度的时间,反而会比串行执行的还慢。
对于硬件资源限制,可以使用集群并行执行程序,让程序在多台机器上运行,让不同的机器处理不同的数据。例如:可以通过 “数据 ID % 机器数”,计算出该数据应该在编号为多少的机器上运行。
对于软件资源限制,可以使用资源池将资源复用。例如:使用连接池将数据库和 Socket 连接复用。