9.线程生产者消费者

.生产者和消费者的synchronized 版本以及虚假唤醒问题

先看一个场景:
    一个卖面的面馆,有一个做面的厨师和一个吃面的食客,需要保证,厨师做一碗面,食客吃一碗面,
    不能一次性多做几碗面,更不能没有面的时候吃面;
    按照上述操作,进行十轮做面吃面的操作。
样例代码:
    public class Juc_test {
        public static void main(String[] args){
            Noodels noodels=new Noodels();
            //重点1:创建两个线程:1厨子造面   2.食客吃面,先做10碗
            new Thread(()->{
                try {
                    for (int i=0;i<=10;i++){
                        noodels.makeNoodeles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"厨子").start();
            new Thread(()->{
                try {
                    for (int i=0;i<=10;i++){
                        noodels.eatNoodeles();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            },"食客").start();
        }
    }
    class Noodels{
        //面的数量
        private int num=0;
        /*
            做面方法
         */
        public synchronized  void makeNoodeles() throws InterruptedException {
            if(num>0){
                this.wait();
            }
            num++;
            System.out.println(Thread.currentThread().getName()+"造面了!当前面数:"+num);
            //面已做好,唤醒食客
            this.notifyAll();
        }
    
        public synchronized void eatNoodeles() throws InterruptedException {
            if (num==0){
                this.wait();
            }
            num--;
            System.out.println(Thread.currentThread().getName()+"造面了!当前面数:"+num);
            this.notifyAll();
        }
    }
运行结果如下:很满意,达到了效果,做一碗吃一碗的水平!
    厨子造面了!当前面数:1
    食客造面了!当前面数:0
    厨子造面了!当前面数:1
    食客造面了!当前面数:0
    厨子造面了!当前面数:1
    食客造面了!当前面数:0
    厨子造面了!当前面数:1


问题来了:如果多个线程呢,即两个厨子,两个食客呢??改造代码,只需要在测试类里多加几个线程!
    new Thread(()->{ for (int i=0;i<=10;i++){noodels.makeNoodeles();}},"厨子A").start();
    new Thread(()->{ for (int i=0;i<=10;i++){noodels.makeNoodeles();}},"厨子B).start();
    new Thread(()->{ for (int i=0;i<=10;i++){noodels.makeNoodeles();}},"食客A").start();
    new Thread(()->{ for (int i=0;i<=10;i++){noodels.makeNoodeles();}},"食客B").start();
运行结果:打破了吃一碗造一碗的逻辑了,并且明明加了synchronized锁,为什么会这样呢?这里就涉及到了线程的虚假唤醒!
    厨子A造面了!当前面数:1
    食客A造面了!当前面数:0
    厨子B造面了!当前面数:1
    厨子A造面了!当前面数:2
    厨子B造面了!当前面数:3
    ...

虚假唤醒

上面的问题就是"虚假唤醒"。
当我们只有一个厨师一个食客时,只能是厨师做面或者食客吃面,并没有其他情况;
但是当有两个厨师,两个食客时,就会出现下面的问题:

  1.初始状态

2.厨师A得到操作权,发现面的数量为0,可以做面,面的份数+1,然后唤醒所有线程;

3.厨师B得到操作权,发现面的数量为1,不可以做面,执行wait操作;

4.厨师A得到操作权,发现面的数量为1,不可以做面,执行wait操作;

5.食客甲得到操作权,发现面的数量为1,可以吃面,吃完面后面的数量-1,并唤醒所有线程;

6.此时厨师A得到操作权了,因为是从刚才阻塞的地方继续运行,就不用再判断面的数量是否为0了,所以直接面的数量+1,并唤醒其他线程;

7.此时厨师B得到操作权了,因为是从刚才阻塞的地方继续运行,就不用再判断面的数量是否为0了,所以直接面的数量+1,并唤醒其他线程;

  if(num != 0){
         this.wait();
    }
出现虚假唤醒的原因:
    是从阻塞态到就绪态再到运行态没有进行判断(num的判断),直接进行下述的+1或-1,
    所以我们只需要让其每次得到操作权时都进行判断就可以了;
解决办法:将if判断改为while,每次都会判断,唤醒以后都先判断条件是否成立!
    while(num != 0){
         this.wait();
    }

JUC版的生产者和消费者

使用Lock和Condition实现上述生产者和消费者问题:
    样例代码:
        class Noodels{
            //面的数量
            private int num=0;
            //重点1:获取lock锁(可重入锁)
            Lock lock = new ReentrantLock();
            //重点2:获取condition对象
            Condition condition = lock.newCondition();
            /*
                做面方法
             */
            public void makeNoodeles() throws InterruptedException {
                //重点3:加锁,不用synchronized关键字
                lock.lock();
                try {
                    while (num>0){
                        //重点4:使用condition.await来阻塞队列
                        condition.await();
                    }
                    num++;
                    System.out.println(Thread.currentThread().getName()+"造面了!当前面数:"+num);
                    //重点5:condition.signalAll();来解锁
                    condition.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    //重点5:解锁
                    lock.unlock();
                }
            }
        
            public synchronized void eatNoodeles() throws InterruptedException {
                lock.lock();
                try {
                    while (num==0){
                        condition.await();
                    }
                    num--;
                    System.out.println(Thread.currentThread().getName()+"造面了!当前面数:"+num);
                    condition.signalAll();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
        
问题:如何精确唤醒呢?例如A,B,C,D四个线程,如何精确的执行顺序A->B->C->D
    实现如下:    
    /**
     * Bruk.liu
     * A执行完调用B,B执行完调用C,C执行完调用A
     */
    public class C {
        public static void main(String[] args) {
            Data3 data = new Data3();
     
            new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    data.printA();
                }
            },"A").start();
            
            new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    data.printB();
                }
            },"B").start();
            
            new Thread(()->{
                for (int i = 0; i < 10; i++) {
                    data.printC();
                }
            },"C").start();
        }
    }
     
    //资源类
    class Data3{
        //创建Lock锁
       private Lock lock = new ReentrantLock();
        //同步监视器,创建三个监视器
        Condition condition1 = lock.newCondition();
        Condition condition2 = lock.newCondition();
        Condition condition3 = lock.newCondition();
        private int number = 1;//1就是A执行,2就是B执行
     
       public void printA(){
            lock.lock();
           try {
               while(number != 1){
                   //等待
                   condition1.await();
               }
               System.out.println(Thread.currentThread().getName()+"===>AAAAAAAAA");
               //唤醒指定线程
                number = 2;
               //调用指定监视器,唤醒指定线程
               condition2.signal();
           } catch (Exception e) {
               e.printStackTrace();
           } finally {
               lock.unlock();
           }
       }
     
        public void printB(){
            lock.lock();
            try {
                while(number != 2){
                    //等待
                    condition2.await();
                }
                System.out.println(Thread.currentThread().getName()+"===>BBBBBBBB");
                //唤醒指定线程
                number = 3;
                condition3.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
     
        public void printC(){
            lock.lock();
            try {
                while(number != 3){
                    //等待
                    condition3.await();
                }
                System.out.println(Thread.currentThread().getName()+"===>CCCCCCCCC");
                //唤醒指定线程
                number = 1;
                condition1.signal();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    }

8锁现象

1.synchronized锁的对象是方法调用者!
2.static synchronized...锁的是class这个类!
示例如下:
1.两种方法都sendMessage/call都加上锁synchronized,锁的是方法的调用者!
        public class Juc_Test_Lock {
            public static void main(String[] args) throws InterruptedException {
               //同一个资源对象Phone,所以所的是同一资源对象phone
               Phone phone=new Phone();
               new Thread(()->{phone.sendMessage();},"发短信!").start();
               //重点1:两个线程间休眠4秒,能明显看出哪个线程先执行
               TimeUnit.SECONDS.sleep(1);;
               new Thread(()->{phone.call();},"打电话!").start();
            }
        }
        class Phone{
            //重点2:方法加锁,并在打印前加入休眠,能明显看出哪个先执行!
            public synchronized void sendMessage(){
                try {
                    TimeUnit.SECONDS.sleep(4);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("发送短信");
            }
            public synchronized void call(){
                System.out.println("打电话!");
            }
        }
    输出:因为两个方法都加了synchronized,锁的是方法的调用者,方法调用者是同一对象phone,所以锁是同一把锁!
        发送短信
        打电话!

2.两个对象:
    创建两个对象,因为synchronized锁的是方法调用者,此处是 phone1和phone2,不是同一对象,所以线程2不会等待线程1执行完毕
        Phone phone1=new Phone();
        Phone phone2=new Phone();
        new Thread(()->{phone1.sendMessage();},"发短信!").start();
        TimeUnit.SECONDS.sleep(1);;
        new Thread(()->{phone2.call();},"打电话!").start();
    结论:
        打电话!   
        发送短信 

3.static synchronized:锁定的是这个类模板
    代码示例如下:
        public class Juc_Test_Lock {
            public static void main(String[] args) throws InterruptedException {
               Phone phone=new Phone();
               new Thread(()->{phone.sendMessage();},"发短信!").start();
               TimeUnit.SECONDS.sleep(1);;
               new Thread(()->{phone.call();},"打电话!").start();
            }
        }
        class Phone{
            重点1:方法上使用static synchronized修饰
            public static synchronized void sendMessage(){
                try {
                    TimeUnit.SECONDS.sleep(4);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("发送短信");
            }
            public static synchronized void call(){
                System.out.println("打电话!");
            }
        }
    输出:static synchronized:锁定的是这个类模板,线程2必须等待线程1执行完毕后才能执行!因为这是一把锁
        发送短信
        打电话!

4.上述情况下,方法加了static synchronized,但是是两个对象
    重点1:创建了两个对象去执行static synchronized修饰的方法
        Phone phone1=new Phone();
        Phone phone2=new Phone();
        new Thread(()->{phone1.sendMessage();},"发短信!").start();
        TimeUnit.SECONDS.sleep(1);;
        new Thread(()->{phone2.call();},"打电话!").start();
    输出:发现线程2必须等线程1执行完毕后才执行,因为static synchronized锁的是类模板,是一般锁!
        发送短信
        打电话!

5.staic synchronized和synchronized混用:
    public class Juc_Test_Lock {
        public static void main(String[] args) throws InterruptedException {
           重点1:单个对象 
           Phone phone=new Phone();
           new Thread(()->{phone.sendMessage();},"发短信!").start();
           TimeUnit.SECONDS.sleep(1);;
           new Thread(()->{phone.call();},"打电话!").start();
        }
    }
    class Phone{
        重点2:使用了static synchronized
        public static synchronized void sendMessage(){
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("发送短信");
        }
        重点3:使用synchronized
        public synchronized void call(){
            System.out.println("打电话!");
        }
    }
    输出:因为staic synchronized和synchronized锁的是不同的对象,synchronized锁的是方法调用者phone对象,而staic synchronized锁的是Phnoe类模板
    所以是两把不同的锁,所以线程2不会等待1执行结束才执行!
        打电话!
        发送短信

 

posted @ 2022-05-19 22:07  努力的达子  阅读(25)  评论(0编辑  收藏  举报