分析java并发中的wait notify notifyAll

一、前言

java 面试是否有被问到过,sleepwait 方法的区别,关于这个问题其实不用多说,大多数人都能回答出最主要的两点区别:

  • sleep 是线程的方法, wait / notify / notifyAll 是 Object 类的方法;
  • sleep 不会释放当前线程持有的锁,到时间后程序会继续执行,wait 会释放线程持有的锁并挂起,直到通过 notify 或者 notifyAll 重新获得锁。

另外还有一些参数、异常等区别,不细说了。本文重点记录一下 wait / notify / notifyAll 的相关知识。

二、常见的同步场景

开发中常常遇到这样的场景:

一个线程执行过程中,需要开启另外一个子线程去做某个耗时的操作(通过休眠3秒模拟),并且**等待**子线程返回结果,主线程再根据返回的结果继续往下执行。

这里注意我上面加*两个字“等待”。如果不需要等待,单纯只是对子线程的结果做处理,我们大可注册回调方法解决问题,此文不再赘述接口回调。

此处场景就是主线程停下来等待子线程执行完毕后,主线程再继续执行。针对该场景下面给出实现:

2.1、设置一个判断的标志位

volatile boolean flag = false;

public void test(){
    //...

    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(3000);
            System.out.println("--- 休眠 3 秒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            flag = true;
        }
    });
    t1.start();

    while(!flag){

    }
    System.out.println("--- work thread run");
}

上面的代码,执行结果:

强调一点,声明标志位的时候,一定注意 volatile 关键字不能忘,如果不加该关键字修饰,程序可能进入死循环。这是同步中的可见性问题,在 《java 并发——内置锁》 中有记录。

显然,这个实现方案并不好,本来主线程什么也不用做,却一直在竞争资源,做空循环,性能上不好,所以并不推荐。

2.2、线程的 join 方法

public void test(){
    //...

    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(3000);
            System.out.println("--- 休眠 3 秒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });

    t1.start();

    try {
        t1.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("--- work thread continue");
}

上面的代码,执行结果同上。利用 Thread 类的 join 方法实现了同步,达到了效果,但是 join 方法不能一定保证效果,在不同的 cpu 上,可能呈现出意想不到的结果,所以尽量不要用上述方法。

2.3、使用闭锁 CountDownLatch

不清楚闭锁的新同学可以了解下java并发中的线程。

public void test(){
    //...

    final CountDownLatch countDownLatch = new CountDownLatch(1);

    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(3000);
            System.out.println("--- 休眠 3 秒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            countDownLatch.countDown();
        }
    });

    t1.start();

    try {
        countDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("--- work thread run");
}

上面的代码,执行结果同上。同样可以实现上述效果,执行结果和上面一样。该方法推荐使用。

2.4、利用 wait / notify 优化标志位方法

为了方便对比,首先给 2.1 中的循环方法增加一些打印。修改后的代码如下:

volatile boolean flag = false;

public void test() {
    //...
    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(3000);
            System.out.println("--- 休眠 3 秒");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            flag = true;
        }
    });
    t1.start();

    while (!flag) {
        try {
            System.out.println("---while-loop---");
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    System.out.println("--- work thread run");
}

执行结果如下:

事实证明,while 循环确实一直在执行。

为了使该线程再不需要执行的时候不抢占资源,我们可以利用 wait 方法将其挂起,在需要它执行的时候,再利用 notify 方法将其唤醒。这样达到优化的目的,优化后的代码如下:

volatile boolean flag = false;

public void test() {
    //...
    final Object obj = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (obj) {
            try {
                Thread.sleep(3000);
                System.out.println("--- 休眠 3 秒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                flag = true;
            }
            obj.notify();
        }
    });
    t1.start();

    synchronized (obj) {
        while (!flag) {
            try {
                System.out.println("---while-loop---");
                Thread.sleep(500);
                obj.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    System.out.println("--- work thread run");

}

执行结果:

结果证明,优化后的程序,循环只执行了一次。

三、理解 wait / notify / notifyAll

在Java中,每个对象都有两个池,锁(monitor)池和等待池

3.1、锁池

锁池:假设线程A已经拥有了某个对象的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

3.2、等待池

等待池:假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.

3.3、notify 和 notifyAll 的区别

3.3.1、wait()

public final void wait() throws InterruptedException,IllegalMonitorStateException
该方法用来将当前线程置入休眠状态,直到接到通知或被中断为止。在调用 wait()之前,线程必须要获得该对象的对象级别锁,即只能在同步方法或同步块中调用 wait()方法。进入 wait()方法后,当前线程释放锁。在从 wait()返回前,线程与其他线程竞争重新获得锁。如果调用 wait()时,没有持有适当的锁,则抛出 IllegalMonitorStateException,它是 RuntimeException 的一个子类,因此,不需要 try-catch 结

3.3.2、notify()

public final native void notify() throws IllegalMonitorStateException
该方法也要在同步方法或同步块中调用,即在调用前,线程也必须要获得该对象的对象级别锁,的如果调用 notify()时没有持有适当的锁,也会抛出 IllegalMonitorStateException。
该方法用来通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个 wait()状态的线程来发出通知,并使它等待获取该对象的对象锁(notify 后,当前线程不会马上释放该对象锁,wait 所在的线程并不能马上获取该对象锁,要等到程序退出 synchronized 代码块后,当前线程才会释放锁,wait所在的线程也才可以获取该对象锁),但不惊动其他同样在等待被该对象notify的线程们。当第一个获得了该对象锁的 wait 线程运行完毕以后,它会释放掉该对象锁,此时如果该对象没有再次使用 notify 语句,则即便该对象已经空闲,其他 wait 状态等待的线程由于没有得到该对象的通知,会继续阻塞在 wait 状态,直到这个对象发出一个 notify 或 notifyAll。这里需要注意:它们等待的是被 notify 或 notifyAll,而不是锁。这与下面的 notifyAll()方法执行后的情况不同。

3.3.3、notifyAll()

public final native void notifyAll() throws IllegalMonitorStateException

该方法与 notify ()方法的工作方式相同,重要的一点差异是:

notifyAll 使所有原来在该对象上 wait 的线程统统退出 wait 的状态(即全部被唤醒,不再等待 notify 或 notifyAll,但由于此时还没有获取到该对象锁,因此还不能继续往下执行),变成等待获取该对象上的锁,一旦该对象锁被释放(notifyAll 线程退出调用了 notifyAll 的 synchronized 代码块的时候),他们就会去竞争。如果其中一个线程获得了该对象锁,它就会继续往下执行,在它退出 synchronized 代码块,释放锁后,其他的已经被唤醒的线程将会继续竞争获取该锁,一直进行下去,直到所有被唤醒的线程都执行完毕。

四、生产者与消费者模式

生产者与消费者问题是并发编程里面的经典问题。接下来说说利用wait()和notify()来实现生产者和消费者并发问题:
显然要保证生产者和消费者并发运行不出乱,主要要解决:当生产者线程的缓存区为满的时候,就应该调用wait()来停止生产者继续生产,而当生产者满的缓冲区被消费者消费掉一块时,则应该调用notify()唤醒生产者,通知他可以继续生产;同样,对于消费者,当消费者线程的缓存区为空的时候,就应该调用wait()停掉消费者线程继续消费,而当生产者又生产了一个时就应该调用notify()来唤醒消费者线程通知他可以继续消费了。

下面是一个简单的代码实现:

package com.sharpcj;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test {
    public static void main(String[] args) {
        Reposity reposity = new Reposity(600);
        ExecutorService threadPool = Executors.newCachedThreadPool();
        for(int i = 0; i < 10; i++){
            threadPool.submit(new Producer(reposity));
        }

        for(int i = 0; i < 10; i++){
            threadPool.submit(new Consumer(reposity));
        }
        threadPool.shutdown();
    }
}

class Reposity {
    private static final int MAX_NUM = 2000;
    private int currentNum;

    private final Object obj = new Object();

    public Reposity(int currentNum) {
        this.currentNum = currentNum;
    }

    public void in(int inNum) {
        synchronized (obj) {
            while (currentNum + inNum > MAX_NUM) {
                try {
                    System.out.println("入货量 " + inNum + " 线程 " + Thread.currentThread().getId() + "被挂起...");
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            currentNum += inNum;
            System.out.println("线程: " + Thread.currentThread().getId() + ",入货:inNum = [" + inNum + "], currentNum = [" + currentNum + "]");
            obj.notifyAll();
        }
    }

    public void out(int outNum) {
        synchronized (obj) {
            while (currentNum < outNum) {
                try {
                    System.out.println("出货量 " + outNum + " 线程 " + Thread.currentThread().getId() + "被挂起...");
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            currentNum -= outNum;
            System.out.println("线程: " + Thread.currentThread().getId() + ",出货:outNum = [" + outNum + "], currentNum = [" + currentNum + "]");
            obj.notifyAll();
        }
    }
}

class Producer implements Runnable {
    private Reposity reposity;

    public Producer(Reposity reposity) {
        this.reposity = reposity;
    }

    @Override
    public void run() {
        reposity.in(200);
    }
}

class Consumer implements Runnable {
    private Reposity reposity;

    public Consumer(Reposity reposity) {
        this.reposity = reposity;
    }

    @Override
    public void run() {
        reposity.out(200);
    }
}

执行结果:

五、总结

1.调用wait方法和notify、notifyAll方法前必须获得对象锁,也就是必须写在synchronized(锁对象){......}代码块中。

2.当线程调用了wait方法后就释放了对象锁,否则其他线程无法获得对象锁。

3.当调用 wait() 方法后,线程必须再次获得对象锁后才能继续执行。

4.如果另外两个线程都在 wait,则正在执行的线程调用notify方法只能唤醒一个正在wait的线程(公平竞争,由JVM决定)。

5.当使用notifyAll方法后,所有wait状态的线程都会被唤醒,但是只有一个线程能获得锁对象,必须执行完while(condition){this.wait();}后才释放对象锁。其余的需要等待该获得对象锁的线程执行完释放对象锁后才能继续执行。

6.当某个线程调用notifyAll方法后,虽然其他线程被唤醒了,但是该线程依然持有着对象锁,必须等该同步代码块执行完(右大括号结束)后才算正式释放了锁对象,另外两个线程才有机会执行。

7.第5点中说明, wait 方法的调用前的条件判断需放在循环中,否则可能出现逻辑错误。另外,根据程序逻辑合理使用 wait 即 notify 方法,避免如先执行 notify ,后执行 wait 方法,线程一直挂起之类的错误。

以上就是分析java并发中的wait notify notifyAll的详细内容,更多关于java并发wait notify notifyAll的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java 中Object的wait() notify() notifyAll()方法使用

    Java 中Object的wait() notify() notifyAll()方法使用 一.前言 对于并发编程而言,除了Thread以外,对Object对象的wati和notify对象也应该深入了解其用法,虽然知识点不多. 二.线程安全基本知识 首先应该记住以下基本点,先背下来也无妨: 同一时间一个锁只能被一个线程持有 调用对象的wait()和notify()前必须持有它 三.wait()和notify()理解 3.1 wait()和notify()方法简介 wait()和notify()都是

  • java多线程之wait(),notify(),notifyAll()的详解分析

    wait(),notify(),notifyAll()不属于Thread类,而是属于Object基础类,也就是说每个对象都有wait(),notify(),notifyAll()的功能.因为每个对象都有锁,锁是每个对象的基础,当然操作锁的方法也是最基础了. wait导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyAll() 方法,或被其他线程中断.wait只能由持有对像锁的线程来调用. notify唤醒在此对象监视器上等待的单个线程.如果所有线程都在此对象上等

  • Java的wait(), notify()和notifyAll()使用心得

    wait(),notify()和notifyAll()都是java.lang.Object的方法:wait(): Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object.notify(): Wakes up a single thread that is waiting on this object's

  • Java多线程基础 线程的等待与唤醒(wait、notify、notifyAll)

    本篇我们来研究一下 wait() notify() notifyAll() . DEMO1: wait() 与 notify() public class Test { static class ThreadOne extends Thread { private Callback mCallback; @Override public void run() { work(); if (mCallback != null) { mCallback.onResult(false); } } //

  • Java通过wait()和notifyAll()方法实现线程间通信

    本文实例为大家分享了Java实现线程间通信的具体代码,供大家参考,具体内容如下 Java代码(使用了2个内部类): package Threads; import java.util.LinkedList; /** * Created by Frank */ public class ProdCons { protected LinkedList<Object> list = new LinkedList<>(); protected int max; protected bool

  • Java常见面试题之多线程和高并发详解

    volatile 对 volatile的理解 volatile 是一种轻量级的同步机制. 保证数据可见性 不保证原子性 禁止指令重排序 JMM JMM(Java 内存模型)是一种抽象的概念,描述了一组规则或规范,定义了程序中各个变量的访问方式. JVM运行程序的实体是线程,每个线程创建时 JVM 都会为其创建一个工作内存,是线程的私有数据区域.JMM中规定所有变量都存储在主内存,主内存是共享内存.线程对变量的操作在工作内存中进行,首先将变量从主内存拷贝到工作内存,操作完成后写会主内存.不同线程间

  • Java多线程中wait、notify、notifyAll使用详解

    基础知识 首先我们需要知道,这几个都是Object对象的方法.换言之,Java中所有的对象都有这些方法. public final native void notify(); public final native void notifyAll(); public final native void wait(long timeout) throws InterruptedException; public final void wait() throws InterruptedExceptio

  • java 并发编程之共享变量的实现方法

    可见性 如果一个线程对共享变量值的修改, 能够及时的被其他线程看到, 叫做共享变量的可见性. Java 虚拟机规范试图定义一种 Java 内存模型 (JMM), 来屏蔽掉各种硬件和操作系统的内存访问差异, 让 Java 程序在各种平台上都能达到一致的内存访问效果. 简单来说, 由于 CPU 执行指令的速度是很快的, 但是内存访问的速度就慢了很多, 相差的不是一个数量级, 所以搞处理器的那群大佬们又在 CPU 里加了好几层高速缓存. 在 Java 内存模型里, 对上述的优化又进行了一波抽象. JM

  • 分析java并发中的wait notify notifyAll

    一.前言 java 面试是否有被问到过,sleep 和 wait 方法的区别,关于这个问题其实不用多说,大多数人都能回答出最主要的两点区别: sleep 是线程的方法, wait / notify / notifyAll 是 Object 类的方法: sleep 不会释放当前线程持有的锁,到时间后程序会继续执行,wait 会释放线程持有的锁并挂起,直到通过 notify 或者 notifyAll 重新获得锁. 另外还有一些参数.异常等区别,不细说了.本文重点记录一下 wait / notify

  • Java多线程中的wait/notify通信模式实例详解

    前言 最近在看一些JUC下的源码,更加意识到想要学好Java多线程,基础是关键,比如想要学好ReentranLock源码,就得掌握好AQS源码,而AQS源码中又有很多Java多线程经典的一些应用:再比如看了线程池的核心源码实现,又学到了很多核心实现,其实这些都可以提出来慢慢消化并变成自己的知识点,今天这个Java等待/通知模式其实是Thread.join()实现的关键,还有线程池工作线程中线程跟线程之间的通信的核心所在,故在此为了加深理解,做此记录! 参考资料<Java并发编程艺术>(电子PD

  • java 并发中的原子性与可视性实例详解

    java 并发中的原子性与可视性实例详解 并发其实是一种解耦合的策略,它帮助我们把做什么(目标)和什么时候做(时机)分开.这样做可以明显改进应用程序的吞吐量(获得更多的CPU调度时间)和结构(程序有多个部分在协同工作).做过java Web开发的人都知道,Java Web中的Servlet程序在Servlet容器的支持下采用单实例多线程的工作模式,Servlet容器为你处理了并发问题. 原子性 原子是世界上的最小单位,具有不可分割性.比如 a=0:(a非long和double类型) 这个操作是不

  • 聊聊Java并发中的Synchronized

    1 引言 在多线程并发编程中Synchronized一直是元老级角色,很多人都会称呼它为重量级锁,但是随着Java SE1.6对Synchronized进行了各种优化之后,有些情况下它并不那么重了,本文详细介绍了Java SE1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程. 2 术语定义 术语 英文 说明 CAS Compare and Swap 比较并设置.用于在硬件层面上提供原子性操作.在 Intel 处理器中,比较并交换通过指令cmpxch

  • java并发中DelayQueue延迟队列原理剖析

    介绍 DelayQueue队列是一个延迟队列,DelayQueue中存放的元素必须实现Delayed接口的元素,实现接口后相当于是每个元素都有个过期时间,当队列进行take获取元素时,先要判断元素有没有过期,只有过期的元素才能出队操作,没有过期的队列需要等待剩余过期时间才能进行出队操作. 源码分析 DelayQueue队列内部使用了PriorityQueue优先队列来进行存放数据,它采用的是二叉堆进行的优先队列,使用ReentrantLock锁来控制线程同步,由于内部元素是采用的Priority

  • 分析Java并发编程之信号量Semaphore

    目录 一.认识Semaphore 1.1.Semaphore 的使用场景 1.2.Semaphore 使用 1.3.Semaphore 信号量的模型 二.Semaphore 深入理解 2.1.Semaphore 基本属性 2.2.Semaphore 的公平性和非公平性 2.3.其他 Semaphore 方法 一.认识Semaphore 1.1.Semaphore 的使用场景 Semaphore 的使用场景主要用于流量控制,比如数据库连接,同时使用的数据库连接会有数量限制,数据库连接不能超过一定的

  • 浅析java并发中的Synchronized关键词

    如果在多线程的环境中,我们经常会遇到资源竞争的情况,比如多个线程要去同时修改同一个共享变量,这时候,就需要对资源的访问方法进行一定的处理,保证同一时间只有一个线程访问. java提供了synchronized关键字,方便我们实现上述操作. 为什么要同步 我们举个例子,我们创建一个类,提供了一个setSum的方法: public class SynchronizedMethods { private int sum = 0; public void calculate() { setSum(get

  • JAVA并发中VOLATILE关键字的神奇之处详解

    并发编程中的三个概念: 1.原子性 在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要么执行,要么不执行. 2.可见性 对于可见性,Java提供了volatile关键字来保证可见性. 当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值. 而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保

  • Java并发中的Fork/Join 框架机制详解

    什么是 Fork/Join 框架 Fork/Join 框架是一种在 JDk 7 引入的线程池,用于并行执行把一个大任务拆成多个小任务并行执行,最终汇总每个小任务结果得到大任务结果的特殊任务.通过其命名也很容易看出框架主要分为 Fork 和 Join 两个阶段,第一阶段 Fork 是把一个大任务拆分为多个子任务并行的执行,第二阶段 Join 是合并这些子任务的所有执行结果,最后得到大任务的结果. 这里不难发现其执行主要流程:首先判断一个任务是否足够小,如果任务足够小,则直接计算,否则,就拆分成几个

  • 浅谈Java并发中ReentrantLock锁应该怎么用

    目录 1.重入锁 说明 2.中断响应 说明 3.锁申请等待限时 tryLock(long, TimeUnit) tryLock() 4.公平锁 说明 源码(JDK8) 重入锁可以替代关键字 synchronized . 在 JDK5.0 的早期版本中,重入锁的性能远远优于关键字 synchronized , 但从 JDK6.0 开始, JDK 在关键字 synchronized 上做了大量的优化,使得两者的性能差距并不大. 重入锁使用 ReentrantLock 实现 1.重入锁 package

随机推荐