Wait for the wake-up mechanism in Java - at least 50% of the engineers have not mastered!

This is a walking heart filled pit notes, self-learning Java a few years are always learning new skills along the way finds himself stepped on countless pit, and the pit fill only a handful. Suddenly found that sometimes it is really not a problem a few years of work experience, even if some things work ten years, there is no intention to study through 10 years is nothing but a pit nothing (true feelings).

When just getting started with multithreading, you know wait / wake this thing, wrote a demo have never seen, as in the end it is what, or what kind of problems it can solve, estimated that most people I, like all ambiguous. This time I will try to take you to get to know the wait / wake-up mechanism, reading this article you will get to the following:

  1. What kind of problems caused by waiting for the cycle
  2. Wake-up mechanism to optimize circulation wait wait
  3. Waiting for the details to be ignored wake-up mechanisms

First, the circular wait problem

Suppose today to wages, a strong boss to eat a good meal, eating whole process can be divided into the following steps:

  1. order
  2. Window waiting for the meal
  3. Dining
  public static void main(String[] args) {
     // 是否还有包子
        AtomicBoolean hasBun = new AtomicBoolean();
        
        // 包子铺老板
        new Thread(() -> {
            try {
                // 一直循环查看是否还有包子
                while (true) {
                    if (hasBun.get()) {
                        System.out.println("老板:检查一下是否还剩下包子...");
                        Thread.sleep(3000);
                    } else {
                        System.out.println("老板:没有包子了, 马上开始制作...");
                        Thread.sleep(1000);
                        System.out.println("老板:包子出锅咯....");
                        hasBun.set(true);
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();


        new Thread(() -> {
            System.out.println("小强:我要买包子...");
            try {
                // 每隔一段时间询问是否完成
                while (!hasBun.get()) {
                    System.out.println("小强:包子咋还没做好呢~");
                    Thread.sleep(3000);
                }
                System.out.println("小强:终于吃上包子了....");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }

In the code above there is a big problem, is the boss needs constantly to check whether there are buns, and customers will need reminders from time to time to see what the boss, which is obviously unreasonable, which is typical of circular wait problem.

Code for this problem are generally follows this pattern:

   while (条件不满足) {
       Thread.sleep(3000);
   }
   doSomething();

Corresponds to the computer, then exposed a problem: ever by the polling mechanism to detect the condition is satisfied, if the polling time is too small will be a waste of CPU resources, if the gap is too large, they can not lead to the desired access to resources in a timely manner .

Second, wait / wake-up mechanism

In order to solve consume CPU cycles to wait and timeliness of the information , provided in the Java wait wake-up mechanism. Popular terms is made active becomes passive , when the conditions are met, the initiative to inform the corresponding thread, the thread itself rather than to ask.

2.1 Basic Concepts

Wait / wake-up mechanism, also known as waiting for notification (I prefer to call rather than the wake-up notification), refers to the thread A calls object O wait () method to enter the wait state, and another thread calls the O's notify () or notifyAll () method, after receiving the notification thread a () method returns the object O from the wait, and further subsequent operations.

上诉过程是通过对象O,使得线程A和线程B之间进行通信, 在线程中调用了对象O的wait()方法后线程久进入了阻塞状态,而在其他线程中对象O调用notify()或notifyAll方法时,则会唤醒对应的阻塞线程。

2.2 基本API

等待/唤醒机制的相关方法时任意Java对象具备的,因为这些方法被定义在所有Java对象的超类Object中。

notify: 通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提时该线程获取到对象的锁

notifyAll: 通知所有等待在该对象上的线程

wait: 调用此方法的线程进入阻塞等待状态,只有等待另外线程的通知或者被中断才会返回,调用wait方法会释放对象的锁

wait(long) : 等待超过一段时间没有被唤醒就超时自动返回,单位时毫秒。

2.3 用等待唤醒机制优化循环等待

public static void main(String[] args) {
   // 是否还有包子
        AtomicBoolean hasBun = new AtomicBoolean();
        // 锁对象
        Object lockObject = new Object();

        // 包子铺老板
        new Thread(() -> {
            try {
                while (true) {
                    synchronized (lockObject) {
                        if (hasBun.get()) {
                            System.out.println("老板:包子够卖了,打一把王者荣耀");
                            lockObject.wait(); 
                        } else {
                            System.out.println("老板:没有包子了, 马上开始制作...");
                            Thread.sleep(3000);
                            System.out.println("老板:包子出锅咯....");
                            hasBun.set(true);
                            // 通知等待的食客
                            lockObject.notifyAll();
                        }
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();


        new Thread(() -> {
            System.out.println("小强:我要买包子...");
            try {
                synchronized (lockObject) {
                    if (!hasBun.get()) {
                        System.out.println("小强:看一下有没有做好, 看公众号cruder有没有新文章");
                        lockObject.wait(); 
                    } else {
                        System.out.println("小强:包子终于做好了,我要吃光它们....");
                        hasBun.set(false);
                        lockObject.notifyAll();
                        System.out.println("小强:一口气把店里包子吃光了, 快快乐乐去板砖了~~");
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
 }

上述流程,减少了轮询检查的操作,并且线程调用wait()方法后,会释放锁,不会消耗CPU资源,进而提高了程序的性能。

三,等待唤醒机制的基本范式

等待、唤醒是线程间通信的手段之一,用来协调多个线程操作同一个数据源。实际应用中通常用来优化循环等待的问题,针对等待方和通知方,可以提炼出如下的经典范式。

需要注意的是,在等待方执行的逻辑中,一定要用while循环来判断等待条件,因为执行notify/notifyAll方法时只是让等待线程从wait方法返回,而非重新进入临界区

/**
 * 等待方执行的逻辑
 * 1. 获取对象的锁
 * 2. 检查条件,如果条件不满足,调用对象的wait方法,被通知后重新检查条件
 * 3. 条件满足则执行对应的逻辑
 */
synchronized(对象){
    while(条件不满足){
        对象.wait()
    }
    doSomething();
}
/**
 * !! 通知方执行的逻辑
 * 1. 获取对象的锁
 * 2. 改变条件
 * 3. 通知(所有)等待在对象上的线程
 */
synchronized(对象){
    条件改变
    对象.notify();
}

这个编程范式通常是针对典型的通知方和等待方,有时双方可能具有双重身份,即使等待方又是通知方,正如我们上文中的案例一样。

四,notify/notifyAll不释放锁

相信这个问题有半数工程师都不知道,当执行wait()方法,锁自动被释放;但执行完notify()方法后,锁不会释放,而是要执行notify()方法所在的synchronized代码块后才会释放。这一点很重要,也是很多工程师容易忽略的地方。

lockObject.notifyAll();
System.out.println("小强:一口气把店里包子吃光了, 快快乐乐去板砖了~~");

案例代码中,故意设置成先notifyAll,然后在打印;上文图中的结果也印证了了我们的描述,感兴趣的小伙伴可以动手执行一下案例代码哦。

五,等待、唤醒必须先获取锁

在等待、唤醒编程范式中的wait,notify,notifyAll方法往往不能直接调用, 需要在获取锁之后的临界区执行

并且只能唤醒等待在同一把锁上的线程。

当线程调用wait方法时会被加入到一个等待队列,当执行notify时会唤醒队列中第一个等待线程(等待时间最长的线程),而调用notifyAll时则会唤醒等待线程中所有的等待线程

六,sleep不释放锁 而wait 释放

在用等待唤醒机制优化循环等待的过程中,有一个重要的特征就是原本的sleep()方法用wait()方法取代,他们的最大的区别在于wait方法会释放锁,而sleep不会,除此之外,还有个重要的区别,sleep是Thread的方法,可以在任意地方执行;而wait是Object对象的方法,必须在synchronized代码块中执行

Guess you like

Origin www.cnblogs.com/liqiangchn/p/12038007.html