概念的引入
我们先看一段代码与运行框。
package Thread;
public class Synchronized {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lock1) {
System.out.println("t1使用锁1");
try {
Thread.sleep(100); // 确保线程2能获取到lock2
} catch (InterruptedException e) {}
System.out.println("t1 等待 lock 2...");
synchronized (lock2) {
System.out.println("t1获取两个锁!");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
System.out.println("t2使用锁2");
try {
Thread.sleep(100); // 确保线程1能获取到lock1
} catch (InterruptedException e) {}
System.out.println("t2: 等待锁1...");
synchronized (lock1) {
System.out.println("t2获取两个锁!");
}
}
});
t1.start();
t2.start();
}
}
我们可以发现t1 与t2 都无法获取对方先使用的lock1和lock2.这是为什么?明明我们在代码中让线程t1与t2都获取了lock1与lock2,但是代码并没有执行。
原因
Synchronized
Synchronized是Java中用于实现线程同步的关键字,它提供了一种内置的锁机制,可以确保多个线程安全地访问共享资源,具有三个核心作用---原子性,可见性,有序性。
原子性:确保被保护的代码块或方法作为一个不可分割的单元执行。
可见性:当一个线程释放锁时,它对共享变量的修改对其他线程立即可见。
有序性:防止指令重排序,确保代码按编写顺序执行。
代码解析
线程中:
线程1获取lock1,线程2获取lock2。
线程1尝试获取lock2(但被线程2持有)
线程2尝试获取lock1(但被线程1持有)
由于Synchronized的作用,代码块无法被分割,且顺序执行。导致了线程1持有lock1等待着lock2,但是线程2不释放lock2.而线程2持有lock2等待lock1,线程1不释放lock1.导致了循环等待。
死锁的四个必要条件
互斥条件(Mutual Exclusion):资源一次只能被一个线程占用,其他线程必须等待该资源被释放后才能获取.
占有且等待(Hold and Wait):线程已经持有至少一个资源,并且正在等待获取其他被占用的资源。
不可抢占(No Preemption):已分配给线程的资源不能被其他线程强行夺取,必须由持有者显式释放.
循环等待(Circular Wait):存在一个线程等待的循环链,每个线程都在等待下一个线程所占用的资源。
学过《操作系统》的同学对死锁的概念,我想并不陌生。
怎么避免死锁?
死锁预防
我们只需要打破死锁的四个必要条件的其中之一,就可以避免死锁。比如:我们可以一次性将线程需要的资源在线程创建时就,全部分配给它。
死锁避免
死锁避免(Deadlock Avoidance)是一种动态预防死锁的策略,它通过资源分配策略确保系统永远不会进入不安全状态(即可能导致死锁的状态)。与死锁预防(Deadlock Prevention)不同,死锁避免不会破坏死锁的四个必要条件,而是通过智能的资源分配算法来避免系统进入可能发生死锁的状态。(这是书上的概念。)
类似的,我们可以通过银行家算法来实现这一过程。
当然,还有一种死锁检测与恢复 ,我们现在就不深入探讨了。以后有机会在进行讲解。