Java多线程编程——初探死锁现象

在操作系统中,当两个以上的运算单元,双方都在等待对方停止运行,以获取系统资源,但是没有一方提前退出时,就称为死锁。例如,线程A在持有锁L1的情况下申请锁L2,而线程B在持有锁L2的情况下申请L1,A只有在获得并释放L2后才会释放L1,而B只有在获得并释放L1后才会释放L2。也就是说,A和B各自都在持有一个锁(分别为L1和L2)的情况下去申请对方持有的另外一个锁(分别为L2和L1),而A和B释放其持有的锁的前提又都是先获得对方持有的另外一个锁,因此两个线程最终都无法获得它们申请的另外一个锁而处于无限等待的状态,这就是死锁的产生。

                   线程A

                  /      \

        持有/          \ 申请

             /              \

          锁L1           锁L2

              \                /

       申请  \            / 持有

                  \        /

                   线程B

我们来看这样一个例子。现在有Alice和Bob两个人去吃午餐,但是餐桌上现在只有一把叉子和一把餐刀。假如Alice眼疾手快先拿到了叉子,紧接着Bob抢到了桌子上的餐刀。那么现在Alice要想能吃得了饭,必须要拿到Bob手中的餐刀;同样,Bob要想吃饭,也都拿到Alice手上的叉子。可是,两个人都不愿放下手中抢到的餐具,于是两个人只能面面相觑,谁也吃不了饭。

我们用Java程序可以模拟这样的情景。

public class Main {
    public static void main(String[] args) {
        new Launch("Alice", 0).start();
        new Launch("Bob", 1).start();
    }
}

class Fork { }

class Knife { }

class Launch extends Thread {
    // should be static, so all the objects of
    // the class share the same fork & knife
    private static Fork fork = new Fork();
    private static Knife knife = new Knife();

    private String name;
    private int choice;

    public Launch(String name, int choice) {
        this.name = name;
        this.choice = choice;
    }

    @Override
    public void run() {
        try {
            eat();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void eat() throws InterruptedException {
        if (choice == 0) {
            synchronized (fork) {
                System.out.println(name + " gets fork");
                Thread.sleep(500);
                synchronized (knife) {
                    System.out.println(name + " gets knife");
                }
            }
        } else {
            synchronized (knife) {
                System.out.println(name + " gets knife");
                Thread.sleep(1000);
                synchronized (fork) {
                    System.out.println(name + " get fork");
                }
            }
        }
    }
}

在上面的代码中,Launch类模拟了一个人吃饭的过程:若choice为0,则先试图抢占叉子,在拿到叉子后又试图抢占餐刀;若choice为1,则先试图抢占餐刀,在拿到餐刀后又试图抢占叉子。需要注意的是,Launch类中的fork和knife均为静态变量,即它是被所有Launch类的对象所共享的,或者说这些资源仅有一份,这样才能保证食客抢的是同一把叉子和餐刀。

运行这段代码,会发现程序会卡死:

Alice gets fork
Bob gets knife

Alice先拿到了叉子,然后去休息0.5秒,在这段时间内Bob过来抢走了餐刀,然后去休息1秒。之后轮到Alice回过神来要拿餐刀了,却发现桌子上根本没有餐刀了,于是线程阻塞;Bob回过神来要拿叉子了,却发现桌子上也没有叉子了,线程也阻塞。于是,两个线程都在等待对方释放资源,形成了死锁。

最后总结一下产生死锁的四个必要条件:

1. 互斥条件:一个资源每次只能被一个进程使用。

2. 请求与保持条件:一个进程因请求资源而被阻塞时,对已获得的资源保持不放。

3. 不剥夺条件:进程已获得的资源,在未使用完毕前,不能被强行剥夺。

4. 循环等待条件:若干进程之间形成了一种头尾相接的循环等待资源关系。

若出现了死锁,则上述四条条件一定都会同时存在;但若四个条件都成立时,并不必然发生死锁。避免死锁,我们只要想办法破除其中的任意一个或多个条件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值