一、什么是死锁
在并发编程中,死锁指的是两个或更多线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力干涉那它们都将无法继续下去,称之为死锁。
死锁的问题通常会出现在多个线程互斥访问共享资源的程序中。互斥就好比是门,每个线程都要有“钥匙”才可以进入,而这个“钥匙”就是资源的锁。当一个线程持有第一个锁并想要请求第二个锁但是无法获取时,而此时另外一个线程持有第二个锁,但是想要请求第一个锁,双方都不会释放资源,这种现象就是死锁。
下面是使用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. 少用全局锁
使用全局锁来对整个系统进行同步,这不仅会降低程序的效率,还会增加出错的可能性。应该尽量使用锁的局部变量。锁的局部变量就是说,锁定的范围必须是局部变量,而且这个变量不能被其他调用方法引用,否则代码很可能会造成死锁。
三、总结
死锁问题在并发编程中是很普遍的,高效的避免死锁的方法能够提高程序的性能和可靠性。通过加锁的顺序、加锁的超时时间、少用全局锁等方法,我们可以在编写多线程应用时有效地避免死锁的出现。熟练掌握以上知识,对于提高多线程编程开发效率和系统性能是非常有必要的。