产生死锁的四个必要条件
互斥使用: 一个资源每次只能被一个线程使用。这意味着如果一个线程已经获取了某个资源(比如锁),那么其他线程就必须等待,直到该线程释放资源。
不可抢占: 已经获得资源的线程在释放资源之前,不能被其他线程抢占。只有拥有资源的线程自己能够释放资源,其他线程无法将其强行抢占。
请求保持: 一个线程在持有至少一个资源的情况下,又请求获取其他资源。这样的情况下,如果其他资源被其他线程持有并且不释放,就会导致请求线程等待,从而可能形成死锁。
循环等待: 存在一组等待进程 {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,请求资源2
Thread 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,请求资源1
Thread 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持有的资源