zoukankan      html  css  js  c++  java
  • 多线程预备知识(二)----线程状态,调度算法及死锁

    线程/进程的基本状态

    和传统的进程一样,线程也拥有三种基本状态,分别是

    • 执行状态:表示该线程获得了CPU的执行权正在运行
    • 就绪状态:表示该线程已经具备了执行所需要的预备条件,等待CPU调度就可以立即执行,在Java中体现为调用了start()方法,或者线程休眠时间完毕等等。但是此时还没有开始执行。需要等待cpu的调度
    • 阻塞状态:表示该线程在执行过程中因为受阻,例如等待另一个资源而处于暂停状态。在Java中可以体现为调用了wait()方法等,其中阻塞状态又分为三种
      • 同步阻塞:在获取一个对象的时候,发现这个对象被其他的线程上了锁,就会进入同步阻塞状态
      • 等待阻塞:例如调用了wait()方法时,会把该线程放进等待队列
      • 其他阻塞:调用了sleep(),或者线程发出了IO请求,就会进入到阻塞状态,当IO完毕或者休眠时间完毕则会到就绪状态

    在原有的基础上,满足完整性,也会引入最常见的的两种状态:新建和终止状态

    • 新建状态:可以理解成没有满足线程创建所需的资源所处的一个状态,在Java中可以体现为创建了一个线程,但是还没有调用start()方法
    • 终止状态:当线程运行完毕,或者抛出异常导致了线程的终止,结束了线程的生命周期

    上图就说明了线程之间转化的五种状态

    进程的调度算法

    CPU的调度算法有非常多,由于是多线程系列的文章,就讲一下跟线程最相关的几种思想,一是抢占式调度,二是时间片轮转调度。现在很多的操作系统都是采用这两者结合的方式去进行调度的。

    抢占式调度:

    这种调度方式允许程序依据某种原则,去暂停某个正在执行的进程,将已经分配给该进程的处理机重新分配给另一个进程,现在的操作系统都普遍才用了这个方式,但是这个抢占也不是任意的,随随便便乱抢,也需要遵循一定的规则

    • 优先权原则:对于优先级高的线程,允许优先级高的线程抢占优先级低线程的cpu执行权
    • 短作业优先:执行时间短的线程允许抢占执行时间长线程的执行权
    • 时间片原则:各进程按时间片轮流运行,当一个时间片用完后,便停止该进程的执行而重新进行调度

    时间片轮转

    每个线程都被分配相等的时间片,轮流在自己的时间片时间内使用cpu,当cpu用完之后,就丢失cpu的执行权,然后将执行权给拥有时间片的线程去调度,本身回到就绪队列等待下一次的调度。

    死锁

    死锁,可以理解成死局,顾名思义就是没有外力因素的情况下解不开的锁,在操作系统层面,死锁就是,由于资源竞争或者相互等待的情况下造成的一种阻塞现象。比如说这种情况进程A锁住了资源1想要资源2,而进程B锁住了进程2想要资源1,在这种情况下,他们俩都获取不到彼此想要的资源,然后就会一直处于等待状态,这个时候就造成了死锁

    死锁产生的原因

    对于有一定操作系统知识的朋友来说,应该知道,死锁产生的必须具备四个条件:

    • 互斥条件:简单的理解就是,某个资源只能被一个线程使用,当该资源被使用了,就会拒绝其他线程的使用请求,其他线程就会进入阻塞状态

    • 请求并持有条件:意思就是,A线程有一个或多个资源的时候,当他去请求另一个资源,但是那个资源被B线程占有了,此时A资源则会进入阻塞状态,但是即便进入阻塞状态也不会释放自己有的资源。可以理解成,自私条件,我得不到的我想要,并且不会放弃手上有的。

    • 不可剥夺条件:意思就是线程拥有的资源,在没用完之前,不会释放

    • 环路等待条件:就是死锁的一个等待链的意思。例如T1线程等待T2的资源,T2等待T3的......以此类推

      下面举一个死锁的例子

      /**
       * 产生死锁的情况
       */
      public class DeadLockTest2 {
          private  static  Object resourceA = new Object();
          private  static  Object resourceB = new Object();
      
          public static void main(String[] args) {
              Thread thread1 = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      synchronized (resourceA) {
                          System.out.println(Thread.currentThread() + "获取资源A");
                          try {
                              //休眠一秒 保证线程2可以获取资源B的锁
                              Thread.sleep(1000);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          System.out.println("等待获取资源B");
                          synchronized (resourceB) {
                              System.out.println(Thread.currentThread() + "获取资源B");
                          }
                      }
                  }
              });
      
             Thread thread2 =  new Thread(new Runnable() {
                  @Override
                  public void run() {
                      synchronized (resourceB) {
                          System.out.println(Thread.currentThread() + "获取资源B");
                          try {
                              //休眠一秒,保证线程1去获取资源B 虽然会被阻塞
                              Thread.sleep(1000);
                          } catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                          System.out.println("等待获取资源A");
                          synchronized (resourceA) {
                              System.out.println(Thread.currentThread() + "获取资源A");
                          }
                      }
                  }
              });
             thread1.start();
             thread2.start();
          }
      }
      
      

      分析一下上面的代码,首先线程1拿到资源A的锁,然后线程1休眠一秒,保证线程2可以获取资源B的锁,之后线程2休眠1秒,此时线程1执行获取资源B的代码,但是资源B被线程2锁住了,因此线程1阻塞进入等待状态,这时线程2休眠结束,想要获取资源A,但是此时资源A被线程1锁住,因此也进入了阻塞等待状态。现在两个线程的状态就是这样:线程1锁住资源A想要B,线程2锁住线程B想要A,就陷入了相互等待的状态,造成了死锁。这段代码是如何满足线程死锁的四个条件呢?

      • 首先在这段代码里面,只有当线程1释放了资源A或者线程2释放了资源B才能被其他线程使用,否则就会阻塞,就满足了资源互斥条件
      • 请求并保持条件:线程1在执行完成之前,或者说获取到B资源之前,都不会释放资源A的锁。对于线程2来说也是同理,因此满足了请求并保持条件
      • 不可剥夺条件:线程1在获取了资源A的锁之后,在线程A主动释放,或者说获取到B资源之前,A资源都不会被线程2剥夺,这就满足了不可剥夺条件
      • 循环等待条件:线程1锁住资源A想要B,线程2锁住线程B想要A,就陷入了相互等待的状态,形成了一个环路,满足了循环等待的条件

      由于满足了以上四个条件,所以线程就有可能会进入到死锁状态,可能会造成死锁的状态在操作系统中被称为,不安全状态

    如何避免死锁

    想要避免死锁,只需要破坏其中一个条件必要条件就可以了,互斥条件由于资源的限制性,有一些资源是被限制了只能被一个线程使用,不然会乱套,例如打印机在打印的时候,只能被一台电脑使用么,不然就会出现问题。不可剥夺条件也是同理,也可以用打印机来解释,打印的时候不能给其他电脑,不然会出问题。因此唯一能破坏的只有请求并持有条件和环路等待条件。

    最常用的避免死锁的方法就是保证资源申请的有序性。只需要把上面的死锁代码稍微改一下就ok,把任意一个线程的申请资源的顺序跟另一个线程一致。例如,此时我把线程2的资源改成了和线程1的顺序相同

    /**
     * 保证两个线程获取资源的顺序一致就可以避免死锁 因为此时任意一个线程获取资源A的时候,另一个线程都会阻塞不会去获取资源B
     */
    public class DeadLockTest3 {
        private  static  Object resourceA = new Object();
        private  static  Object resourceB = new Object();
    
        public static void main(String[] args) {
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (resourceA) {
                        System.out.println(Thread.currentThread() + "获取资源A");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread()+"等待获取资源B");
                        synchronized (resourceB) {
                            System.out.println(Thread.currentThread() + "获取资源B");
                        }
                    }
                }
            });
            //修改了这个线程的获取顺序,和线程1一致
            Thread thread2 =  new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (resourceA) {
                        System.out.println(Thread.currentThread() + "获取资源A");
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println( Thread.currentThread()+"等待获取资源B");
                        synchronized (resourceB) {
                            System.out.println(Thread.currentThread() + "获取资源B");
                        }
                    }
                }
            });
            thread1.start();
            thread2.start();
        }
    }
    
    

    再回过头来分析新的安全代码,一开始线程1获取资源A,然后休眠1秒,到线程2执行,线程2首先想要获取资源A但是发现被锁住了,因此线程2阻塞,然后线程1休眠完毕,执行获取资源B的代码,获取到资源B,线程1执行完毕,释放资源A和B的锁。然后线程2获取资源A,再获取资源B,执行完毕,释放A和B的锁。代码执行完毕。可以发现,只要资源的申请顺序合理就可以打破请求持有条件和环路等待条件,就不会造成死锁

  • 相关阅读:
    adb命令使用总结
    python os.system()和os.popen()
    Source Insight 中文注释为乱码解决办法(完美解决,一键搞定)
    Source Insight 常用设置
    Source Insight 有用设置配置
    Source Insight 常用设置和快捷键大全
    Source Insight 4.0常用设置
    远程桌面中Tab键不能补全的解决办法
    python中if __name__ == '__main__': 的解析
    python os用法笔记
  • 原文地址:https://www.cnblogs.com/blackmlik/p/12850514.html
Copyright © 2011-2022 走看看