zoukankan      html  css  js  c++  java
  • 多线程(三)

    前言

    本篇文章是多线程系列的第三篇(第二篇可参考多线程(二)),主要讲解:死锁、等待-唤醒机制、Lock和Condition。文章讲解的思路是:先通过一个例子来演示死锁的现象,再通过分析引出一系列的解决方案。同样,重点部分我都会用红色字体标识。

    正文

    死锁现象?

    前一篇文章讲过:通过"synchronized"实现的同步是带有锁的。我们不免联想到生活中的一个场景:出门忘带钥匙被锁在了门外。其实这种情况在多线程程序中也可能会出现,并且它还有一个专业名称叫"死锁"。比如,下面这个程序就说明了可能会发生死锁的一个场景:

    
    public class Test {
    
        // 创建资源
        private static Object resourceA = new Object();
        private static Object resourceB = new Object();
    
        public static void main(String[] args) {
    
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (resourceA) {
                        System.out.println(Thread.currentThread() + "get ResourceA");
    
                        try {
                            Thread.sleep(1000);   // 休眠1s的目的是让线程B抢占到CPU资源从而获取到resourceB上的锁
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        System.out.println(Thread.currentThread() + "waiting get ResourceB");
                        synchronized (resourceB) {
                            System.out.println(Thread.currentThread() + "get ResourceB");
                        }
                    }
                }
            });
    
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (resourceB) {
                        System.out.println(Thread.currentThread() + " get ResourceB");
    
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        System.out.println(Thread.currentThread() + "waiting get ResourceA");
                        synchronized (resourceA) {
                            System.out.println(Thread.currentThread() + "get ResourceA");
                        }
                    }
                }
            });
    
            threadA.start();
            threadB.start();
        }
    }
    
    

    上面的代码就是可能会发生"死锁"的一个场景:同步的嵌套。线程A获取到了resourceA的监视器锁,然后调用sleep方法休眠了1s,在线程A休眠期间,线程B获取到了resourceB的监视器锁,也休眠了1s,当线程A休眠结束后会企图获取resourceB的的监视器锁,然而由于该资源被线程B所持有,所以线程A就会被阻塞并等待,而同理当线程B休眠结束后也会被阻塞并等待,最终线程A和线程B就陷入了相互等待的状态,也就产生了"死锁"。于是我们就可以用专业术语来总结什么是死锁:死锁就是指多个线程在执行的过程中,因争夺资源而造成的互相等待现象,并且在无外力作用的情况下,这些线程会一直相互等待而无法继续运行下去。

    我们可以通过使用资源申请的有序性原则去避免死锁。那么什么是资源申请的有序性原则呢?它是指假如线程A和线程B都需要资源1,2,3,...,n时,对资源进行排序,线程A和线程B只有在获取了资源n-1时才能去获取资源n。就像下面这样:

    
    public class Test {
    
        private static Object resourceA = new Object();
        private static Object resourceB = new Object();
    
        public static void main(String[] args) {
    
            Thread threadA = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (resourceA) {
                        System.out.println(Thread.currentThread() + "get ResourceA");
    
                        try {
                            Thread.sleep(1000);   
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        System.out.println(Thread.currentThread() + "waiting get ResourceB");
                        synchronized (resourceB) {
                            System.out.println(Thread.currentThread() + "get ResourceB");
                        }
                    }
                }
            });
    
            Thread threadB = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (resourceA) {   // 先获取resourceA的监视器锁
                        System.out.println(Thread.currentThread() + " get ResourceA");
    
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        System.out.println(Thread.currentThread() + "waiting get ResourceA");
                        synchronized (resourceB) {
                            System.out.println(Thread.currentThread() + "get ResourceB");
                        }
                    }
                }
            });
    
            threadA.start();
            threadB.start();
        }
    }
    
    

    等待唤醒机制?

    线程间通信?

    我们在第二篇文章中讲过的"卖票功能"其实是多个线程处理同一资源(即num),任务也相同(都是卖票),那么线程间通信就是指:多个线程处理同一资源,但是任务却不同。就像下面这样:

    那么在这种情况下又会出现怎样的问题呢?现在考虑这样一个场景:有两个任务,一个输入负责为资源赋值,另外一个输出负责读取资源的值并打印。代码如下:

    
    // 资源
    class Resource
    {
        String name;
        String sex;
    }
    
    // 输入
    class Input implements Runnable
    {
        Resource r ;
        Input(Resource r)
        {
            this.r = r;
        }
    
        public void run()
        {
            int x = 0;
            while(true)
            {
                synchronized(r) {
                    if (x == 0) {
                        r.name = "mike";
                        r.sex = "nan";
                    } else {
                        r.name = "丽丽";
                        r.sex = "女女女女女女";
                    }
                    x = (x + 1) % 2;
                }
            }
        }
    }
    
    // 输出
    class Output implements Runnable
    {
        Resource r;
        Output(Resource r)
        {
            this.r = r;
        }
    
        public void run()
        {
            while(true)
            {
                synchronized(r) {
                    System.out.println(r.name + "... ..." + r.sex);
                }
            }
        }
    }
    
    class  ResourceDemo
    {
        public static void main(String[] args)
        {
            Resource r = new Resource();
    
            Input in = new Input(r);
            Output out = new Output(r);
            Thread t1 = new Thread(in);
            Thread t2 = new Thread(out);
    
            t1.start();
            t2.start();
        }
    }
    
    

    由于使用了同步代码块,上面的代码就没有线程安全问题了。但是输出结果似乎有点不尽如人意:mike和丽丽都是成片输出,而我们希望的是输入一个就输出一个。在这种需求下,我们就需要使用到另一种技术:等待-唤醒机制,它其实就是wait()-notify()。我们的解决思路就是:输入线程为资源赋完值之后就去唤醒另一个输出线程去打印,并且输入线程在唤醒输出线程之后就进入等待状态。同理输出线程打印完之后就去唤醒另一个输入线程去赋值,并且输出线程在唤醒输入线程之后就进入等待状态。就像下面这样:

    
    // 资源
    class Resource
    {
        private String name;
        private String sex;
        private boolean flag = false;   // 标记,false代表资源现在没有值
    
        public synchronized void set(String name, String sex)
        {
            if(flag)   // 如果现在资源有值
                try{this.wait();}catch(InterruptedException e){}
            this.name = name;
            this.sex = sex;
            flag = true;
            this.notify();
        }
    
        public synchronized void out()
        {
            if(!flag)   // 如果资源现在没有值
                try{this.wait();}catch(InterruptedException e){}
            System.out.println(name +"... ..." + sex);
            flag = false;
            notify();
        }
    }
    
    // 输入
    class Input implements Runnable
    {
        Resource r ;
        Input(Resource r)
        {
            this.r = r;
        }
    
        public void run()
        {
            int x = 0;
            while(true)
            {
                if(x == 0)
                {
                    r.set("mike", "nan");
                }
                else
                {
                    r.set("丽丽", "女女女女女女");
                }
                x = (x+1)%2;
            }
        }
    }
    
    // 输出
    class Output implements Runnable
    {
    
        Resource r;
        Output(Resource r)
        {
            this.r = r;
        }
    
        public void run()
        {
            while(true)
            {
                r.out();
            }
        }
    }
    
    class  ResourceDemo
    {
        public static void main(String[] args)
        {
            Resource r = new Resource();
    
            Input in = new Input(r);
            Output out = new Output(r);
            Thread t1 = new Thread(in);
            Thread t2 = new Thread(out);
    
            t1.start();
            t2.start();
        }
    }
    
    

    通过上面的代码,我们可以总结出wait()和sleep()的区别

    • wait()可以指定时间也可以不指定;sleep()必须指定时间。

    • 在同步中时,对CPU的执行权和锁的处理不同:wait()会释放执行权也会释放锁;sleep会释放执行权但不会不释放锁。

    多生产者-多消费者问题?

    "多生产者-多消费者问题"是学习"等待-唤醒机制"最经典的案例。顾名思义,这个案例其实就是:有多个生产者在生产资源,同时有多个消费者在消费资源。考虑如下情景:现在有多个人在生产烤鸭,同时有多个人在消费烤鸭。代码如下:

    
    // 资源
    class Resource
    {
        private String name;
        private int count = 1;
        private boolean flag = false;
    
        public synchronized void set(String name)
        {
            while(flag)
                try{this.wait();}catch(InterruptedException e){}
            this.name = name + count;
            count++;
            System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
            flag = true;
            notify();
        }
    
        public synchronized void out()
        {
            while(!flag)
                try{this.wait();}catch(InterruptedException e){}
            System.out.println(Thread.currentThread().getName() + "...消费者........." + this.name);
            flag = false;
            notify();
        }
    }
    
    // 生产者
    class Producer implements Runnable
    {
        private Resource r;
        Producer(Resource r)
        {
            this.r = r;
        }
    
        public void run()
        {
            while(true)
            {
                r.set("烤鸭");
            }
        }
    }
    
    // 消费者
    class Consumer implements Runnable
    {
        private Resource r;
        Consumer(Resource r)
        {
            this.r = r;
        }
        public void run()
        {
            while(true)
            {
                r.out();
            }
        }
    }
    
    class  ProducerConsumerDemo
    {
        public static void main(String[] args)
        {
            Resource r = new Resource();
            Producer pro = new Producer(r);
            Consumer con = new Consumer(r);
    
            Thread t0 = new Thread(pro);
            Thread t1 = new Thread(pro);
            Thread t2 = new Thread(con);
            Thread t3 = new Thread(con);
            t0.start();
            t1.start();
            t2.start();
            t3.start();
        }
    }
    
    

    我们通过执行上面的代码发现:出现了"死锁"。这其实是由于:生产者(或消费者)线程调用notify()不仅可以唤醒消费者(或生产者)线程,也可以唤醒生产者(或消费者)线程。从而导致所有线程都进入了休眠状态,也就出现了"死锁"。那我们如何解决这个问题呢?

    notifyAll解决?

    我们知道之所以出现上面"死锁"的情况是由于notify()唤醒了本方线程(即是生产者唤醒了生产者,消费者唤醒了消费者),这就导致对方线程由于没有线程notify它们而一直等待下去。于是我们可以通过notifyAll()唤醒所有线程,这样对方线程就能够被唤醒从而解决了死锁的问题。

    
    // 资源
    class Resource
    {
        private String name;
        private int count = 1;
        private boolean flag = false;
    
        public synchronized void set(String name)
        {
            while(flag)
                try{this.wait();}catch(InterruptedException e){}
            this.name = name + count;
            count++;
            System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
            flag = true;
            notifyAll();   // 唤醒所有线程
        }
    
        public synchronized void out()
        {
            while(!flag)
                try{this.wait();}catch(InterruptedException e){}
            System.out.println(Thread.currentThread().getName() + "...消费者........." + this.name);
            flag = false;
            notifyAll();   // 唤醒所有线程
        }
    }
    
    

    Lock解决?

    通过notify()确实能够解决"多生产者-多消费者问题"的死锁情况,但是我们只是想唤醒对方线程,唤醒本方线程是没有意义的并且会多消耗资源。于是我们可以通过另一种方法来解决这个问题:Lock接口。

    
    import java.util.concurrent.locks.*;
    
    class Resource
    {
        private String name;
        private int count = 1;
        private boolean flag = false;
    
        //	创建一个锁对象。
        Lock lock = new ReentrantLock();    
    
        // 通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。
        Condition producer_con = lock.newCondition();
        Condition consumer_con = lock.newCondition();
    
        public  void set(String name)
        {
            lock.lock();
            try
            {
                while(flag)
                    try{producer_con.await();}catch(InterruptedException e){}    // 生产者等待
    
                this.name = name + count;
                count++;
                System.out.println(Thread.currentThread().getName() + "...生产者..." + this.name);
                flag = true;
                consumer_con.signal();   // 唤醒消费者
            }
            finally
            {
                lock.unlock();
            }
    
        }
    
        public  void out()
        {
            lock.lock();
            try
            {
                while(!flag)
                    try{consumer_con.await();}catch(InterruptedException e){}   // 消费者等待
                System.out.println(Thread.currentThread().getName() + "...消费者........." + this.name);
                flag = false;
                producer_con.signal();    // 唤醒生产者
            }
            finally
            {
                lock.unlock();
            }
    
        }
    }
    
    

    我们需要注意:在jdk1.5之前,同步的解决方案synchronized对锁的操作是隐式的;而在jdk1.5之后提供的Lock接口将锁和对锁的操作封装到对象中,将隐式变成了显式。同时它更为灵活,因为我们可以在一个锁上加上多组监视器(即Condition)。Condition 将 Object 监视器方法(wait、notify 和 notifyAll)分解成截然不同的对象,以便通过将这些对象与任意 Lock 实现组合使用。

  • 相关阅读:
    Object.keys方法之详解
    ackbone入门系列(5)路由
    backbone入门系列(4)集合
    backbone入门系列(3)视图
    backbone入门系列(2)模型
    backbone入门系列(1)基本组成部分
    $(document).ready(function(){ })、window.onload=function(){}与(function($){...})(jQuery)的对比和作用
    backbone笔记1,MVC
    用Object.prototype.toString()来检测对象的类型
    Python生成requirements.txt方法
  • 原文地址:https://www.cnblogs.com/syhyfh/p/12500360.html
Copyright © 2011-2022 走看看