线程安全问题(二)——死锁

07-01 1954阅读

死锁

  • 前言
  • 可重入锁
    • 逻辑
    • 两个线程两把锁(死锁)
    • 死锁的特点
    • 多个线程多把锁(哲学家就餐问题)
    • 总结

      前言

      在前面的文章中,介绍了锁的基本使用方式——锁

      在上一篇文章中,通过synchronized关键字进行加锁操作,使得【多线程修改同一变量】的情况可以得到解决。

      那么在本文中,将会继续讲解锁的相关知识点。


      可重入锁

      我们可以通过synchronized确定锁对象,对线程进行加锁的操作。那么如果锁对象重复使用是否会出现不一样的结果?

      在下面的案例中,t1和t2线程使用了连续synchronized,设置的加锁对象同样是counter,如果运行这段代码,结果却是正确的。

      理论上,当synchronized(counter)开始使用时,只有执行完其中的代码(大括号中的代码块)才会释放锁。而这个锁中又嵌套了相同的锁,按道理来说此时counter锁对象还没有被释放,应该出现阻塞等待状态最终代码无法运行才是。

      那么为什么在Java中这段代码可以编译通过?

      原因: 在Java中,已经对synchronized内部进行了特殊处理。在每个锁对象中,会记录当前是哪个线程持有这把锁,接下来,当针对这个对象进行加锁操作的时候就会进行判定,判定当前尝试加锁线程是否是这一对象锁的线程。 通过这样的操作,系统就可以知道如果不是,就会阻塞;如果是,就会直接放行。

      class Counter {
          public static int count;
           void add(){
              count++;
          }
          public int getCount(){
              return count;
          }
      }
      public static void main(String[] args) throws InterruptedException {
              Counter counter = new Counter();
              Thread t1 = new Thread(()-> {
                  for (int i = 0; i  {
                  for (int i = 0; i  
      

      逻辑

      当加了多层锁的时候,代码如何知道执行到哪里要真正进行解锁。如果有若干层加锁操作,如何判定当前遇到的}是最外层的} ?

      以我的理解,进行加锁操作时在内部给锁对象设计了一个计数器(int n)。 每次遇到【{ 】时,n++,遇到【 } 】时,n–,当n=0时才真正解锁。

      通过这样的方式,针对同一线程中的同一锁对象进行的锁操作,可以让程序猿避免了死锁的情况,我们也称之为可重入锁。

      两个线程两把锁(死锁)

      现在存在线程t1和线程t2;锁对象A和B。

      当进行锁操作的时候,可能会出现这样的情况:线程1和线程2都需要使用到锁A和锁B,对于线程A来说,首先获取锁A然后获取锁B;而对于B来说,首先获取锁B再获取锁A。

      让两个线程同时获得第一把锁,接下来需要尝试去获取对方的锁。

      在下面的代码启动后,线程t1获取到了锁locker1,线程t2获取到了锁locker2.
      对于t1,接下来需要获取locker2才能继续执行接下来的代码
      对于t2,需要获取locker1才能继续执行接下来的代码
      

      两个线程之间无法让步,于是一同进入了阻塞等待状态,都在等待对方释放锁。

      public static void main(String[] args) {
              Object locker1 = new Object();
              Object locker2 = new Object();
              Thread t1 = new Thread(()-> {
                  synchronized (locker1){
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                              throw new RuntimeException(e);
                      }
                      synchronized (locker2){
                          System.out.println("获取到了2把锁");
                      }
                  }
              });
              Thread t2 = new Thread(()-> {
                  synchronized (locker2){
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                      synchronized (locker1){
                          System.out.println("获取到了2把锁");
                      }
                  }
              });
              t1.start();
              t2.start();
          }
      

      在执行这段代码后,我们可以通过jconsole查看线程状态。如下图所示,我们可以知道两个线程都处于阻塞状态,同时我们也可以知道阻塞所需要获取的锁目前在哪个线程身上。

      线程安全问题(二)——死锁

      线程安全问题(二)——死锁

      死锁的特点

      1.锁具有互斥性。这是锁的基本特点,当一个线程拿到锁A时,其他线程只能等待该线程释放。

      2.锁不可抢占。只有该线程主动释放锁,别的线程无法抢占。

      3.请求和保持。线程拿到锁以后可以继续尝试获取其他锁。

      4.循环等待。多个线程多个锁的状态中,出现了A等待B,B等待A的情况。

      当全部满足这些条件以后,就可以发生死锁。

      多个线程多把锁(哲学家就餐问题)

      情景:在一个餐桌上存在五个哲学家,他们做两件事情:一是思考哲学,二是就餐。

      每个哲学家左右手各有一根筷子以供就餐时使用。如果哲学家手中只有一根筷子,他会等到另一根筷子拥有的时候才会就餐,而不会放下筷子。

      线程安全问题(二)——死锁

      通过这个情景,我们可以很明显的预知到一种情况:所有的哲学家手中都拿起左边的筷子,于是所有哲学家都停下了,进入阻塞等待状态。

      我们通过这个问题可以反映到线程的情况,因为多线程多锁的原因,如果没有合理安排则会导致线程阻塞甚至死锁!

      解决这种情况的一种办法就是约定好加锁的顺序,破除循环等待的情况。

      在下面的代码中,两个线程轮流获取locker1和locker2,这就可以有效规避死锁的问题了。

          public static void main(String[] args) {
              Object locker1 = new Object();
              Object locker2 = new Object();
              Thread t1 = new Thread(()-> {
                  synchronized (locker1){
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                      synchronized (locker2){
                          System.out.println("获取到了2把锁");
                      }
                  }
              });
              Thread t2 = new Thread(()-> {
                  synchronized (locker1){
                      try {
                          Thread.sleep(1000);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                      synchronized (locker2){
                          System.out.println("获取到了2把锁");
                      }
                  }
              });
              t1.start();
              t2.start();
          }
      

      总结

      死锁在多线程中是及其常见的一个问题,导致了线程的不安全。是我们要极力规避的情况。我们了解了死锁发生的情况,死锁的原因等多个点。

      本文使用源码☞ 源码

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]