Java并发 线程间的等待与通知

前言:

前面讲完了一些并发编程的原理,现在我们要来学习的是线程之间的协作。通俗来说就是,当前线程在某个条件下需要等待,不需要使用太多系统资源。在某个条件下我们需要去唤醒它,分配给它一定的系统资源,让它继续工作。这样能更好的节约资源。

一、Object的wait()与notify()

基本概念:

一个线程因执行目标动作的条件未能满足而被要求暂停就是wait,而一个线程满足执行目标动作的条件之后唤醒被暂停的线程就是notify。

基本模板:

synchronized (obj){
      //保护条件不成立
      while(flag){
        //暂停当前线程
        obj.wait();
      }
      //当保护条件成立,即跳出while循环执行目标动作
      doAction();
    }

解析wait():Object.wait()的作用是使执行线程被暂停,该执行线程生命周期就变更为WAITING,这里注意一下,是无限等待,直到有notify()方法通知该线程唤醒。Object.wait(long timeout)的作用是使执行线程超过一定时间没有被唤醒就自动唤醒,也就是超时等待。Object.wait(long timeout,int naous)是更加精准的控制时间的方法,可以控制到毫微秒。这里需要注意的是wait()会在当前线程拥有锁的时候才能执行该方法并且释放当前线程拥有的锁,从而让该线程进入等待状态,其他线程来尝试获取当前锁。也就是需要申请锁与释放锁。

解析notify():Object.notify()方法是唤醒调用了wait()的线程,只唤醒最多一个。如果有多个线程,不一定能唤醒我们所想要的线程。Object.notifyAll()唤醒所有等待的线程。notify方法一定是通知线程先获取到了锁才能进行通知。通知之后当前的通知线程需要释放锁,然后由等待线程来获取。所以涉及到了一个申请锁与释放锁的步骤。

wait()与notify()之间存在的三大问题:

从上面的解析可以看出,notify()是无指向性的唤醒,notifyAll()是无偏差唤醒。所以会产生下面三个问题

过早唤醒:假设当前有三组等待(w1,w2,w3)与通知(n1,n2,n3)线程同步在对象obj上,w1,w2的判断唤醒条件相同,由线程n1更新条件并唤醒,w3的判断唤醒条件不同,由n2,n3更新条件并唤醒,这时如果n1执行了唤醒,那么不能执行notify,因为需要叫醒两条线程,只能用notifyAll(),可是用了之后w3的条件未能满足就被叫醒,就需要一直占用资源的去等待执行。

信号丢失:这个问题主要是程序员编程出现了问题,并不是内部实现机制出现的问题。编程时如果在该使用notifyAll()的地方使用notify()那么只能唤醒一个线程,从而使其他应该唤醒的线程未能唤醒,这就是信号丢失。如果等待线程在执行wait()方法前没有先判断保护条件是否成立,就会出现通知线程在该等待线程进入临界区之前就已经更新了相关共享变量,并且执行了notify()方法,但是由于wait()还未能执行,且没有设置共享变量的判断,所以会执行wait()方法,导致线程一直处于等待状态,丢失了一个信号。

欺骗性唤醒:等待线程并不是一定有notify()/notifyAll()才能被唤醒,虽然出现的概率特别低,但是操作系统是允许这种情况发生的。

上下文切换问题:首先wait()至少会导致线程对相应对象内部锁的申请与释放。notify()/notifyAll()时需要持有相应的对象内部锁并且也会释放该锁,会出现上下文切换问题其实就是从RUNNABLE状态变为非RUNNABLE状态会出现。

针对问题的解决方案:

信号丢失与欺骗性唤醒问题:都可以使用while循环来避免,也就是上面的模板中写的那样。

上下文切换问题:在保证程序正确性的情况下使用notify()代替notifyAll(),notify不会导致过早唤醒,所以减少了上下文的切换。并且使用了notify之后应该尽快释放相应内部锁,从而让wait()能够更快的申请到锁。

过早唤醒:使用java.util.concurrent.locks.Condition中的await与signal。

PS:由于Object中的wait与notify使用的是native方法,即C++编写,这里不做源码解析。

二、Condition中的await()与signal()

这个方法相应的改变了上面所说的无指向性的问题,每个Condition内部都会维护一个队列,从而让我们对线程之间的操作更加灵活。下面通过分析源码让我们了解一下内部机制。Condition是个接口,真正的实现是AbstractQueuedSynchronizer中的内部类ConditionObject。

基本属性:

public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    /** First node of condition queue. */
    private transient Node firstWaiter;
    /** Last node of condition queue. */
    private transient Node lastWaiter;
}

从基本属性中可看出维护的是双端队列。

await()方法解析:

public class ConditionObject implements Condition, java.io.Serializable {
  public final void await() throws InterruptedException {
   // 1. 判断线程是否中断
  if(Thread.interrupted()){
    throw new InterruptedException();
  }
   // 2. 将线程封装成一个 Node 放到 Condition Queue 里面
  Node node = addConditionWaiter();
   // 3. 释放当前线程所获取的所有的锁 (PS: 调用 await 方法时, 当前线程是必须已经获取了独占的锁)
  int savedState = fullyRelease(node);
  int interruptMode = 0;
   // 4. 判断当前线程是否在 Sync Queue 里面(这里 Node 从 Condtion Queue 里面转移到 Sync Queue 里面有两种可能    //(1) 其他线程调用 signal 进行转移 (2) 当前线程被中断而进行Node的转移(就在checkInterruptWhileWaiting里面进行转移))
  while(!isOnSyncQueue(node)){
     // 5. 当前线程没在 Sync Queue 里面, 则进行 block
    LockSupport.park(this);
     // 6. 判断此次线程的唤醒是否因为线程被中断, 若是被中断, 则会在checkInterruptWhileWaiting的transferAfterCancelledWait 进行节点的转移;     if((interruptMode = checkInterruptWhileWaiting(node)) != 0){
     // 说明此是通过线程中断的方式进行唤醒, 并且已经进行了 node 的转移, 转移到 Sync Queue 里面
      break;
    }
  }
   // 7. 调用 acquireQueued在 Sync Queue 里面进行独占锁的获取, 返回值表明在获取的过程中有没有被中断过
  if(acquireQueued(node, savedState) && interruptMode != THROW_IE){
    interruptMode = REINTERRUPT;
  }
   // 8. 通过 "node.nextWaiter != null" 判断 线程的唤醒是中断还是 signal。   //因为通过中断唤醒的话, 此刻代表线程的 Node 在 Condition Queue 与 Sync Queue 里面都会存在
  if(node.nextWaiter != null){
     // 9. 进行 cancelled 节点的清除
    unlinkCancelledWaiters();
  }
   // 10. "interruptMode != 0" 代表通过中断的方式唤醒线程
  if(interruptMode != 0){
     // 11. 根据 interruptMode 的类型决定是抛出异常, 还是自己再中断一下
    reportInterruptAfterWait(interruptMode);
  }
  }
}

上面源代码可看出Condition内部维护的队列是一个等待队列,当需要调用signal()方法时就会让当前线程节点从Condition queue转到Sync queue队列中去竞争锁从而唤醒。

signal()源码解析:

public class ConditionObject implements Condition, java.io.Serializable {
  public final void signal() {
      if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
      Node first = firstWaiter;
      if (first != null)
        doSignal(first);
    }
  private void doSignal(Node first) {
      do {
        //传入的链表下一个节点为空,则尾节点置空
        if ( (firstWaiter = first.nextWaiter) == null)
          lastWaiter = null;
        //当前节点的下一个节点为空
        first.nextWaiter = null;
        //如果成功将node从condition queue转换到sync queue,则退出循环,节点为空了也退出循环。否则就接着在队列中找寻节点进行唤醒
      } while (!transferForSignal(first) &&
           (first = firstWaiter) != null);
    }
}

signal()会使等待队列中的一个任意线程被唤醒,signalAll()则是唤醒该队列中的所有线程。这样通过不同队列维护不同线程,就可以达到指向性的功能。可以消除由过早唤醒带来的资源损耗。注意的是在使用signal()方法前需要获取锁,即lock(),而后需要尽快unlock(),这样可以避免上下文切换的损耗。

总结:

面向对象的世界中,一个类往往需要借助其他的类来一起完成计算,同样线程的世界也是,多个线程可以同时完成一个任务,通过唤醒与等待,能更好的操作线程,从而让线程在需要使用资源的时候分配资源给它,而不使用资源的时候就可以将资源让给其他线程操作。关于Condition中提到的Sync queue可参考Java并发 结合源码分析AQS原理来看内部维护的队列是如何获取锁的。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

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

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

  • Java多线程并发开发之DelayQueue使用示例

    在学习Java 多线程并发开发过程中,了解到DelayQueue类的主要作用:是一个无界的BlockingQueue,用于放置实现了Delayed接口的对象,其中的对象只能在其到期时才能从队列中取走.这种队列是有序的,即队头对象的延迟到期时间最长.注意:不能将null元素放置到这种队列中. Delayed,一种混合风格的接口,用来标记那些应该在给定延迟时间之后执行的对象.此接口的实现必须定义一个 compareTo 方法,该方法提供与此接口的 getDelay 方法一致的排序. 在网上看到了一些

  • Java并发之线程池Executor框架的深入理解

    线程池 无限制的创建线程 若采用"为每个任务分配一个线程"的方式会存在一些缺陷,尤其是当需要创建大量线程时: 线程生命周期的开销非常高 资源消耗 稳定性 引入线程池 任务是一组逻辑工作单元,线程则是使任务异步执行的机制.当存在大量并发任务时,创建.销毁线程需要很大的开销,运用线程池可以大大减小开销. Executor框架 说明: Executor 执行器接口,该接口定义执行Runnable任务的方式. ExecutorService 该接口定义提供对Executor的服务. Sched

  • Java并发之串行线程池实例解析

    前言 做Android的这两年时间,通过研究Android源码,也会Java并发处理多线程有了自己的一些理解. 那么问题来了,如何实现一个串行的线程池呢? 思路 何为串行线程池呢? 也就是说,我们的Runnable对象应该有个排队的机制,它们顺序从队列尾部进入,并且从队列头部选择Runnable进行执行. 既然我们有了思路,那我们就考虑一下所需要的数据结构? 既然是从队列尾部插入Runnable对象,从队列头部执行Runnable对象,我们自然需要一个队列.Java的SDK已经给我们提供了很好的

  • java并发编程_线程池的使用方法(详解)

    一.任务和执行策略之间的隐性耦合 Executor可以将任务的提交和任务的执行策略解耦 只有任务是同类型的且执行时间差别不大,才能发挥最大性能,否则,如将一些耗时长的任务和耗时短的任务放在一个线程池,除非线程池很大,否则会造成死锁等问题 1.线程饥饿死锁 类似于:将两个任务提交给一个单线程池,且两个任务之间相互依赖,一个任务等待另一个任务,则会发生死锁:表现为池不够 定义:某个任务必须等待池中其他任务的运行结果,有可能发生饥饿死锁 2.线程池大小 注意:线程池的大小还受其他的限制,如其他资源池:

  • JAVA多线程并发下的单例模式应用

    单例模式应该是设计模式中比较简单的一个,也是非常常见的,但是在多线程并发的环境下使用却是不那么简单了,今天给大家分享一个我在开发过程中遇到的单例模式的应用. 首先我们先来看一下单例模式的定义: 一个类有且仅有一个实例,并且自行实例化向整个系统提供. 单例模式的要素: 1.私有的静态的实例对象 2.私有的构造函数(保证在该类外部,无法通过new的方式来创建对象实例) 3.公有的.静态的.访问该实例对象的方法 单例模式分为懒汉形和饿汉式 懒汉式: 应用刚启动的时候,并不创建实例,当外部调用该类的实例

  • java编程多线程并发处理实例解析

    本文主要是通过一个银行用户取钱的实例,演示java编程多线程并发处理场景,具体如下. 从一个例子入手:实现一个银行账户取钱场景的实例代码. 第一个类:Account.java 账户类: package cn.edu.byr.test; public class Account { private String accountNo; private double balance; public Account(){ } public Account(String accountNo,double

  • 简单谈谈RxJava和多线程并发

    前言 相信对于RxJava,大家应该都很熟悉,他最核心的两个字就是异步,诚然,它对异步的处理非常的出色,但是异步绝对不等于并发,更不等于线程安全,如果把这几个概念搞混了,错误的使用RxJava,是会来带非常多的问题的. RxJava与并发 首先让我们来看一段RxJava协议的原文: Observables must issue notifications to observers serially (not in parallel). They may issue these notificat

  • Java并发 线程间的等待与通知

    前言: 前面讲完了一些并发编程的原理,现在我们要来学习的是线程之间的协作.通俗来说就是,当前线程在某个条件下需要等待,不需要使用太多系统资源.在某个条件下我们需要去唤醒它,分配给它一定的系统资源,让它继续工作.这样能更好的节约资源. 一.Object的wait()与notify() 基本概念: 一个线程因执行目标动作的条件未能满足而被要求暂停就是wait,而一个线程满足执行目标动作的条件之后唤醒被暂停的线程就是notify. 基本模板: synchronized (obj){ //保护条件不成立

  • Java编程线程间通信与信号量代码示例

    1.信号量Semaphore 先说说Semaphore,Semaphore可以控制某个资源可被同时访问的个数,通过acquire()获取一个许可,如果没有就等待,而release()释放一个许可.一般用于控制并发线程数,及线程间互斥.另外重入锁ReentrantLock也可以实现该功能,但实现上要复杂些. 功能就类似厕所有5个坑,假如有10个人要上厕所,那么同时只能有多少个人去上厕所呢?同时只能有5个人能够占用,当5个人中的任何一个人让开后,其中等待的另外5个人中又有一个人可以占用了.另外等待的

  • Java并发线程之线程池的知识总结

    初始化线程池后,把任务丢进去,等待调度就可以了,使用起来比较方便. JAVA中Thread是线程类,不建议直接使用Thread执行任务,在并发数量比较多的情况下,每个线程都是执行一个很短的时间就任务结束了,这样频繁创建线程会大大降低系统的效率,因为频繁的创建和销毁线程需要时间.而线程池可以复用,就是执行完一个任务,并不销毁,而是可以继续执行其它任务. Thread的弊端 每次new Thread() 创建对象,性能差. 线程缺乏统一管理,可能无限制创建线程,相互竞争,有可能占用过多系统资源导致死

  • java 并发线程个数的如何确定

    目录 java 并发线程个数的确定 cpu密集型 io密集型 有锁的情况 java 线程池线程数量确定思路 多线程可以快速执行任务的原理 创建线程池需要的参数 确定线程数 java 并发线程个数的确定 本文从控制变量的角度来谈决定线程个数的依据.模型很简单,在实际的生产环境中,情况肯定比下文要复杂的多.要充分的进行测试,以使线程个数为优. java应用程序大概分为两种:cpu密集型和io密集型. cpu密集型 就是指线程大部分时间都在用cpu,一般来说,普通的操作都需要用到cpu,比如计算,读取

  • Java并发编程线程间通讯实现过程详解

    在Java中线程间通讯有多种方式,我这里列出一些常用方式,并用代码的方式展示他们是如何实现的: 共享变量 wait, notify,notifyAll(这3个方法是Object对象中的方法,且必须与synchronized关键字结合使用) CyclicBarrier.CountDownLatch 利用LockSupport Lock/Condition机制 管道,创建管道输出流PipedOutputStream和管道输入流PipedInputStream 示例一: package com.zhi

  • 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多线程-线程的同步与锁的问题

    一.同步问题提出 线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏. 例如:两个线程ThreadA.ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据. package cn.thread; public class Foo { private int x = 100; public int getX() { return x; } public int fix(int y) { x = x - y; return x; } } package cn.thread

  • Java并发编程之线程间的通信

    一.概念简介 1.线程通信 在操作系统中,线程是个独立的个体,但是在线程执行过程中,如果处理同一个业务逻辑,可能会产生资源争抢,导致并发问题,通常使用互斥锁来控制该逻辑.但是在还有这样一类场景,任务执行是有顺序控制的,例如常见的报表数据生成: 启动数据分析任务,生成报表数据: 报表数据存入指定位置数据容器: 通知数据搬运任务,把数据写入报表库: 该场景在相对复杂的系统中非常常见,如果基于多线程来描述该过程,则需要线程之间通信协作,才能有条不紊的处理该场景业务. 2.等待通知机制 如上的业务场景,

  • java 中线程等待与通知的实现

    java 中线程等待与通知的实现 前言: 关于等待/通知,要记住的关键点是: 必须从同步环境内调用wait().notify().notifyAll()方法.线程不能调用对象上等待或通知的方法,除非它拥有那个对象的锁. wait().notify().notifyAll()都是Object的实例方法.与每个对象具有锁一样,每个对象可以有一个线程列表,他们等待来自该信号(通知).线程通过执行对象上的wait()方法获得这个等待列表.从那时候起,它不再执行任何其他指令,直到调用对象的notify()

  • java并发数据包Exchanger线程间的数据交换器

    java.util.concurrent.Exchanger可以用来进行数据交换,或者被称为“数据交换器”.两个线程可以使用Exchanger交换数据,下图用来说明Exchanger的作用 在下面的代码中 首先我们定义了一个Exchanger,用于数据交换 然后定义了两个线程对象bookExchanger1和bookExchanger2,两个线程都持有Exchanger交换器对象用于数据交换 两个线程中的每个线程都有自己的数据,比如下面代码中的String[] 书籍数组. public stat

随机推荐