产生死锁的四个必要条件
互斥使用: 一个资源每次只能被一个线程使用。这意味着如果一个线程已经获取了某个资源(比如锁),那么其他线程就必须等待,直到该线程释放资源。
不可抢占: 已经获得资源的线程在释放资源之前,不能被其他线程抢占。只有拥有资源的线程自己能够释放资源,其他线程无法将其强行抢占。
请求保持: 一个线程在持有至少一个资源的情况下,又请求获取其他资源。这样的情况下,如果其他资源被其他线程持有并且不释放,就会导致请求线程等待,从而可能形成死锁。
循环等待: 存在一组等待进程 {P1, P2, ..., Pn},其中P1等待P2持有的资源,P2等待P3持有的资源,...,Pn等待P1持有的资源。这样的循环等待条件是死锁的充分条件
注意 以上是必要条件 在数学中 分必要条件 充要条件等 必要条件是四个缺一不可的
也就是说 要产生死锁 这些条件是不可或缺的
以上四个必要条件分别用java代码解释说明
目录
互斥使用
不可抢占
请求保持
循环等待
互斥使用
互斥是指在多任务处理中,对共享资源的访问进行限制,确保同一时刻只有一个任务(或线程)能够访问共享资源。这种限制保证了对共享资源的安全访问,避免了数据竞争和数据不一致的问题。
在并发编程中,互斥通常通过锁(如Java中的`synchronized`关键字或`Lock`接口)来实现。当一个任务需要访问共享资源时,它会尝试获取锁,如果锁已被其他任务持有,则该任务会被阻塞,直到锁被释放。一旦任务获取到锁,它就可以安全地访问共享资源,在完成操作后释放锁,以便其他任务可以继续访问。
函数起名根据:
public static void criticalSection1() {System.out.println(Thread.currentThread().getName() + "进入临界区");System.out.println(Thread.currentThread().getName() + "离开临界区");}public static void main(String[] args) {new Thread(()->{criticalSection1();}).start();new Thread(()->{criticalSection1();}).start();}
对于这段代码 会有这样一个执行结果 因为是并发执行的
可以看到 在进程1进入临界区的时候 0也能进入临界区
接下来我们加上一段锁
private static final Object lock = new Object();public static void criticalSection() {synchronized(lock) {System.out.println(Thread.currentThread().getName() + "进入临界区");// 这里是临界区,只有一个线程可以执行这段代码System.out.println(Thread.currentThread().getName() + "离开临界区");}}public static void main(String[] args) {new Thread(()->{criticalSection();}).start();new Thread(()->{criticalSection();}).start();}
对于这段代码 只有这一一种执行结果 因为每次只有一个线程能够进入临界区执行代码,确保了临界区内的操作不会被并发执行,从而避免了数据竞争和数据不一致的问题。 表现了互斥等到(两个或多个线程同时想要获取一个资源 但是只能等到另一个释放)
不可抢占
public class Main3 {private static final Object lock = new Object();public static void main(String[] args) {// 线程1获取锁并执行耗时操作new Thread(() -> {try {// 等待2启动,保证2在1之后获取锁Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}synchronized(lock) {System.out.println("线程1获得了锁");try {// 模拟耗时操作Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("线程1释放了锁");}}).start();// 线程2尝试获取锁new Thread(() -> {try {// 等待一段时间,模拟线程2稍晚启动Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}synchronized(lock) {// 线程2无法获取锁,因为锁已被线程1持有System.out.println("线程2获得了锁");}System.out.println("线程2释放了锁");}).start();}
}
以上代码有两种执行结果
线程2获得了锁
线程2释放了锁
线程1获得了锁
线程1释放了锁
线程1获得了锁
线程1释放了锁
线程2获得了锁
线程2释放了锁
而没有 1获得了锁下一句是2释放了锁 这种情况
这就表现不可抢占的特性,即已经获得资源的线程在释放资源之前,不能被其他线程抢占。
请求保持
先想一下这段话: 一个线程在持有至少一个资源的情况下,又请求获取其他资源。这样的情况下,如果其他资源被其他线程持有并且不释放,就会导致请求线程等待,从而可能形成死锁。
我们可以理解为
t1线程 在持有lock资源的情况下,又请求获取lock2资源。这样的情况下,如果lock2资源被t2线程持有并且不释放,就会导致请求线程等待,从而可能形成死锁。
public class Main4 {private static Object lock = new Object();private static Object lock2 = new Object();public static void main(String[] args) {Thread t1 = new Thread(()-> {synchronized (lock) {System.out.println("t1获得lock");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}synchronized (lock2) {System.out.println("t1获得lock2");}}});Thread t2 = new Thread(()->{synchronized (lock2) {System.out.println("t2获得lock2");while (true) {try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}}}});t1.start();t2.start();}
}
可以观察到 t2不释放lock2 t1就不能拿到lock2
以上表现出 请求保持是死锁的一个必要条件之一,指的是一个线程在持有至少一个资源的情况下,又请求获取其他资源,但这些资源已被其他线程持有并且不释放,从而导致请求线程等待,可能形成死锁。
循环等待
也就是哲学家进餐问题
public class Main5 {private static final Object resource1 = new Object();private static final Object resource2 = new Object();public static void main(String[] args) {// 线程1持有资源1,请求资源2Thread t1 = new Thread(() -> {synchronized (resource1) {System.out.println("线程1持有资源1");try {Thread.sleep(1000); // 为了确保线程2先持有资源2} catch (InterruptedException e) {e.printStackTrace();}synchronized (resource2) {System.out.println("线程1持有资源2");}}});// 线程2持有资源2,请求资源1Thread t2 = new Thread(() -> {synchronized (resource2) {System.out.println("线程2持有资源2");synchronized (resource1) {System.out.println("线程2持有资源1");}}});t1.start();t2.start();}
}
导致了循环等待 也就是 存在一组等待进程 {P1, P2},其中P1等待P2持有的资源,P2等待P1持有的资源