zoukankan      html  css  js  c++  java
  • 关于wait/notify(二)

    一.wait/notity的使用

    wait()方法可以使线程进入等待状态,而notify()可以使等待的状态唤醒。

    这样的同步机制十分适合生产者、消费者模式:消费者消费某个资源,而生产者生产该资源。

    当该资源缺失时,消费者调用wait()方法进行自我阻塞,等待生产者的生产;生产者生产完毕后调用notify/notifyAll()唤醒消费者进行消费。

    例子1:

    代码示例:

    public class ThreadTest {
    
        static final Object obj = new Object();
    
        private static boolean flag = false; //flag标志表示资源的有无。
    
        public static void main(String[] args) throws Exception {
    
            Thread consume = new Thread(new Consume(), "Consume");
            Thread produce = new Thread(new Produce(), "Produce");
            consume.start();
            Thread.sleep(1000);
            produce.start();
    
            try {
                produce.join();
                consume.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        // 生产者线程
        static class Produce implements Runnable {
    
            @Override
            public void run() {
    
                synchronized (obj) {
                    System.out.println("进入生产者线程");
                    System.out.println("生产");
                    try {
                        TimeUnit.MILLISECONDS.sleep(2000);  //模拟生产过程
                        flag = true;
                        obj.notify();  //通知消费者
                        TimeUnit.MILLISECONDS.sleep(1000);  //模拟其他耗时操作
                        System.out.println("退出生产者线程");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    
        //消费者线程
        static class Consume implements Runnable {
    
            @Override
            public void run() {
                synchronized (obj) {
                    System.out.println("进入消费者线程");
                    System.out.println("wait flag 1:" + flag);
                    while (!flag) {  //判断条件是否满足,若不满足则等待
                        try {
                            System.out.println("还没生产,进入等待");
                            obj.wait();
                            System.out.println("结束等待");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("wait flag 2:" + flag);
                    System.out.println("消费");
                    System.out.println("退出消费者线程");
                }
            }
        }
    }

    运行结果为:

    进入消费者线程
    wait flag 1:false
    还没生产,进入等待
    进入生产者线程
    生产
    退出生产者线程
    结束等待
    wait flag 2:true
    消费
    退出消费者线程

    理解了输出结果的顺序,也就明白了wait/notify的基本用法。有以下几点需要知道:

    在示例中没有体现但很重要的是,wait/notify方法的调用必须处在该对象的锁(Monitor)中,在调用这些方法时首先需要获得该对象的锁,否则会爬出IllegalMonitorStateException异常。

    从输出结果来看,在生产者调用notify()后,消费者并没有立即被唤醒,而是等到生产者退出同步块后才唤醒执行。

    这点其实也好理解,synchronized同步方法(块)同一时刻只允许一个线程在里面,生产者不退出,消费者也进不去。

    注意:消费者被唤醒后是从wait()方法(被阻塞的地方)后面执行,而不是重新从同步块开头。

    例子2:

    箱子中的苹果代表资源,现在有消费者从箱子中拿走苹果,生产者往箱子中放苹果。代码如下:

    资源--箱子中的苹果:

    public class Box {
    int size; int num; public Box(int size, int num) { this.size = size; this.num = num; } public synchronized void put() { try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } while (num == 10) { //用while循环检查更好,在下面的wait()结束后还再判断一次,防止虚假唤醒 try { System.out.println("箱子满了,生产者暂停。。。"); this.wait(); //等待消费者消费一个才能继续生产,所以要让出锁 } catch (InterruptedException e) { e.printStackTrace(); } finally { } } num++; System.out.println("箱子有空闲,开始生产。。。"+num); this.notify(); //唤醒可能因为没苹果而等待的消费者 } public synchronized void take() { try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } while (num == 0) { //用while循环检查更好,在wait()结束后还再判断一次,防止虚假唤醒 try { System.out.println("箱子空了,消费者暂停。。。"); this.wait(); //等待生产者生产一个才能继续消费,所以要让出锁 } catch (InterruptedException e) { e.printStackTrace(); } finally { } } num--; System.out.println("箱子有了,开始消费。。。"+num); this.notify(); //唤醒可能因为苹果满了而等待的生产者 } }

    生产者、消费者:

    public class Consumer implements Runnable {
     
        private Box box;
     
        public Consumer(Box box) {
            this.box= box;
        }
     
        @Override
        public void run() {
            while (true){
                box.take();
            }
     
        }
    }
    public class Producer implements Runnable {
     
        private Box box;
     
        public Producer(Box box) {
            this.box= box;
        }
     
        @Override
        public void run() {
            while (true){
                box.put();
            }
     
        }
    }
    public class ConsumerAndProducer {
     
        public static void main(String[] args) {
    Box box
    = new Box();
    Producer p1
    = new Producer(box); //生产线程 Consumer c1 = new Consumer(box); //消费线程 new Thread(p1).start(); new Thread(c1).start(); } }

    以上,就是生产者消费者模式的Java代码实现。当然,我们完全可以使用JUC包的Lock接口下的类代替Synchronized完成代码同步:

    Lock l = new ReentrantLock();
    Condition condition = l.newCondition();
    
    l.lock()  //加锁
    
    l.unlock()  //释放锁
    
    condition.await()  //代替wait()
    
    condition.signal()   //代替notify()

    除了上述方法,也可以使用JUC包下BlockingQueue接口的阻塞队列完成,那样更简单。

    实际上,阻塞队列也是基于上述的基本思想实现的----队列满了就停止装入线程、空了就让取队列元素的线程等待。

    上述的Box就是一个阻塞队列的抽象模型(当然阻塞队列比这个还是要复杂很多)。

    1、wait、notify要放在同步块中

    其实很简单,如果不在同步块中,调用this.wait()时当前线程都没有取得对象的锁,又谈何让对象通知线程释放锁、或者来竞争锁呢?

    如果确实不放到同步块中,则会产生 Lost-wake的问题,即丢失唤醒,以生产者消费者例子来说:

    (1)箱子发现自己满了调用box.wait()通知生产者等待,但是由于wait没在同步块中,还没等生产者接到wait信号进入等待,

    消费者线程就插队执行消费箱子苹果的方法了(因为wait不在同步块中,也就是调用时箱子的锁没被占有,所以箱子的消费方法是可以被消费者插队调用的)。

    (2)这时消费者线程从缓冲区消费一个产品后箱子调用box.notify()方法,但生产者此时还没进入等待,因此notify消息将被生产者忽略。

    (3)生产者线程恢复执行接收到迟来的wait()信号后进入等待状态,但是得不到notify通知了,一直等待下去。

    总结就是,由于wait不在同步块中,所以对象执行wait()到线程接到通知进入等待这段时间是可以被其他线程插队,

    如果这时插队的线程把notify信号发出则会被忽略,因为本来要被wait的线程还在卡着呢。

    总之,这里的竞争条件,我们可能在丢失一个通知,如果我们使用缓冲区或者只有一个产品,生产者线程将永远等待,你的程序也就挂起了。

    2、虚假唤醒

    notify/notifyAll时唤醒的线程并不一定是满足真正可以执行的条件了。比如对象o,不满足A条件时发出o.wait(),

    然后不满足条件B时也发出o.wait;然后条件B满足了,发出o.notify(),唤醒对象o的等待池里的对象,但是唤醒的线程有可能是因为条件A进入等待的线程,这时把他唤醒条件A还是不满足。

    这是底层系统决定的一个小遗憾。为了避免这种情况,判断调用o.wait()的条件时必须使用while,而不是if,这样在虚假唤醒后会继续判断是否满足A条件,不满足说明是虚假唤醒又会调用o.wait()。

    3、改变线程优先级

    每个线程执行时都具有一定的优先级,优先级高的线程获得较多的执行机会,而优先级低的线程则获得较少的执行机会。

    每个线程默认的优先级都与创建它的父线程的优先级相同。

    Thread类提供了setPriority(int newPriority)来设置指定线程的优先级,提供了getPriority()来返回指定线程的优先级。

    JAVA提供了10个优先级级别,但这些优先级需要操作系统支持。不同的操作系统上的优先级并不相同,而且也不能很好的和JAVA的10个优先级对应,

    比如:Windows 2000仅提供了7个优先级。因此,写代码的时候应该尽量避免直接为线程指定优先级,

    而应该使用MAX_PRIORITY、MIN_PRIORITY、NORM_PRIORITY这三个静态常量来设置优先级,这样才能保证程序有最好的可移植性。

    4、一个线程两次调用start方法会出现什么情况?

    Java的线程是不允许启动两次的,第二次调用必然会抛出IllegalThreadStateException,这是一种运行时异常。

    5、park()与unpark()

    concurrent包是基于AQS(AbstractQueuedSynchronizer)框架的,AQS框架借助于两个类:

    (1)Unsafe(提供CAS操作);

    (2)LockSupport(提供park/unpark操作);

    LockSupport.park()和LockSupport.unpark(Thread thread)调用的是Unsafe中的native代码。

    6、park与unpark的特点

    (1)park/unpark的设计原理核心是“许可”(permit):park是等待一个许可,unpark是为某线程提供一个许可。permit不能叠加,也就是说permit的个数要么是0,要么是1。

    也就是不管连续调用多少次unpark,permit也是1个。线程调用一次park就会消耗掉permit,再一次调用park又会阻塞住。

    如果某线程A调用park,那么除非另外一个线程调用unpark(A)给A一个许可,否则线程A将阻塞在park操作上。

    (2)unpark可以先于park调用。在使用park和unpark的时候可以不用担心park的时序问题造成死锁。

    相比之下,wait/notify存在时序问题,wait必须在notify调用之前调用,否则虽然另一个线程调用了notify,但是由于在wait之前调用了,wait感知不到,就造成wait永远在阻塞。

    (3)park和unpark调用的时候不需要获取同步锁。

    7、park与unpark的优点

    与Object类的wait/notify机制相比,park/unpark有两个优点:

    (1)以thread为操作对象更符合阻塞线程的直观定义。

    (2)操作更精准,可以准确地唤醒某一个线程(notify随机唤醒一个线程,notifyAll唤醒所有等待的线程),增加了灵活性。

    底层实现原理:

    在Linux系统下,是用的Posix线程库pthread中的mutex(互斥量),condition(条件变量)来实现的。 mutex和condition保护了一个_counter的变量,

    当park时,这个变量被设置为0,当unpark时,这个变量被设置为1。

  • 相关阅读:
    VBOX虚拟化工具做VPA学习都很方便硬件信息完全实现真实模拟
    Dynamics CRM2016 使用web api来创建注释时的注意事项
    Dynamics CRM build numbers
    域控制器的角色转移
    辅域控制器的安装方法
    利用QrCode.Net生成二维码 asp.net mvc c#
    给现有的word和pdf加水印
    利用LogParser将IIS日志插入到数据库
    短文本情感分析
    Dynamics CRM Ribbon WorkBench 当ValueRule的值为空时的设置
  • 原文地址:https://www.cnblogs.com/ZJOE80/p/12875379.html
Copyright © 2011-2022 走看看