1 死锁成因
死锁是在多线程或多进程环境中一种特定的并发问题。当两个或多个线程(或进程)相互等待对方所持有的资源时,就会发生死锁,导致系统无法继续执行。就是说,死锁是由于相互等待对方所持有的资源而导致的一种僵局。在这种状态下,系统无法继续进行,不能取得任何进展。
在Java中,synchronized关键字用于创建线程安全的类或方法,确保同步方法或同步块在同一时间只能由一个线程访问,从而防止多线程环境中的数据损坏和竞态条件的发生。当一个线程正在执行同步代码时,其他线程必须等待,直到当前线程释放锁,才能访问同步资源。这种机制确保了线程之间的顺序执行,可避免数据不一致的问题。
图片
图片
然而,在使用synchronized关键字时需小心谨慎,因为可能导致死锁的问题。当多个线程以不同的顺序请求相同的锁时,可能会发生死锁。例如,线程A持有锁A并等待锁B,而线程B持有锁B并等待锁A,它们会相互等待对方释放锁,导致程序无法继续执行。
Thread A: Lock Resource 1 Wait for Resource 2
Thread B: Lock Resource 2 Wait for Resource 1
- 线程A:锁定资源1,等待资源2
- 线程B:锁定资源2,等待资源1
例如,假设有两个线程,
- BankTransferExample表示了两个线程(transferThread1和transferThread2)在两个银行账户之间转账的场景,锁/资源(account1和account2)代表了与账户1和账户2相关联的锁。
- Lock1和Lock2分别对应于与account1和account2相关联的锁。
- 每个转账线程首先获取一个账户的锁,在等待一段时间以模拟工作后,再获取另一个账户的锁,然后执行转账操作。
- BankAccount是一个简单的表示银行账户的类,具有转账和存款资金的方法。
以下是代码示例:
public class BankTransferExample {
public static final Object Lock1 = new Object();
public static final Object Lock2 = new Object();
public static void main(String[] args) {
BankAccount account1 = new BankAccount(1000);
BankAccount account2 = new BankAccount(1500);
Thread transferThread1 = new Thread(() -> {
synchronized (Lock1) {
System.out.println("转账线程1:获取锁1。");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("转账线程1:等待锁2...");
synchronized (Lock2) {
System.out.println("转账线程1:获取锁1和锁2。");
account1.transferTo(account2, 200);
}
}
});
Thread transferThread2 = new Thread(() -> {
synchronized (Lock2) {
System.out.println("转账线程2:获取锁2。");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("转账线程2:等待锁1...");
synchronized (Lock1) {
System.out.println("转账线程2:获取锁1和锁2。");
account2.transferTo(account1, 100);
}
}
});
transferThread1.start();
transferThread2.start();
}
}
class BankAccount {
private int balance;
public BankAccount(int initialBalance) {
this.balance = initialBalance;
}
public void transferTo(BankAccount targetAccount, int amount) {
if (this.balance >= amount) {
this.balance -= amount;
targetAccount.deposit(amount);
System.out.println("将 $" + amount + " 从一个账户转账到另一个账户。");
} else {
System.out.println("转账余额不足。");
}
}
public void deposit(int amount) {
this.balance += amount;
}
}
输出结果:
Transfer Thread 1: Lock 1 acquired.
Transfer Thread 2: Lock 2 acquired.
Transfer Thread 1: Waiting for Lock 2...
Transfer Thread 2: Waiting for Lock 1...
现在,对输出进行解析:
- 转账线程1:已获取锁1。线程1开始执行并成功获取锁1。
- 转账线程2:已获取锁2。线程2开始执行并成功获取锁2。
此时,两个线程都已经各自获取了一个锁。然而,它们现在需要另一个锁来完成交易,从而进入等待阶段:
- 转账线程1:等待锁2
线程1在持有锁1的同时,尝试获取锁2以完成交易。然而,锁2已被线程2获取,因此线程1被迫等待锁2释放。
- 转账线程2:等待锁1
类似的,线程2在持有锁2的同时,尝试获取锁1以完成交易。然而,锁1已被线程1获取,因此线程2被迫等待锁1释放。
此时,两个线程都处于等待状态,每个线程都在等待另一个线程释放它所需的锁。由于处于死锁状态,因此两个线程都无法继续执行。
2 预防死锁
2.1 锁的顺序
锁的顺序是一种简单但有效的死锁预防技术。它要求所有线程按照相同的顺序获取锁。在示例中,有两个银行账户,并且多个线程代表这些账户之间的交易。为了避免死锁,将定义一种一致的顺序,以避免循环等待条件。下面是修改后的代码:
public class BankTransferExample {
public static final Object Lock1 = new Object();
public static final Object Lock2 = new Object();
public static void main(String[] args) {
BankAccount account1 = new BankAccount(1000);
BankAccount account2 = new BankAccount(1500);
Thread transferThread1 = new Thread(() -> {
synchronized (Lock1) {
System.out.println("转账线程1:已获取锁1。");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("转账线程1:等待锁2...");
synchronized (Lock2) {
System.out.println("转账线程1:已获取锁1和锁2。");
account1.transferTo(account2, 200);
}
}
});
Thread transferThread2 = new Thread(() -> {
synchronized (Lock1) {
System.out.println("转账线程2:已获取锁1。");
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("转账线程2:等待锁2...");
synchronized (Lock2) {
System.out.println("转账线程2:已获取锁1和锁2。");
account2.transferTo(account1, 100);
}
}
});
transferThread1.start();
transferThread2.start();
}
}
Transfer Thread 1: Lock 1 acquired.
Transfer Thread 1: Waiting for Lock 2...
Transfer Thread 1: Lock 1 & Lock 2 acquired.
Transferred $200 from one account to another.
Transfer Thread 2: Lock 2 acquired.
Transfer Thread 2: Waiting for Lock 1...
Transfer Thread 2: Lock 1 & Lock 2 acquired.
Transferred $100 from one account to another.
这个银行账户转账程序如何避免死锁?
该银行账户转账程序使用锁来避免死锁。两个线程,转账线程1和转账线程2,都需要获取锁1和锁2才能进行转账。然而,它们以不同的顺序获取锁。
转账线程1:
(1)获取锁1。
(2)等待锁2。
(3)获取锁2。
(4)从一个账户转账200美元到另一个账户。
(5)释放锁2。
(6)释放锁1。
转账线程2:
(1)获取锁2。
(2)等待锁1。
(3)获取锁1。
(4)从一个账户转账100美元到另一个账户。
(5)释放锁1。
(6)释放锁2。
这两个线程以不同的顺序获取锁,但释放锁的顺序与获取锁的相反顺序相同。这样可以避免死锁。
在这种情况下避免死锁的关键是,两个线程按照相同的顺序获取锁:首先是Lock1,然后是Lock2。锁获取顺序的一致性确保了一个线程在另一个线程释放锁之后可以继续执行,避免了循环等待条件,从而使两个交易都能成功完成。
2.2 使用超时机制
使用超时机制是预防死锁的另一种方式。在获取锁时,线程可以指定一个超时时间。如果在指定的时间内无法获取锁,线程将放弃并稍后重试。
这在某些情况下很有用,例如线程正在等待一个被其他线程持有且无响应或被阻塞的锁。通过使用超时机制,线程可以避免进入死锁状态。
public class LockTimeoutExample {
public static final Object Lock1 = new Object();
public static final Object Lock2 = new Object();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
synchronized (Lock1) {
System.out.println("线程1:已获取锁1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
if (synchronized (Lock2, 1000)) {
System.out.println("线程1:已获取锁2");
} else {
System.out.println("线程1:等待锁2超时");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (Lock1) {
System.out.println("线程2:已获取锁1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (Lock2) {
System.out.println("线程2:已获取锁2");
}
}
});
thread1.start();
thread2.start();
}
}
Thread 1: Acquired Lock1
Thread 2: Acquired Lock1
Thread 2: Acquired Lock2
Thread 1: Timed out waiting for Lock2
解释:
在这个示例中,线程1在尝试获取锁2时使用了超时机制。这意味着如果在指定的时间内无法获取锁,它将打印一条消息并继续执行。
在这种情况下,线程1能够获取锁1,但无法获取锁2。而线程2则能够获取两个锁。在线程2获取锁2之后,线程1超时并打印一条消息。