美文网首页服务端开发实战java面试
死锁产生的原因和解锁的方法

死锁产生的原因和解锁的方法

作者: AKyS佐毅 | 来源:发表于2018-09-05 22:54 被阅读13次

    死锁产生的原因和解锁的方法

    • 1、产生死锁的四个必要条件:

      • 互斥条件:一个资源每次只能被一个进程使用。
      • 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
      • 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
      • 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

    举例说明

    /**
     * 一个简单的死锁类
     * 当DeadLock类的对象flag==1时(td1),先锁定o1,睡眠500毫秒
     * 而td1在睡眠的时候另一个flag==0的对象(td2)线程启动,先锁定o2,睡眠500毫秒
     * td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被td2锁定;
     * td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被td1锁定;
     * td1、td2相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
     */
    
    @Slf4j
    public class DeadLock implements Runnable {
        public int flag = 1;
        //静态对象是类的所有对象共享的
        private static Object o1 = new Object(), o2 = new Object();
    
        @Override
        public void run() {
            log.info("flag:{}", flag);
            if (flag == 1) {
                synchronized (o1) {
                    try {
                        Thread.sleep(500);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    synchronized (o2) {
                        log.info("1");
                    }
                }
            }
            if (flag == 0) {
                synchronized (o2) {
                    try {
                        Thread.sleep(500);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    synchronized (o1) {
                        log.info("0");
                    }
                }
            }
        }
    
        public static void main(String[] args) {
            DeadLock td1 = new DeadLock();
            DeadLock td2 = new DeadLock();
            td1.flag = 1;
            td2.flag = 0;
            //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
            //td2的run()可能在td1的run()之前运行
            new Thread(td1).start();
            new Thread(td2).start();
        }
    }
    
    • 2、如何避免死锁

      • 1、死锁检测和恢复(deadlock detection and recovery)

        死锁检测(deadlock detection)即探查和识别死锁的方法。这种策略并不采取任何动作来使死锁不出现,而是系统事件触发执行一个检测算法。,也即在系统运行过程中,及时地探查和识别死锁的存在,并识别出处于死锁之中的进程和资源等。死锁恢复(deadlock recovery)是指当检测并识别出系统中出现处于死锁之中的一组进程时,如何使系统回复到正常状态并继续执行下去。死锁恢复常采用下述两种方法。

      • 1、 撤消进程即当发现死锁时,就撤消(夭折)一些处于死锁状态的进程,并收回它的占用的资源,以解除死锁,使其它进程能继续运行,或者在提供检查点(checkpoint)信息情况下回退(rolled back)到一个较早的状态。这里有一个开销问题,即撤消哪个(些)进程比较“划算”。

      • 2、 挂起进程即当发现死锁时,挂起一些进程,抢占它们占用的资源,使得处于死锁之中的其它进程继续执行。待以后条件满足后,再恢复被挂起的进程。

    常利用资源分配图、进程等待图来协助这种检测。

    • 2、死锁预防(deadlock prevention)
     - 1、死锁预防(deadlock prevention)是在系统运行之前,事先考虑防止死锁发生的对策,即在最初设计各种资源调度算法时,就没法防止在系统运行过程中可能产生的死锁。Coffmman 等人[1971]曾提出进程在利用可重用性资源时产生死锁的四个必要条件:
    
        - 1、 **互斥使用(mutual exclusion)**:系统中存在一次只能给一个进程使用的资源。
    
        - 2、**占用并等待(resource holding and waiting)**:系统中存在这样的进程,它(们)已占有部分资源并等待得到另外的资源,而这些资源又被其它进程所占用还未释放。
    
        - 3、**非抢占分配(nonpreemption)**:资源在占有它的进程自愿交出之前,不可被其它进程所强行占用。
    
        - 4、**部分地分配(partial allocation)或循环等待(circular waiting)**:存在一个两个或都个进程的循环链,链中每一个进程等待被链中下一个进程占有的资源。即在一定条件下,若干进程进入了相互无休止地等待所需资源的状态。
    
    所有这四个必要条件都应该在死锁时出现。
    
     - 1、每个进程必须一次性地请求它所需要的所有资源。若系统无法满足这一要求,则它不能执行。这是一种预分资源方法,它破坏了产生死锁的第三个必要条件。
     - 2、一个已占有资源的进程若要再申请新资源,它必须先释放已占资源。若随后它还需要它们,则需要重新提出申请。换言之,一个进程在使用某资源过程中可以放弃该资源,从而破坏了产生死锁的第二个必要条件。
     - 3、将系统中所有资源顺序编号,规定进程只能依次申请资源,这就是说,一个进程只有在前面的申请满足后,才能提出对其后面序号的资源的请求。这是一种有序使用资源法,它破坏了产生死锁的第四个必要条件。
    
    • 3、死锁避免(deadlock avoidence)
       
         死锁避免(deadlock avoidence)是在系统运行过程中注意避免死锁的发生。这就要求每当申请一个资源时,系统都应根据一定的算法判断是否认可这次申请,使得在今后一段时间内系统不会出现死锁。

    2、对象锁和类锁是否会互相影响?

    • 1、对象锁:Java的所有对象都含有1个互斥锁,这个锁由JVM自动获取和释放。线程进入synchronized方法的时候获取该对象的锁,当然如果已经有线程获取了这个对象的锁,那么当前线程会等待;synchronized方法正常返回或者抛异常而终止,JVM会自动释放对象锁。这里也体现了用synchronized来加锁的1个好处,方法抛异常的时候,锁仍然可以由JVM来自动释放。

    • 2、类锁:对象锁是用来控制实例方法之间的同步,类锁是用来控制静态方法(或静态变量互斥体)之间的同步。其实类锁只是一个概念上的东西,并不是真实存在的,它只是用来帮助我们理解锁定实例方法和静态方法的区别的。我们都知道,java类可能会有很多个对象,但是只有1个Class对象,也就是说类的不同实例之间共享该类的Class对象。Class对象其实也仅仅是1个java对象,只不过有点特殊而已。由于每个java对象都有1个互斥锁,而类的静态方法是需要Class对象。所以所谓的类锁,不过是Class对象的锁而已。获取类的Class对象有好几种,最简单的就是MyClass.class的方式。

    • 3、类锁和对象锁不是同1个东西,一个是类的Class对象的锁,一个是类的实例的锁。也就是说:1个线程访问静态synchronized的时候,允许另一个线程访问对象的实例synchronized方法。反过来也是成立的,因为他们需要的锁是不同的。

    3、线程同步机制

    线程同步是为了确保线程安全,所谓线程安全指的是多个线程对同一资源进行访问时,有可能产生数据不一致问题,导致线程访问的资源并不是安全的。如果多线程程序运行结果和单线程运行的结果是一样的,且相关变量的值与预期值一样,则是线程安全的。

    Java中与线程同步有关的关键字/类包括:

    volatile、synchronized、Lock、AtomicInteger等concurrent包下的原子类。。。等

    • 1、volatile

    volatile一般用在多个线程访问同一个变量时,对该变量进行唯一性约束,volatile保证了变量的可见性,不能保证原子性。

    用法(例):

    private volatile booleanflag = false

    • 保证变量的可见性:volatile本质是告诉JVM当前变量在线程寄存器(工作内存)中的值是不确定的,需要从主存中读取,每个线程对该变量的修改是可见的,当有线程修改该变量时,会立即同步到主存中,其他线程读取的是修改后的最新值。

    • 不能保证原子性:原子性指的是不会被线程调度机制打断的操作,在java中,对基本数据类型的变量的读取和赋值操作是原子性操作。自增/自减操作不是原子性操作。例如:i++,其实是分成三步来操作的:1)从主存中读取i的值;2)执行+1操作;3)回写i的值。volatile关键字并不能保证原子性操作。非原子操作都会产生线程安全的问题,那么如何实现自增/自减的原子性呢?后续将有讲解。

    • 2、synchronized

      • synchronized提供了一种独占的加锁方式,是比较常用的线程同步的关键字,一般在“线程安全的单例”中普遍使用。该关键字能够保证代码块的同步性和方法层面的同步。

      • 代码块同步

      • 方法同步

      //使用synchronized关键字实现线程安全的单例模式   代码块同步
      private static Singleton instance;
      public static Singleton getInstance(){
          if(instance == null){
              synchronized (Singleton.class)
              {
                  if(instance == null){
                      instance = new Singleton();
                  }              
              }
          }
          return instance;
      }
      privateSingleton(){ }
      
      //方法同步
      public static synchronized Singleton getInstance2(){
          if(instance == null){
              instance = new Singleton();
          }
          return instance;
       }
      private Singleton(){ }
      
    • volatile和synchronized的区别

      • volatile通过变量的可见性,指定线程必须从主存中读取变量的最新值;synchronized通过阻塞线程的方式,只有当前线程能访问该变量,锁定了当前变量。

      • volatile使用在变量级别;synchronized可以使用在变量、方法、类级别

      • volatile不会造成线程阻塞;synchronized可能会造成线程阻塞

      • volatile不能保证原子性;synchronized能保证原子性

      • volatile标记的变量不会被编译器优化;synchronized标记的变量有可能会被编译器优化(指令重排)。

    • 3、如何保证自增/自减的原子性

    • 使用java.util.concurrent包下提供的原子类,如AtomicIntegerAtomicLongAtomicReference等。用法:

    AtomicInteger atomicInteger = new AtomicInteger();  
    atomicInteger.getAndIncrement();//实现原子自增  
    atomicInteger.getAndDecrement();//实现原子自减  
    
    • 2、使用synchronized同步代码块
    Synchronized(this){  
        value++;  
    }  
    
    • 3、使用Lock显示锁同步代码块
    private Lock lock = new ReentrantLock();  
     private final int incrementAndGet(){  
         lock.lock();  
         try  {  
             return value++;  
         }  finally  {  
             // TODO: handle finally clause  
             lock.unlock();  
         }  
     }  
    

    4、如何保证多线程读写文件的安全

    • 沉睡唤醒机制:

      • 需求:两个线程写一个TXT文件,线程1:I love you 线程2:so do I 。保证线程1、2有序执行,一句I love you,对应一句so do I。
      • 第一步,先创建WRFile类。这一步是关键的。
       public class WRFile {
             //String str;
             boolean flag;
             public WRFile(){
                 
             }
             
             public synchronized void read1(){
                 if(this.flag){
                     try {
                         this.wait();
                     } catch (InterruptedException e) {
                         
                         e.printStackTrace();
                     }
                 }
                 RandomAccessFile ra = null;
                 try {
                     ra = new RandomAccessFile("love.txt", "rw");
                     ra.seek(ra.length());
                 
                     ra.writeBytes("I love you");
                     ra.writeBytes("\r\n");
                 } catch (Exception e) {
                     e.printStackTrace();
                 }
                 finally {
                     try {
                         ra.close();
                     } catch (IOException e) {
                         
                         e.printStackTrace();
                     }
                 }
                 //修改标记 唤醒线程
                 this.flag = true;
                 this.notify();
             }
             
             public synchronized void read2(){
                 if(!this.flag){
                     try {
                         this.wait();
                     } catch (InterruptedException e) {
                         
                         e.printStackTrace();
                     }
                 }
                 RandomAccessFile ra = null;
                 try {
                     ra = new RandomAccessFile("love.txt", "rw");
                     ra.seek(ra.length());
                 
                     ra.writeBytes("so do I");
                     ra.writeBytes("\r\n");
                 } catch (Exception e) {
                     
                     e.printStackTrace();
                 } finally {
                     try {
                         ra.close();
                     } catch (IOException e) {
                         
                         e.printStackTrace();
                     }
                 }
                 //修改标记 唤醒线程
                 this.flag = false;
                 this.notify();
             }
    
    }
    
    • 第二步,创建我们的两个线程类,第一个FirstThread。
    public class FirstThread implements Runnable {
          private WRFile wr = new WRFile();
          
             public FirstThread(WRFile wr) {
                  this.wr = wr;
              } 
              
              @Override
              public void run() {
                  
                  while(true){
                      wr.read1();
                  }
             }
      }
    
    public class SecondThrad implements Runnable{
          private WRFile wr = new WRFile();
          public SecondThrad(WRFile wr) {
              this.wr = wr;
          }
          @Override
          public void run() {
              while(true)
              {
                  wr.read2();
              }
          }
    }
    
    • 第三步:
    public static void main(String[] args) {
          //创建数据对象
          WRFile wr = new WRFile();
          //设置和获取类
          FirstThread ft = new FirstThread(wr);
          SecondThrad st = new SecondThrad(wr);
          //线程类
          Thread th1 = new Thread(ft);
          Thread th2 = new Thread(st);
          //启动线程
          th1.start();
          th2.start();
      }
    

    145天以来,Java架构更新了 428个主题,已经有91位同学加入。微信扫码关注java架构,获取Java面试题和架构师相关题目和视频。上述相关面试题答案,尽在Java架构中。

    相关文章

      网友评论

        本文标题:死锁产生的原因和解锁的方法

        本文链接:https://www.haomeiwen.com/subject/pcazwftx.html