关于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。

原文地址:https://www.cnblogs.com/ZJOE80/p/12875379.html