zoukankan      html  css  js  c++  java
  • 廖雪峰Java11多线程编程-2线程同步-4wait和notify

    1.多线程协调

    synchronized解决了多线程竞争的问题,我们可以在synchronized块中安全的对一个变量进行修改,但是它没有解决多线程协调的问题。
    例如设计一个TaskQueue,内部通过LinkedList<>()表示一个队列。addTask()将新任务放入队列,getTask()取出队列中的第一个任务。
    下面这段代码的问题:当线程执行到getTask()内部的时候,如果队列为空,会导致死循环。该线程将会100%的占用CPU,而其他线程根本不能调用addTask()方法。这是因为此时对象的this锁已经被正在执行死循环的线程获得且不会释放。

    class TaskQueue{
        Queue<String> queue = new LinkedList<>();
        public synchronized void addTask(String s){    //线程1不能执行,因为操作对象已经被线程2锁住了
            this.queue.add(s);
        }
        public synchronized String getTask(){
            while(queue.isEmpty()){   } //线程2执行getTask()时,如果队列为空,陷入死循环
            return queue.remove();
        }
    }
    

    预期效果:

    • 线程1通过addTask()不断往队列中添加任务,而线程2可以调用getTask()从队列中获取任务。
    • 如果队列为空,则getTask()应该等待,直到队列中至少有一个任务再执行getTask()。

      因此我们需要一种多线程协调的机制:当条件不满足时,线程进入等待状态。

    2.wait

    当一个线程执行getTask()时:

    • 首先获取锁。
    • 然后执行while条件判断:条件符合,进入等待状态。注意:进入等待状态以后,wait()方法不会返回,直到某个时刻,线程从等待状态被其他线程唤醒以后,wait()方法才会返回。
    • 之后线程继续执行下一条语句。

    wait()方法的执行机制:

    • 首先,wait()不是一个普通的Java方法,而是定义在Object类上面的一个native方法,即是由JVM虚拟机的C代码实现的。
    • 其次,必须在synchronized代码块中才能调用wait()方法,因为wait()方法调用的时候,线程会释放它获得的锁,wait()方法返回后,线程又回重新获得锁。我们只能在锁对象调用wait()方法。此处我们使用synchronized修饰方法,所以我们获得对象是this对象。因此只能在this对象上调用wait()方法
    • 正是因为wait()方法会释放锁,所以其他的线程才能获得锁,并且进入addTask()方法。在addTask()方法内,我们向队列添加一个元素以后,就可以调用this.notify()来唤醒正在this对象上等待的线程,这样在wait()方法上等待的线程就可以被唤醒,然后从wait()方法返回以后继续执行。

    完整的wait/notify机制:

    3.示例

    import java.util.LinkedList;
    import java.util.Queue;
    
    class TaskQueue{
        final Queue<String> queue = new LinkedList<>(); //定义一个LinkedList作为queue
        public synchronized String getTask() throws InterruptedException{
            System.out.println("开始执行");
            while(this.queue.isEmpty()){ //判断queue是否是空,如果空,就释放对象锁,进入等待状态。
                this.wait();
                System.out.println("继续执行");
            }
            return queue.remove();//删除queue最上面的值
        }
        public synchronized void addTask(String name){
            this.queue.add(name); //向queue添加一个任务
            this.notifyAll(); //调用notifyALL()唤醒所有正在等待的线程
            System.out.println("唤醒线程");
        }
    }
    class WorkerThread extends Thread {
        TaskQueue taskQueue;
        public WorkerThread(TaskQueue taskqueue){
            this.taskQueue = taskqueue;
        }
        public void run(){
            while(!isInterrupted()){ //run()方法不断的执行getTask,获取taskQueue中的任务,一旦获取到,就打印hello name
                String name;
                try{
                    name = taskQueue.getTask();
                }catch (InterruptedException e){
                    break;
                }
                String result = "Hello, "+name+"!";
                System.out.println(result);
            }
        }
    }
    public class Main{
        public static void main(String[] args) throws Exception{
            TaskQueue taskqueue = new TaskQueue();
            WorkerThread worker = new WorkerThread(taskqueue);
            worker.start();
            taskqueue.addTask("Bob");
            Thread.sleep(1000);
            taskqueue.addTask("Alice");
            Thread.sleep(1000);
            taskqueue.addTask("Tim");
            Thread.sleep(1000);
            worker.interrupt();//对worker线程调用interrupted方法让它中断
            worker.join();//调用join()方法等待worker线程的结束
            System.out.println("End");
        }
    }
    
    ## 4.问题 ### 4.1为什么在while循环wait,而不是一个if语句中wait ? 真是因为很有可能有2个或者更多的线程在wait(),当它们从wait返回的时候,只有1个线程有机会获得this锁。这个线程会正确执行queue.remove()方法。 但是如果我们用if语句,当这个线程执行完毕以后,其他线程获得this锁以后继续执行remove方法,可能会因为队列为空而报错,其他线程又需要重新判断条件并可能再次进入wait状态。 所以通常在while循环中调用wait方法。 唤醒当前线程的不一定是执行添加队列的线程1。当非添加队列的线程1唤醒获取队列的线程2时: * 使用while可以判断当前队列是否为空, * 使用if继续执行,如果队列为空,执行remove方法会报错

    4.2 为什么用notifyAll(),而不是notify()

    因为notify只唤醒某一个等待的线程,而notifyAll会唤醒全部的等待线程。通常来说,notifyAll更安全。
    有些时候如果我们的代码逻辑考虑不周,用notify会唤醒一个线程,而其他线程可能会永远等待下去,就醒不过来了。
    所以要正确的编写多线程代码是非常困难的,需要考虑的条件非常多。

    4.3 这个TaskQueue有什么意义呢

    如果我们在编写一个浏览器,当我们解析html的时候,每遇到一个图片链接,我们就可以把链接放到TaskQueue中,而worker线程就可以不断的从Queue中取出链接,然后把print语句替换为下载图片的代码。这个程序可以看成它能够实现后台线程顺序下载多个图片的功能。

    5.总结

    • 在synchronized内部可以调用wait()是线程进入等待状态
    • 必须在已获得的锁对象上调用wait()方法
    • 在synchronized内部可以调用notify/notifyAll()唤醒其他等待线程
    • 必须在已获得的锁对象上调用notify()/notifyAll()方法
  • 相关阅读:
    生成器,迭代器
    装饰器
    作业修改配置文件 查询,添加
    continue 和 break的实例
    作业,修改haproxy配置文件
    zabbix分布式部署
    zabbix全网监控介绍
    nginx+tomcat9+memcached-session-manager会话共享
    tomcat管理登陆界面无法进行登陆
    JAVA与tomcat部署
  • 原文地址:https://www.cnblogs.com/csj2018/p/11005544.html
Copyright © 2011-2022 走看看