您的位置:

Java死锁详解

一、什么是死锁

在并发编程中,死锁指的是两个或更多线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法继续下去,称之为死锁。

死锁的问题通常会出现在多个线程互斥访问共享资源的程序中。互斥就好比是门,每个线程都要有“钥匙”才可以进入,而这个“钥匙”就是资源的锁。当一个线程持有第一个锁并想要请求第二个锁但是无法获取时,而此时另外一个线程持有第二个锁,但是想要请求第一个锁,双方都不会释放资源,这种现象就是死锁。

下面是使用Java代码实现死锁的示例:

public class DeadLockDemo {
    private static Object lockA = new Object();
    private static Object lockB = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "获取到了lockA");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "获取到了lockB");
                }
            }
        }, "Thread1").start();
        new Thread(() -> {
            synchronized (lockB) {
                System.out.println(Thread.currentThread().getName() + "获取到了lockB");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockA) {
                    System.out.println(Thread.currentThread().getName() + "获取到了lockA");
                }
            }
        }, "Thread2").start();
    }
}

在以上代码示例中,两个线程分别通过不同顺序获取到lockA和lockB锁,进入后会在等待另一个锁的释放,但是两个线程都没有释放自己持有的锁,造成了死锁。

二、避免死锁的方法

死锁在编写代码时是很容易发生的,并解除死锁有多种方式。下面介绍一些常见的避免死锁的方法:

1. 加锁的顺序

加锁顺序是死锁的主要原因之一。如果多个线程都按照相同的顺序获取锁,则不会出现死锁。相反,如果多个线程以不同的顺序获取锁,死锁将很容易发生。

下面是使用Java代码实现按照锁的顺序来避免死锁的示例:

public class DeadLockDemo {
    private static Object lockA = new Object();
    private static Object lockB = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "获取到了lockA");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "获取到了lockB");
                }
            }
        }, "Thread1").start();
        new Thread(() -> {
            synchronized (lockA) {
                System.out.println(Thread.currentThread().getName() + "获取到了lockA");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB) {
                    System.out.println(Thread.currentThread().getName() + "获取到了lockB");
                }
            }
        }, "Thread2").start();
    }
}

以上代码改变了线程获取锁的顺序,两个线程都是先获取lockA,再获取lockB。这个时候就能够避免死锁。

2. 加锁的超时时间

超时时间可以避免死锁,如果一个线程无法在超时时间内获取到锁,它就会放弃尝试并释放锁。这种方式并不是消除死锁的根本方式,只是赋给了一个死锁的"时间限制",如果一个线程获取锁的时间过长,会被认为是死锁并进行异常处理。

下面是使用Java代码实现加锁超时时间来避免死锁的示例:

public class DeadLockDemo {
    private static Object lockA = new Object();
    private static Object lockB = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            boolean flag = false;
            try {
                if (lockA instanceof Lock) {
                    flag = ((Lock) lockA).tryLock(1000, TimeUnit.MILLISECONDS);
                } else {
                    synchronized(lockA) {
                        flag = true;
                    }
                }
                if (flag) {
                    System.out.println(Thread.currentThread().getName() + "获取到了lockA");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (lockB instanceof Lock) {
                        ((Lock) lockB).lock();
                    } else {
                        synchronized(lockB) {}
                    }
                    System.out.println(Thread.currentThread().getName() + "获取到了lockB");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (flag && lockA instanceof Lock) {
                    ((Lock) lockA).unlock();
                }
                if (lockB instanceof Lock) {
                    ((Lock) lockB).unlock();
                }
            }
        }, "Thread1").start();
        new Thread(() -> {
            boolean flag = false;
            try {
                if (lockB instanceof Lock) {
                    flag = ((Lock) lockB).tryLock(1000, TimeUnit.MILLISECONDS);
                } else {
                    synchronized(lockB) {
                        flag = true;
                    }
                }
                if (flag) {
                    System.out.println(Thread.currentThread().getName() + "获取到了lockB");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    if (lockA instanceof Lock) {
                        ((Lock) lockA).lock();
                    } else {
                        synchronized(lockA) {}
                    }
                    System.out.println(Thread.currentThread().getName() + "获取到了lockA");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (lockA instanceof Lock) {
                    ((Lock) lockA).unlock();
                }
                if (flag && lockB instanceof Lock) {
                    ((Lock) lockB).unlock();
                }
            }
        }, "Thread2").start();
    }
}

以上代码使用了tryLock和lock实现了加锁超时时间来避免死锁。当一个线程无法获取锁的时候,它就会放弃尝试并释放锁。在以上代码中,参数1000表示等待锁的时间限制,如果在这个时间内无法获取到锁,该线程就会跳过当前操作。最后,我们通过unlock方法进行锁的释放。

3. 少用全局锁

使用全局锁来对整个系统进行同步,这不仅会降低程序的效率,还会增加出错的可能性。应该尽量使用锁的局部变量。锁的局部变量就是说,锁定的范围必须是局部变量,而且这个变量不能被其他调用方法引用,否则代码很可能会造成死锁。

三、总结

死锁问题在并发编程中是很普遍的,高效的避免死锁的方法能够提高程序的性能和可靠性。通过加锁的顺序、加锁的超时时间、少用全局锁等方法,我们可以在编写多线程应用时有效地避免死锁的出现。熟练掌握以上知识,对于提高多线程编程开发效率和系统性能是非常有必要的。