内卷老员工之死锁与死锁的预防

191 阅读3分钟

这是我参与8月更文挑战的第8天,活动详情查看:8月更文挑战

死锁

  • 死锁是指两个或多个线程被阻塞,等待获得死锁状态下一些其他线程锁持有的锁。当多线程需要获取相同的锁,但以不同顺序进行获取时,则可能会出现死锁。

死锁举例

  • 当a线程锁定对象lock1,同时尝试获得对象lock2;同时b线程锁定对象lock2,同时尝试获得对象lock1。这种情况下a线程永远会锁定lock1而无法获得lock2,b线程会永远获得lock2而无法获得lock1,a线程与b线程则会进入死锁状态。
Object lock1;
Object lock2;
new Thread(() -> {
    synchronized(lock1){
        synchronized(lock2){
            
        }
    }
},"thread a").start();
new Thread(() -> {
    synchronized(lock2){
        synchronized(lock1){
            
        }
    }
},"thread b").start();
  • 而死锁的前提条件时需要lock1和lock2两个对象同时被锁定,这样就会同时进入等待状态导致死锁。如果lock1和lock2的锁定时间有差异,可能由于执行顺序的问题导致不会死锁。

复杂状态下的死锁

  • 当死锁包含两个以上的线程时,会形成比较复杂的死锁。
线程 1 锁定 A,等待 B
线程 2 锁定 B,等待 C
线程 3 锁定 C,等待 D
线程 4 锁定 D,等待 A
  • 线程1等待线程2,线程2等待线程3,线程3等待线程4,线程4等待线程1。

数据库死锁

  • 由于数据库事务的特殊性,可能会产生更为复杂的死锁。
  • 在事务期间进行数据库记录更新时,该记录将会被数据库进行锁定,直至事务完成。因此同一事务中的每个更新请求都可能锁定数据库的若干行记录。
  • 如果多个事务同时运行,都需要更新数据库的相同一条记录,则可能产生复杂的数据库事务死锁。
事务 1,请求 1,锁定记录 1 以进行更新
事务 2,请求 1,锁定记录 2 以进行更新
事务 1,请求 2,尝试锁定记录 2 以进行更新。
事务 2,请求 2,尝试锁定记录 1 以进行更新。
  • 锁是在不同的请求中获取的,因此数据库的锁并非提前知道的,因此很难预防。

死锁预防-锁排序

  • 当不同的线程以不同的顺序获取相同锁时才会发生死锁,如果获取顺序相同,则不会发生死锁。
Thread 1:
  lock A 
  lock B

Thread 2:
   wait for A
   lock C (when A locked)

Thread 3:
   wait for A
   wait for B
   wait for C
  • 当一个线程持有多个锁时,必须要以固定的顺序进行获取。如果3线程先获取到锁c,就会导致死锁。对锁排序是避免死锁的一种简单而有效的方式,但是必须在获得任何锁之了解所需要的所有锁才能使用。

死锁预防-锁超时

  • 另一种死锁预防的方式是通过锁超时机制。当一个线程反复尝试获得锁但没有成功获取到锁时,它会备份并释放所有的锁,等待随机时间重试。
  • 锁超时不能采用synchronized关键字,需要采用自定义锁或 java.util.concurrency包中的构造。

死锁检测

  • 死锁检测是一种更复杂的死锁预防的机制。每次线程获取锁时在数据结构中注明。当请求锁但被拒绝时,可以遍历锁图检查死锁。
  • 如果检测到死锁,线程会做什么:
    • 备份、释放所有锁,等待随机时间
    • 确定线程的优先级

后记

  • 千古兴亡多少事?悠悠。不尽长江滚滚流。