Java的死锁现象

很久以前学习Java的多线程机制时,一直没有搞懂Java的锁机制,今天花费了一整天的时间研究了一下。在学习的过程中发现一项非常有趣的问题,那就是死现象,下面是对死锁现象的完整描述:


当A线程等待由B线程持有的锁,而B线程正在等待A线程持有的锁,随即发生死锁现象,

JVM不会检测也不试图避免这种情况,完全需要靠程序员自己注意。


要避免锁现象,我们首先需要搞清楚什么是死锁现象,然后才能找到有效的避免方法,接下来我们将探索什么事死锁现象。


一、死锁现象


阅读上面对死锁现象的定义,我们可以断定,死锁现象至少需要两个线程,这里我们假设为Thread A和Thread B。还有,我们知道,每个对象的同步锁同一时刻只能被一个线程持有,所以,根据上面定义,我们还可以断定,这里必须包含两个带锁的对象,我们将这两个带锁的对象命名为Lock X和Lock Y。下面我们来创建代码,观察死锁现象的具体表现形式:


1) 首先创建两个带锁的对象类


    // Lock X
    static class LockX {
        synchronized public void get(LockY lock) {
            System.out.println("Before sleep of lock X");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Waiting for lock Y");
            lock.samething();
        }
        
        synchronized public void something() {
            System.out.println("Lock X somthing method");
        }
    }
    
    static class LockY {
        synchronized public void get(LockX lock) {
            System.out.println("Before sleep of lock Y");
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Waiting for lock X");
            lock.something();
        }
        
        synchronized public void samething() {
            System.out.println("Lock Y somthing method");
        }
    }

上面代码看起来可能很奇怪,难以理解。现在先不要着急去理解每个方法的用意。最后我们会详细讲解每个方法的具体调用过程。


2) 创建两个线程类


    // Thread A
    static class ThreadA extends Thread {
        private LockX lockX;
        private LockY lockY;
        
        public ThreadA(LockX lockx, LockY locky) {
            this.lockX = lockx;
            this.lockY = locky;
        }
        
        @Override
        public void run() {
            System.out.println("Start run thread A");
            lockX.get(lockY);
        }
    }
    
    // Thread B
    static class ThreadB extends Thread {
        private LockY lockY;
        private LockX lockX;
        
        public ThreadB(LockX lockx, LockY locky) {
            this.lockX = lockx;
            this.lockY = locky;
        }
        
        @Override
        public void run() {
            System.out.println("Start run thread B");
            lockY.get(lockX);
        }
    }

3)创建主方法


    public static void main(String[] args) {
        LockX x = new LockX();
        LockY y = new LockY();
        new ThreadA(x, y).start();
        new ThreadB(x, y).start();
    }


首先运行一次程序,观察是否真的发生死锁现象。下面截图展示了控制台的运行结果:




注意上面红色圆圈,表明程序并未结束,而且控制台也没有输出"Lock X somthing method"和"Lock X someting method"两条语句。接下来我们来分析上述代码的执行过程:

首先,我们在main方法中启动了ThreadA和ThreadB两个线程,并且分别为这两个线程传递了两个共享的资源对象x和y,x和y属于不同类的对象,但他们都包含get和something方法,而且这两个方法都是同步方法。

从控制台的输出我们可以确定,首先启动的是ThreadA线程。当ThreadA线程启动后,首先输出"Start run thread A"一行文字,这也是控制台输出的第一行文字。接着ThreadA线程调用x对象的get方法,由于get属于同步方法,所以此时由ThreadA持有该对象的同步锁,直到ThreadA执行完get方法之前,其他线程将不能访问该对象的任何同步方法,这就是同步锁的排斥性。当ThreadA线程进入get方法之后,首先输出一行"Before sleep of lock X"文字,然后进入10毫秒的睡眠状态。

我们知道此时还有一个ThreadB线程已经启动了,当ThreadA线程在(x的)get方法中睡眠时,ThreadB等不及了,调度器此时将CPU让给ThreadB开始执行代码(这里一定要清楚,同步锁并不能保证CPU一次性执行完成同步代码块或同步方法内的所有代码)。

ThreadB开始执行代码,原理与ThreadA线程的开始过程完全相同,先是输出"Start run thread B",接着通过调用y对象的get方法获得y的同步锁,然后输出"Before sleep of lock Y"文字,紧接着也进入10毫秒的睡眠状态。

当ThreadB进入睡眠状态后,调度器又会将CPU的执行权还给ThreadA线程,此时ThreadA线程的睡眠时间已经结束,开始继续执行下面代码。所以ThreadA线程此时应该输出"Waiting for lock Y"一行文字,然后继续执行lock.samething()的调用,lock引用的是y对象,由于此时y对象的同步锁是由线程ThreadB持有的,所以ThreadA不能进入y的something()方法,只能等待ThreadB释放同步锁。

当ThreadA在等待的时候,CPU又会进入ThreadB线程,此时ThreadB线程输出"Waiting for lock X"一行文字,然后也调用lock.something()方法,但是此时的lock属于x对象,而x对象的同步锁此时还被ThreadA持有着,因为ThreadA并没有退出x的get方法,所有ThreadB只能等待ThreadA释放同步锁。

到这个时候就已经产生死锁现象了,因为此后,无论CPU切换到哪一个线程都无法继续执行代码。

二 如何避免死锁现象

避免死锁其实就一句话:当多个线程都要访问共享的资源A,B,C时,保证每一个线程都按照相同的顺序去访问他们,比如都先访问A,接着B,最后C。

根据上面这句话我们就可以修改上面锁死的例子。首先,我们不应该为LockX和LockY的get方法设置参数对象,其次在ThreadA和ThreadB线程中,应该该着相同的顺序访问资源对象x和y。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值