关于多线程:母鸡下蛋实例多线程通信生产者和消费者waitnotify和conditionawaitsignal条件队列

43次阅读

共计 8619 个字符,预计需要花费 22 分钟才能阅读完成。

简介


多线程通信始终是高频 面试 考点,有些面试官可能要求现场手写 生产者 / 消费者 代码来考查多线程的功底,明天咱们以理论生存中母鸡下蛋案例用代码分析下实现过程。母鸡在鸡窝下蛋了,叫练从鸡窝里把鸡蛋拿进去这个过程,母鸡在鸡窝下蛋,是生产者,叫练捡出鸡蛋,叫练是消费者,一进一出就是线程中的生产者和消费者模型了,鸡窝是放鸡蛋容器。事实中还有很多这样的案例,如医院叫号。上面咱们画个图示意下。

一对一生产和生产:一只母鸡和叫练


wait/notify

package com.duyang.thread.basic.waitLock.demo;

import java.util.ArrayList;
import java.util.List;

/**
 * @author:jiaolian
 * @date:Created in 2020-12-30 16:18
 * @description:母鸡下蛋:一对一生产者和消费者
 * @modified By:* 公众号: 叫练
 */
public class SingleNotifyWait {

    // 装鸡蛋的容器
    private static class EggsList {private static final List<String> LIST = new ArrayList();
    }

    // 生产者:母鸡实体类
    private static class HEN {
        private String name;

        public HEN(String name) {this.name = name;}

        // 下蛋
        public void proEggs() throws InterruptedException {synchronized (EggsList.class) {if (EggsList.LIST.size() == 1) {EggsList.class.wait();
                }
                // 容器增加一个蛋
                EggsList.LIST.add("1");
                // 鸡下蛋须要劳动能力持续产蛋
                Thread.sleep(1000);
                System.out.println(name+": 下了一个鸡蛋!");
                // 告诉叫练捡蛋
                EggsList.class.notify();}
        }
    }

    // 人对象
    private static class Person {
        private String name;

        public Person(String name) {this.name = name;}
        // 取蛋
        public void getEggs() throws InterruptedException {synchronized (EggsList.class) {if (EggsList.LIST.size() == 0) {EggsList.class.wait();
                }
                Thread.sleep(500);
                EggsList.LIST.remove(0);
                System.out.println(name+": 从容器中捡出一个鸡蛋");
                // 告诉叫练捡蛋
                EggsList.class.notify();}
        }
    }

    public static void main(String[] args) {
        // 发明一个人和一只鸡
        HEN hen = new HEN("小黑");
        Person person = new Person("叫练");
        // 创立线程执行下蛋和捡蛋的过程;new Thread(()->{
            try {for (int i=0; i<Integer.MAX_VALUE;i++) {hen.proEggs();
                }
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }).start();
        // 叫练捡鸡蛋的过程!new Thread(()->{
            try {for (int i=0; i<Integer.MAX_VALUE;i++) {person.getEggs();
                }
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }).start();}
}

如下面代码,咱们定义 EggsList 类来装鸡蛋,HEN类示意母鸡,Person类示意人。在主函数中创立母鸡对象“小黑”,人对象“叫练”,创立两个线程别离执行下蛋和捡蛋的过程。代码中定义鸡窝中最多只能装一个鸡蛋(当然能够定义多个)。具体过程:“小黑”母鸡线程和“叫练”线程线程竞争锁,如果“小黑”母鸡线程先获取锁,发现 EggsList 鸡蛋的个数大于 0,示意有鸡蛋,那就调用 wait 期待并开释锁给“叫练”线程,如果没有鸡蛋,就调用 EggsList.LIST.add(“1”)示意生产了一个鸡蛋并告诉“叫练”来取鸡蛋并开释锁让“叫练”线程获取锁。“叫练”线程调用 getEggs()办法获取锁后发现,如果鸡窝中并没有鸡蛋就调用 wait 期待并开释锁告诉“小黑”线程获取锁去下蛋,如果有鸡蛋,阐明“小黑”曾经下蛋了,就把鸡蛋取走,因为鸡窝没有鸡蛋了,所以最初也要告诉调用 notify()办法告诉“小黑”去下蛋,咱们察看程序的执行后果如下图。两个线程是死循环程序会始终执行上来,下蛋和捡蛋的过程中用到的锁的是 EggsList 类的 class,“小黑”和“叫练”竞争的都是对立把锁,所以这个是同步的。这就是母鸡“小黑”和“叫练”沟通的过程。

神马???鸡和人能沟通!!

Lock 条件队列

package com.duyang.thread.basic.waitLock.demo;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author:jiaolian
 * @date:Created in 2020-12-30 16:18
 * @description:母鸡下蛋:一对一生产者和消费者 条件队列
 * @modified By:* 公众号: 叫练
 */
public class SingleCondition {private static Lock lock = new ReentrantLock();
    // 条件队列
    private static Condition condition = lock.newCondition();

    // 装鸡蛋的容器
    private static class EggsList {private static final List<String> LIST = new ArrayList();
    }

    // 生产者:母鸡实体类
    private static class HEN {
        private String name;

        public HEN(String name) {this.name = name;}

        // 下蛋
        public void proEggs() {
            try {lock.lock();
                if (EggsList.LIST.size() == 1) {condition.await();
                }
                // 容器增加一个蛋
                EggsList.LIST.add("1");
                // 鸡下蛋须要劳动能力持续产蛋
                Thread.sleep(1000);
                System.out.println(name+": 下了一个鸡蛋!");
                // 告诉叫练捡蛋
                condition.signal();} catch (Exception e) {e.printStackTrace();
            } finally {lock.unlock();
            }
        }
    }

    // 人对象
    private static class Person {
        private String name;

        public Person(String name) {this.name = name;}
        // 取蛋
        public void getEggs() {
            try {lock.lock();
                if (EggsList.LIST.size() == 0) {condition.await();
                }
                Thread.sleep(500);
                EggsList.LIST.remove(0);
                System.out.println(name+": 从容器中捡出一个鸡蛋");
                // 告诉叫练捡蛋
                condition.signal();} catch (Exception e) {e.printStackTrace();
            } finally {lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        // 发明一个人和一只鸡
        HEN hen = new HEN("小黑");
        Person person = new Person("叫练");
        // 创立线程执行下蛋和捡蛋的过程;new Thread(()->{for (int i=0; i<Integer.MAX_VALUE;i++) {hen.proEggs();
            }
        }).start();
        // 叫练捡鸡蛋的过程!new Thread(()->{for (int i=0; i<Integer.MAX_VALUE;i++) {person.getEggs();
            }
        }).start();}
}

如下面代码,只是将 synchronized 换成了 Lock,程序运行的后果和下面的统一,wait/notify 换成了 AQS 的条件队列 Condition 来控制线程之间的通信。Lock 须要手动加锁 lock.lock(),解锁 lock.unlock()的步骤放在 finally 代码块保障锁始终能被开释。await 底层是 unsafe.park(false,0)调用 C ++ 代码实现。

多对多生产和生产:2 只母鸡和叫练 / 叫练媳妇


wait/notifyAll

package com.duyang.thread.basic.waitLock.demo;

import java.util.ArrayList;
import java.util.List;

/**
 * @author:jiaolian
 * @date:Created in 2020-12-30 16:18
 * @description:母鸡下蛋:多对多生产者和消费者
 * @modified By:* 公众号: 叫练
 */
public class MultNotifyWait {

    // 装鸡蛋的容器
    private static class EggsList {private static final List<String> LIST = new ArrayList();
    }

    // 生产者:母鸡实体类
    private static class HEN {
        private String name;

        public HEN(String name) {this.name = name;}

        // 下蛋
        public void proEggs() throws InterruptedException {synchronized (EggsList.class) {while (EggsList.LIST.size() >= 10) {EggsList.class.wait();
                }
                // 容器增加一个蛋
                EggsList.LIST.add("1");
                // 鸡下蛋须要劳动能力持续产蛋
                Thread.sleep(1000);
                System.out.println(name+": 下了一个鸡蛋!共有"+EggsList.LIST.size()+"个蛋");
                // 告诉叫练捡蛋
                EggsList.class.notify();}
        }
    }

    // 人对象
    private static class Person {
        private String name;

        public Person(String name) {this.name = name;}
        // 取蛋
        public void getEggs() throws InterruptedException {synchronized (EggsList.class) {while (EggsList.LIST.size() == 0) {EggsList.class.wait();
                }
                Thread.sleep(500);
                EggsList.LIST.remove(0);
                System.out.println(name+": 从容器中捡出一个鸡蛋!还剩"+EggsList.LIST.size()+"个蛋");
                // 告诉叫练捡蛋
                EggsList.class.notify();}
        }
    }

    public static void main(String[] args) {
        // 发明一个人和一只鸡
        HEN hen1 = new HEN("小黑");
        HEN hen2 = new HEN("小黄");
        Person jiaolian = new Person("叫练");
        Person wife = new Person("叫练媳妇");
        // 创立线程执行下蛋和捡蛋的过程;new Thread(()->{
            try {for (int i=0; i<Integer.MAX_VALUE;i++) {hen1.proEggs();
                    Thread.sleep(50);
                }
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {for (int i=0; i<Integer.MAX_VALUE;i++) {hen2.proEggs();
                    Thread.sleep(50);
                }
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }).start();
        // 叫练捡鸡蛋的线程!new Thread(()->{
            try {for (int i=0; i<Integer.MAX_VALUE;i++) {jiaolian.getEggs();
                }
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }).start();
        // 叫练媳妇捡鸡蛋的线程!new Thread(()->{
            try {for (int i=0; i<Integer.MAX_VALUE;i++) {wife.getEggs();
                }
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }).start();}
}

如下面代码,参照一对一生产和生产中 wait/notify 代码做了一些批改,创立了两个母鸡线程“小黑”,“小黄”,两个捡鸡蛋的线程“叫练”,“叫练媳妇”,执行后果是同步的,实现了多对多的生产和生产,如下图所示。有如下几点须要留神的中央:

  1. 鸡窝中能包容最大的鸡蛋是 10 个。
  2. 下蛋 proEggs()办法中判断鸡蛋数量是否大于等于 10 个应用的是 while 循环,wait 收到告诉,唤醒以后线程,须要从新判断一次,防止程序呈现逻辑问题,这里不能用 if,如果用 if,程序可能呈现 EggsList 有超过 10 以上鸡蛋的状况。这是这道程序中容易呈现谬误的中央,也是常常会被问到的点,值得重点探索下。
  3. 多对多的生产者和消费者。

Lock 条件队列


import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author:jiaolian
 * @date:Created in 2020-12-30 16:18
 * @description:母鸡下蛋:多对多生产者和消费者 条件队列
 * @modified By:* 公众号: 叫练
 */
public class MultCondition {private static Lock lock = new ReentrantLock();
    // 条件队列
    private static Condition condition = lock.newCondition();

    // 装鸡蛋的容器
    private static class EggsList {private static final List<String> LIST = new ArrayList();
    }

    // 生产者:母鸡实体类
    private static class HEN {
        private String name;

        public HEN(String name) {this.name = name;}

        // 下蛋
        public void proEggs() {
            try {lock.lock();
                while (EggsList.LIST.size() >= 10) {condition.await();
                }
                // 容器增加一个蛋
                EggsList.LIST.add("1");
                // 鸡下蛋须要劳动能力持续产蛋
                Thread.sleep(1000);
                System.out.println(name+": 下了一个鸡蛋!共有"+ EggsList.LIST.size()+"个蛋");
                // 告诉叫练 / 叫练媳妇捡蛋
                condition.signalAll();} catch (Exception e) {e.printStackTrace();
            } finally {lock.unlock();
            }
        }
    }

    // 人对象
    private static class Person {
        private String name;

        public Person(String name) {this.name = name;}
        // 取蛋
        public void getEggs() throws InterruptedException {
            try {lock.lock();
                while (EggsList.LIST.size() == 0) {condition.await();
                }
                Thread.sleep(500);
                EggsList.LIST.remove(0);
                System.out.println(name+": 从容器中捡出一个鸡蛋!还剩"+ EggsList.LIST.size()+"个蛋");
                // 告诉叫练捡蛋
                condition.signalAll();} catch (Exception e) {e.printStackTrace();
            } finally {lock.unlock();
            }
        }
    }

    public static void main(String[] args) {
        // 发明一个人和一只鸡
        HEN hen1 = new HEN("小黑");
        HEN hen2 = new HEN("小黄");
        Person jiaolian = new Person("叫练");
        Person wife = new Person("叫练媳妇");
        // 创立线程执行下蛋和捡蛋的过程;new Thread(()->{
            try {for (int i=0; i<Integer.MAX_VALUE;i++) {hen1.proEggs();
                    Thread.sleep(50);
                }
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {for (int i=0; i<Integer.MAX_VALUE;i++) {hen2.proEggs();
                    Thread.sleep(50);
                }
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }).start();
        // 叫练捡鸡蛋的线程!new Thread(()->{
            try {for (int i=0; i<Integer.MAX_VALUE;i++) {jiaolian.getEggs();
                }
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }).start();
        // 叫练媳妇捡鸡蛋的线程!new Thread(()->{
            try {for (int i=0; i<Integer.MAX_VALUE;i++) {wife.getEggs();
                }
            } catch (InterruptedException e) {e.printStackTrace();
            }
        }).start();}
}

如下面代码,只是将 synchronized 换成了 Lock,程序运行的后果和下面的统一,上面咱们比拟下 Lock 和 synchronized 的异同。这个问题也是面试中会常常问到的!

Lock 和 synchronized 比拟


Lock 和 synchronized 都能让多线程同步。次要异同点体现如下!

  1. 锁性质:Lock 乐观锁是非阻塞的,底层是依赖 cas+volatile 实现,synchronized 乐观锁是阻塞的,须要上下文切换。实现思维不一样。
  2. 性能细节上:Lock 须要手动加解锁,synchronized 主动加解锁。Lock 还提供颗粒度更细的性能,比方 tryLock 等。
  3. 线程通信:Lock 提供 Condition 条件队列,一把锁能够对应多个条件队列,对线程管制更细腻。synchronized 只能对应一个 wait/notify。

次要就这些吧,如果对 synchronized,volatile,cas 关键字不太理解的童鞋,能够看看我之前的文章,有很具体的案例和阐明。

总结


明天用生存中的例子转化成代码,实现了两种多线程中消费者 / 生产者模式,给您的倡议就是须要把代码敲一遍,如果认真执行了一遍代码应该能看明确,喜爱的请点赞加关注哦。我是 叫练【公众号】,边叫边练。

正文完
 0