多线程(三)

前言

本篇文章是多线程系列的第三篇(第二篇可参考多线程(二)),主要讲解:死锁、等待-唤醒机制、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 实现组合使用。

posted @ 2020-03-16 15:41  samsaraaa  阅读(136)  评论(0编辑  收藏  举报