多线程预备知识(二)----线程状态,调度算法及死锁

线程/进程的基本状态

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

  • 执行状态:表示该线程获得了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的锁。代码执行完毕。可以发现,只要资源的申请顺序合理就可以打破请求持有条件和环路等待条件,就不会造成死锁

posted @ 2020-05-08 14:55  穿黑风衣的牛奶  阅读(348)  评论(0编辑  收藏  举报