Java并发编程与API详解

news2024/10/2 1:51:55

文章目录

  • 前言
  • 操作系统——进程和线程
    • 进程
      • 进程组成
      • 进程状态
      • 进程控制
        • 进程创建
        • 进程终止
        • 进程阻塞和唤醒
      • 进程通信
    • 线程
      • 线程组成
      • 线程状态
      • 线程控制
      • 线程的实现方式
        • 用户线程
        • 内核线程
        • 混合方式
    • CPU调度
      • 调度的层次
      • 调度的实现
        • 调度器
        • 调度的时机、切换与过程
        • 进程调度的方式
        • 闲逛进程
        • 两种线程的调度
      • 进程切换
    • 同步与互斥
      • 实现临界区互斥的基本方法
      • 互斥锁
      • 管程
  • Java并发模型
    • 线程组成
    • 线程状态
    • 线程控制
      • 线程创建
        • 任务
        • 线程池
          • 线程池的构造
          • 工作流程
          • 钩子方法
          • 队列的维护
          • 终结
          • 预定义线程池
      • 线程中断
      • 线程阻塞
    • 线程安全
      • JMM
      • 不可变对象
      • ThreadLocal
      • Unsafe类解析
      • volatile
      • 原子类
      • 对象锁
        • 对象锁的实现
          • 管程实现方式
          • 锁记录实现方式
        • 线程通信
      • AQS
        • 状态变量
        • 等待队列
        • AQS的实现流程
        • AQS的使用流程
        • 线程通信
        • JDK中AQS的实现
      • 集合安全
        • CopyOnWriteArrayList
        • ConcurrentHashMap
        • ConcurrentSkipListMap
        • BlockingQueue

前言

本文分为两个部分,第一部分是操作系统中进程和线程的知识,第二部分是Java中进程和线程的知识。之所以这么划分,是因为Java是运行在JVM之上的,而JVM就等价于Java的操作系统。如果只从Java层面学习,那么始终是知其然而不知其所以然,只有通过对比学习,才能彻底地接受、理解和掌握。

操作系统——进程和线程

进程

进程组成

在多程序环境下,允许多个程序并发执行,为了更好地描述和控制程序的并发执行,引入了进程的概念。进程是一个独立的运行单位,也是操作系统进行资源分配和调度的基本单位。它由以下三部分组成:

  • 进程控制块(PCB):PCB是一个专门的数据结构。系统利用PCB来描述进程的基本情况和运行状态,进而控制和管理进程。所谓创建进程,就是创建进程的PCB;而撤销进程,就是撤销进程的PCB。PCB中包含的主要内容如下:
进程描述信息进程控制和管理信息资源分配清单处理及相关信息
进程标识符(PID)进程当前状态代码段指针通用寄存器值
用户标识符(UID)进程优先级数据段指针地址寄存器值
代码运行入口地址堆栈段指针控制寄存器值
程序的外存地址文件描述符标志寄存器值
进入内存时间键盘状态字
CPU占用时间鼠标
信号量使用
  • 程序段:程序段就是能被进程调度程序调度到CPU执行的程序代码段,多个进程可以共享同一个程序段。
  • 数据段:一个进程的数据段,可以是进程对应的程序加工处理的原始数据,也可以是程序执行时产生的中间或最终结果。

进程具有的特征如下:

  • 动态性:进程是程序的一次执行,它有创建、活动、暂停、终止等过程,具有一定的生命周期,是动态产生、变化和消亡的。
  • 并发性:指多个进程实体同存于内存中,能在一段时间内同时运行。引入进程的目的就是使进程能和其它进程并发执行。
  • 独立性:指进程实体是一个能独立运行、独立获得资源和独立接受调度的基本单位。凡未建立PCB的程序,都不能作为一个独立的单位参与运行。
  • 异步性:由于进程的相互制约,使得进程按各自独立的、不可预知的速度向前推进。异步性会导致执行结果的不可再现性,为此在操作系统中必须配置相应的进程同步机制。

进程状态

由于系统中各进程之间的相互制约及系统运行环境的变化,使得进程的状态也在不断地发生变化。通常进程有以下5种状态:

  • 运行态:进程正在计算机上运行。在单核计算机中,每个时刻只有一个进程处于运行态。
  • 就绪态:进程获得了除CPU外的一切所需资源,一旦得到CPU,便可立即运行。系统中处于就绪态的进程可能有多个,通常将它们排成一个队列,称为就绪队列
  • 阻塞态:进程正在等待某一事件(如等待某资源为可用或等待I/O完成)而暂停运行,即使CPU空闲,该进程也不能运行。系统通常将处于阻塞态的进程也排成一个队列,称为阻塞队列。甚至根据阻塞原因的不同,设置多个阻塞队列。
  • 创建态:创建进程需要多个步骤,首先申请一个空白PCB,并向PCB中填写用于控制和管理进程的信息;然后为该进程分配运行时所必须的资源;最后把该进程转入就绪态并插入就绪队列。但是,如果进程所需的资源尚不能得到满足(如内存不足),则创建工作尚未完成,进程此时所处的状态就是创建态。
  • 结束态:进程正从系统中消失,可能是进程正常结束或其它原因退出运行。进程需要结束运行时,系统首先将该进程置为结束态,然后进一步处理资源释放和回收等工作。

前三种是进程的基本状态,它们之间的状态转换如下:

  • 就绪态 → \rightarrow 运行态:处于就绪态的进程被调度后,获得CPU,于是进程由就绪态转换为运行态。
  • 运行态 → \rightarrow 就绪态:处于运行态的进程在时间片用完后,不得不让出CPU,从而进程由运行态转换为就绪态。此外,在可剥夺的操作系统中,当有更高优先级的进程就绪时,调度程序将正在执行的进程转换为就绪态,让更高优先级的进程执行。
  • 运行态 → \rightarrow 阻塞态(主动):进程请求某资源的使用和分配或等待某一事件的发生时,它就从运行态转换为阻塞态。
  • 阻塞态 → \rightarrow 就绪态(被动):进程等待的事件到来时,中断处理程序必须把相应进程的状态由阻塞态转换为就绪态。

在这里插入图片描述

进程控制

进程控制的主要功能是对系统中的所有进程实施有效的管理,它具有创建新进程、撤销已有进程、实现进程状态转换等功能。在操作系统中,一般把进程控制用的程序段称为原语,原语的特点是执行期间不允许中断,它是一个不可分割的基本单位。

进程创建

操作i系统允许一个进程创建另一个进程,此时创建者称为父进程,被创建的进程称为子进程。子进程可以继承父进程所拥有的资源。当子进程被撤销时,应将其从父进程那里获得的资源归还给父进程。此外,在撤销父进程时,通常也会同时撤销其所有的子进程。操作系统创建一个新进程的过程如下(创建原语):

  • 为新进程分配一个唯一的进程标识号,并申请一个空白PCB(PCB是有限的)。若PCB申请失败,则创建失败。
  • 为进程分配其运行所需的资源,如内存、文件、I/O设备和CPU时间等(在PCB中体现)。这些资源或从操作系统获得,或仅从其父进程获得。如果资源不足,则并不是创建失败,而是处于创建态,等待资源。
  • 初始化PCB,主要包括初始化标志信息、初始化计算机状态信息和初始化计算机控制信息,以及设置进程的优先级等。
  • 若进程就绪队列能够接纳新进程,则将新进程插入就绪队列,等待被调度运行。

进程终止

操作系统终止进程的过程如下(终止原语):

  • 根据被终止进程的标识符,检索出该进程的PCB,从中读出该进程的状态。
  • 若要被终止进程处于执行状态,立即终止该进程的执行,并将计算机资源分配给其它进程。
  • 若该进程还有子孙进程,则应将其所有子孙进程终止。
  • 将该进程所拥有的全部资源,或归还给其父进程,或操作系统。
  • 将该PCB从所在队列中删除。

进程阻塞和唤醒

正在执行的进程,由于期待的某些事件未发生,进程便通过调用阻塞原语,使自己由运行态变为阻塞态。阻塞原语的执行过程如下:

  • 找到将要被阻塞进程的标识号对应的PCB。
  • 若该进程为运行态,则保护其现场,将其状态转为阻塞态,停止运行。
  • 把该PCB插入相应事件的等待队列,将处理机资源调度给其它就绪进程。

当被阻塞进程所期待的事件出现时,由有关进程调用唤醒原语,将等待该事件的进程唤醒。唤醒原语的执行过程如下:

  • 在该事件的等待队列中找到相应进程的PCB。
  • 将其从等待队列中移出,并置其状态为就绪态。
  • 把该PCB插入就绪队列,等待调度程序调度。

注意,阻塞原语和唤醒原语是一对作用刚好相反的原语,必须成对使用。如果在某进程中调用了阻塞原语,则必须在与之合作的或其它相关的进程中安排一条相应的唤醒原语,以便唤醒阻塞进程。否则,阻塞进程将会因不能被唤醒而永久地处于阻塞状态。

进程通信

进程通信是指进程之间的信息交换,常用的高级通信方式有以下三种:

  • 共享存储:在通信的进程之间存在一块可直接访问的共享空间,通过对这片共享空间进行读写操作实现进程之间的信息交换。在对共享空间进行读写操作时,需要使用同步互斥工具进行控制。共享存储又分为两种:低级方式的共享是基于数据结构的共享;高级方式的共享则是基于存储区的共享。操作系统只负责为通信进程提供可共享使用的存储空间和同步互斥工具,而数据交换则由用户自己安排读写指令完成。
  • 消息传递:在消息传递系统中,进程间的数据交换以格式化的消息为单位。若通信的进程之间不存在可直接访问的共享空间,则必须利用操作系统提供的消息传递方法实现进程通信。进程通过系统提供的发送消息和接收消息两个原语进行数据交换。这种方式隐藏了通信实现细节,使通信过程对用户透明,简化了通信程序的设计,是当前应用最广泛的进程间通信机制。消息传递的方式有以下两种:
    • 直接通信方式:发送进程直接把消息发送给接收进程,并将它挂在接收进程的消息缓冲队列上,接收进程从消息缓冲队列中取得消息。
    • 间接通信方式: 发送进程把消息发送到某个中间实体,接收进程从中间实体取得消息。这种中间实体一般称为信箱。该通信方式广泛应用于计算机网络中。
  • 管道通信:管道通信是消息传递的一种特殊方式。所谓管道,是指用于连接一个读进程和一个写进程以实现它们之间通信的一个共享文件,又名pipe文件。向管道提供输入的发送进程,以字符流形式将大量的数据送入管道;而接收管道输出的接收进程则从管道中接收数据。

线程

线程组成

引入线程的目的是减小程序在并发执行时所付出的时空开销。线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。与进程类似,系统也为每个线程配置一个线程控制块TCB,用于记录控制和管理线程的信息。TCB通常包括:

  • 线程标识符。
  • 一组寄存器,包括程序计数器、状态寄存器和通用寄存器。
  • 线程运行状态,用于描述线程正处于何种状态。
  • 优先级。
  • 线程专有存储区,线程切换时用于保存现场等。
  • 堆栈指针,用于过程调用时保存局部变量及返回地址等。

引入线程后,进程的内涵发生了改变,进程只作为除CPU外的系统资源的分配单元,而线程则作为CPU的分配单元。由于一个进程内部有多个线程,若线程的切换发生在同一个进程内部,则只需要很少的时空开销。引入线程操作系统的变化如下:

  • 调度:在传统的操作系统中,拥有资源和独立调度的基本单位都是进程,每次调度都要进行上下文切换,开销较大。在引入线程的操作系统中,线程是独立调度的基本单位。在同一进程中,线程的切换不会引起进程切换。但从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。
  • 并发性:在引入线程的操作系统中,不仅进程之间可以并发执行,而且一个进程中的多个线程之间亦可并发执行,甚至不同进程中的线程也能并发执行。
  • 拥有资源:进程是系统中拥有资源的基本单位,而线程不拥有系统资源,但线程可以访问其隶属进程的系统资源,这主要表现在属于同一进程的所有线程都具有相同的地址空间。
  • 独立性:每个进程都拥有独立的地址空间和资源,除了共享全局变量,不允许其它进程访问。某进程中的线程对其它进程不可见。同一进程中的不同线程是为了提高并发性及进行相互之间的合作而创建的,它们共享进程的地址空间和资源。
  • 系统开销:在创建或撤销进程时,系统都要为之分配或回收PCB及其它资源。操作系统为此所付出的开销,明显大于创建或撤销线程时的开销。类似地,在进程切换时涉及进程上下文的切换,而线程切换时只需保存和设置少量寄存器内容,开销很小。此外,由于同一进程内的多个线程共享进程的地址空间,因此,这些线程之间的同步与通信非常容易实现,甚至无须操作系统的干预。
  • 支持多处理机系统:对于传统单线程进程,不管有多少处理机,进程只能运行在一个处理机上。对于多线程进程,可以将进程中的多个线程分配到多个处理机上执行。

线程状态

与进程一样,各线程之间也存在共享资源和相互合作的制约关系,致使线程在运行时也具有间断性。相应地,线程在运行时也具有以下三种基本状态:

  • 运行状态:线程已获得CPU而正在运行。
  • 就绪状态:线程已具备各种执行条件,只需再获得CPU便可立即执行。
  • 阻塞状态:线程在执行中因某事件受阻而处于暂停状态。

线程这三种基本状态之间的转换和进程基本状态之间的转换是一样的。

线程控制

  • 线程的创建:操作系统中有用于创建线程和终止线程的函数(或系统调用)。用户程序启动时,通常仅有一个称为初始化线程的线程正在执行,其主要功能是用于创建新线程。在创建新线程时,需要利用一个线程创建函数, 并提供相应的参数。线程创建函数执行完后,将返回一个线程标识符。
  • 线程的终止:当一个线程完成自己的任务后,或线程在运行中出现异常而要被强制终止时,由终止线程调用相应的函数执行终止操作。但是有些线程(主要是系统线程)一旦被建立,便一直运行而不会被终止。通常,线程被终止后并不立即释放它所占有的资源,只有当进程中的其它线程执行了分离函数后,被终止线程才与资源分离,此时的资源才能被其它线程利用。被终止但尚未释放资源的线程仍可被其它线程调用,以使被终止线程重新恢复运行。

线程的实现方式

线程的实现可以分为两类:用户级线程(ULT)和内核级线程(KLT)。

用户线程

在用户级线程中,有关线程管理的所有工作都由应用程序在用户空间中完成,内核意识不到线程的存在。应用程序可以通过使用线程库设计成多线程程序。通常,应用程序从单线程开始,在该线程中开始运行,在其运行的任何时刻,可以通过调用线程库中的派生函数创建一个在相同进程中运行的新线程。

在这里插入图片描述

对于设置了用户级线程的系统,其调度仍是以进程为单位进行的,各个进程轮流执行一个时间片。假设进程A包含1个用户级线程,进程B包含100 个用户级线程,这样,进程A中线程的运行时间将是进程B中各线程运行时间的100倍。这种实现的优点如下:

  • 线程切换不需要转换到内核空间,节省了模式切换的开销。
  • 调度算法可以是进程专用的,不同的进程可根据自身的需要,对自己的线程选择不同的调度算法。
  • 用户级线程的实现与操作系统平台无关,对线程管理的代码是属于用户程序的一部分。

这种实现的缺点如下:

  • 系统调用的阻塞问题,当线程执行一个系统调用时,不仅该线程被阻塞,而且进程内的所有线程都被阻塞。
  • 不能发挥多处理机的优势,内核每次分配给一个进程的仅有一个CPU,因此进程中仅有一个线程能执行。

内核线程

在操作系统中,无论是系统进程还是用户进程,都是在操作系统内核的支持下运行的,与内核紧密相关。内核级线程同样也是在内核的支持下运行的,线程管理的所有工作也是在内核空间内实现的。内核空间也为每个内核级线程设置一个TCB,内核根据TCB感知某线程的存在,并对其加以控制。

在这里插入图片描述

这种实现方式的优点如下:

  • 能发挥多处理机的优势,内核能同时调度同一进程中的多个线程并行执行。
  • 如果进程中的一个线程被阻塞,内核可以调度该进程中的其它线程占用处理机,也可运行其它进程中的线程。
  • 内核支持线程具有很小的数据结构和堆栈,线程切换比较快、开销小。
  • 内核本身也可采用多线程技术,可以提高系统的执行速度和效率。

这种实现方式的缺点如下:

  • 同一进程中的线程切换,需要从用户态转到核心态进行,系统开销较大。这是因为用户进程的线程在用户态运行,而线程调度和管理是在内核实现的。

混合方式

在组合实现方式中,内核支持多个内核级线程的建立、调度和管理,同时允许用户程序建立、调度和管理用户级线程。一些内核级线程对应多个用户级线程,这是用户级线程通过时分多路复用内核级线程实现的。同一进程中的多个线程可以同时在多处理机上并行执行,且在阻塞一个线程时不需要将整个进程阻塞,所以组合方式能结合KLT和ULT的优点,并且克服各自的不足。

在这里插入图片描述

实现线程库主要的方法有如下两种:.

  • 在用户空间中提供一个没有内核支持的库。这种库的所有代码和数据结构都位于用户空间中。这意味着,调用库内的一个函数只导致用户空间中的一个本地函数的调用。
  • 实现由操作系统直接支持的内核级的一个库。对于这种情况,库内的代码和数据结构位于内核空间。调用库中的一个API函数通常会导致对内核的系统调用。

CPU调度

CPU调度是对CPU进行分配,以实现进程并发地执行。CPU调度是多道程序操作系统的基础,是操作系统设计的核心问题。

调度的层次

一个作业从提交开始直到完成,往往要经历以下三级调度:

  • 高级调度(作业调度):按照一定的原则从外存上处于后备队列的作业中挑选一个(或多个),给它们分配必要的资源,并建立相应的进程,以使它们获得竞争CPU的权利。
  • 中级调度(内存调度):将那些暂时不能运行的进程调至外存等待,此时进程的状态称为挂起态。当它们已具备运行条件且内存又稍有空闲时,由中级调度来决定把外存上的那些己具备运行条件的就绪进程再重新调入内存,并修改其状态为就绪态,挂在就绪队列上等待。
  • 低级调度(进程调度):按照某种算法从就绪队列中选取一个进程,将CPU分配给它。进程调度是最基本的一种调度,在各种操作系统中都必须配置这级调度。进程调度的频率很高,一般几十亳秒一 次。

在这里插入图片描述

调度的实现

调度器

在操作系统中,用于调度和分派CPU的组件称为调度程序,它通常由三部分组成:

  • 排队器:将系统中的所有就绪进程按照一定的策略排成一个或多个队列,以便于调度程序选择。每当有一个进程转变为就绪态时,排队器便将它插入到相应的就绪队列中。
  • 分派器:依据调度程序所选的进程,将其从就绪队列中取出,将CPU分配给新进程。
  • 上下文切换器:在对CPU进行切换时,会发生两对上下文的切换操作:
    • 第一对,将当前进程的上下文保存到其PCB中,再装入分派程序的上下文,以便分派程序运行;
    • 第二对,移出分派程序的上下文,将新选进程的CPU现场信息装入处理机的各个相应寄存器。

在这里插入图片描述

调度的时机、切换与过程

调度程序是操作系统内核程序。请求调度的事件发生后,才可能运行调度程序,调度了新的就绪进程后,才会进行进程切换。理论上这三件事情应该顺序执行,但在实际的操作系统内核程序运行中,若某时刻发生了引起进程调度的因素,则不一定能马上进行调度与切换。现代操作系统中,不能进行进程的调度与切换的情况有以下几种:

  • 在处理中断的过程中:中断处理过程复杂,在实现上很难做到进程切换,而且中断处理是系统工作的一部分,逻辑上不属于某一进程,不应被剥夺处理机资源。
  • 进程在操作系统内核临界区中:进入临界区后,需要独占式地访问,理论上必须加锁,以防止其它并行进程进入,在解锁前不应切换到其它进程,以加快临界区的释放。
  • 其它需要完全屏蔽中断的原子操作过程中:如加锁、解锁、中断现场保护、恢复等原子操作。在原子过程中,连中断都要屏蔽,更不应该进行进程调度与切换。

若在上述过程中发生了引起调度的条件,则不能马上进行调度和切换,应置系统的请求调度标志,直到上述过程结束后才进行相应的调度与切换。应该进行进程调度与切换的情况如下:

  • 发生引起调度条件且当前进程无法继续运行下去时,可以马上进行调度与切换。若操作系统只在这种情况下进行进程调度,则是非抢占调度。
  • 中断处理结束或自陷处理结束后,返回被中断进程的用户态程序执行现场前,若置上请求调度标志,即可马上进行进程调度与切换。若操作系统支持这种情况下的运行调度程序,则实现了抢占方式的调度。

进程切换往往在调度完成后立刻发生,它要求保存原进程当前断点的现场信息,恢复被调度进程的现场信息。现场切换时,操作系统内核将原进程的现场信息推入当前进程的内核堆栈来保存它们,并更新堆栈指针。内核完成从新进程的内核栈中装入新进程的现场信息、更新当前运行进程空间指针、重设PC寄存器等相关工作之后,开始运行新的进程。

进程调度的方式

所谓进程调度方式,是指当某个进程正在CPU上执行时,若有某个更为重要或紧迫的进程需要处理,即有优先权更高的进程进入就绪队列,此时应如何分配CPU。通常有以下两种进程调度方式:

  • 非抢占调度方式:是指当一个进程正在CPU上执行时,即使有某个更为重要或紧迫的进程进入就绪队列,仍然让正在执行的进程继续执行,直到该进程运行完成或发生某种事件而进入阻塞态时,才把CPU分配给其他进程。非抢占调度方式的优点是实现简单、系统开销小,适用于大多数的批处理系统,但它不能用于分时系统和大多数的实时系统。
  • 抢占调度方式:是指当一个进程正在处理机上执行时,若有某个更为重要或紧迫的进程需要使用CPU,则允许调度程序根据某种原则去暂停正在执行的进程,将处理机分配给这个更为重要或紧迫的进程。抢占调度方式对提高系统吞吐率和响应效率都有明显的好处。但抢占不是一种任意性行为,必须遵循一定的原则,主要有优先权、短进程优先和时间片原则等。

闲逛进程

在进程切换时,如果系统中没有就绪进程,就会调度闲逛进程运行,如果没有其它进程就绪,该进程就一直运行, 并在执行过程中测试中断。闲逛进程的优先级最低,没有就绪进程时才会运行闲逛进程,只要有进程就绪,就会立即让出处理机。闲逛进程不需要CPU之外的资源,它不会被阻塞。

两种线程的调度

  • 用户级线程调度:由于内核并不知道线程的存在,所以内核还是和以前一样,选择一个进程,并给予时间控制。由进程中的调度程序决定哪个线程运行。
  • 内核级线程调度。内核选择一个特定线程运行,通常不用考虑该线程属于哪个进程。对被选择的线程赋予一个时间片,如果超过了时间片,就会强制挂起该线程。

用户级线程的线程切换在同一进程中进行,仅需少量的机器指令;内核级线程的线程切换需要完整的上下文切换、修改内存映像、使高速缓存失效,这就导致了若干数量级的延迟。

进程切换

对于通常的进程而言,其创建、撤销及要求由系统设备完成的I/O操作,都是利用系统调用而进入内核,再由内核中的相应处理程序予以完成的。进程切换同样是在内核的支持下实现的,因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。

  • 上下文切换:切换CPU到另–个进程需要保存当前进程状态并恢复另一个进程的状态,这个任务称为上下文切换。上下文是指某一时刻CPU寄存器和程序计数器的内容。进行上下文切换时,内核会将旧进程状态保存在其PCB中,然后加载经调度而要执行的新进程的上下文。上下文切换实质上是指处理机从一个进程的运行转到另一个进程上运行,在这个过程中,进程的运行环境产生了实质性的变化。上 下文切换的流程如下:
    • 挂起一个进程,保存CPU上下文,包括程序计数器和其他寄存器。
    • 更新PCB信息。
    • 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
    • 选择另一个进程执行,并更新其PCB。
    • 跳转到新进程PCB中的程序计数器所指向的位置执行。
    • 恢复处理机上下文。
  • 上下文切换的消耗:上下文切换通常是计算密集型的,即它需要相当可观的CPU时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间,所以上下文切换对系统来说意味着消耗大量的CPU时间。有些处理器提供多个寄存器组,这样,上下文切换就只需要简单改变当前寄存器组的指针。
  • 上下文切换与模式切换:模式切换与上下文切换是不同的,模式切换时,CPU逻辑上可能还在执行同一进程。用户进程最开始都运行在用户态,若进程因中断或异常进入核心态运行,执行完后又回到用户态刚被中断的进程运行。用户态和内核态之间的切换称为模式切换,而不是上下文切换,因为没有改变当前的进程。上下文切换只能发生在内核态,它是多任务操作系统中的一个必需的特性。

同步与互斥

在多道程序环境下,进程是并发执行的,不同进程之间存在着不同的相互制约关系。为了协调进程之间的相互制约关系,引入了进程同步的概念。

  • 临界资源和临界区:虽然多个进程可以共享系统中的各种资源,但其中许多资源一次只能为一个进程所用,我们将一次仅允许一个进程使用的资源称为临界资源。对临界资源的访问,必须互斥地进行,在每个进程中,访问临界资源的那段代码称为临界区。为了保证临界资源的正确使用,可把临界资源的访问过程分成4个部分:
    • 进入区:为了进入临界区使用临界资源,在进入区要检查可否进入临界区,若能进入临界区,则应设置正在访问临界区的标志,以阻止其它进程同时进入临界区。
    • 临界区:进程中访问临界资源的那段代码。
    • 退出区:将正在访问临界区的标志清除。
    • 剩余区:代码中的其余部分。
  • 同步: 是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。进程间的直接制约关系源于它们之间的相互合作。
  • 互斥:当一个进程进入临界区使用临界资源时,另一个进程必须等待,当占用临界资源的进程退出临界区后,另一进程才允许去访问此临界资源。

实现临界区互斥的基本方法

软件实现方式:在进入区设置并检查一些标志来标明是否有进程在临界区中,若已有进程在临界区,则在进入区通过循环检查进行等待,进程离开临界区后则在退出区修改标志。

  • 单标志法:该算法可确保每次只允许一个进程进入临界区。但两个进程必须交替进入临界区,若某个进程不再进入临界区,则另一个进程也将无法进入临界区。
//P0进程					//P1进程
while(turn!=0);				while(turn!=1);			//进入区
critical section;			critical section;		//临界区
turn=1;						turn=0;					//退出去
remainder section;			remainder section;		//剩余区
  • 双标志先检查:该算法的优点在于不用交替进入;缺点在于两个进程可能同时进入临界区。
//Pi进程					//Pj进程
while(flag[j]);				while(flag[i]);			//进入区
flag[i]=true;				flag[j]=true;			//进入区
critical section;			critical section;		//临界区
flag[i]=false;				flag[j]=false;			//退出区
remainder section;			remainder section;		//剩余区
  • 双标志法后检查:该算法优点在于两个进程不可能同时进入临界区;缺点在于双方相互谦让,从而导致饥饿现象。
//Pi进程					//Pj进程
flag[i]=true;				flag[j]=true;			//进入区
while(flag[j]);				while(flag[i]);			//进入区
critical section;			critical section;		//临界区
flag[i]=false;				flag[j]=false;			//退出区
remainder section;			remainder section;		//剩余区
  • Peterson’s Algorithm:该算法解决了饥饿现象。
//Pi进程							//Pj进程
flag[i]=true;turn=j;				flag[j]=true;turn=i;		//进入区
while(flag[j]&&turn=j);				while(flag[i]&&turn==i);	//进入区
critical section;					critical section;			//临界区
flag[i]=false;						flag[j]=false;				//退出区
remainder section;					remainder section;			//剩余区

硬件实现方式:计算机提供了特殊的硬件指令,允许对一个字中的内容进行检测和修正,或对两个字的内容进行交换等。通过硬件支持实现临界段问题的方法称为元方法。

  • 中断屏蔽方法:当一个进程正在执行它的临界区代码时,防止其它进程进入其临界区的最简方法是关中断。因为CPU只在发生中断时引起进程切换,因此屏蔽中断能够保证当前运行的进程让临界区代码顺利地执行完,进而保证互斥的正确实现,然后执行开中断。
//...
关中断;
临界区;
开中断;
//...

硬件指令方法:

  • TestAndSet指令:这条指令是原子操作,即执行改代码时不允许被中断。其功能是读出指定标志后把该标志设置为真。可以为每个临界资源设置一个共享布尔变量lock,表示资源的两种状态,true表示被占用,初值为false。进程在进入临界资源之前,利用TestAndSet指令检查标志lock,若无进程在临界区,则其值为false,可以进入,关闭临界资源,把lock设置为true,使任何进程都不能进入临界区;若有进程在临界区,则循环检查,直到进程退出。
boolean TestAndSet(boolean * lock){
	boolean old;
	old=*lock;
	*lock=true;
	return old;
}

while TestAndSet(&lock);
进程的临界区代码;
lock=false;
进程的其它代码;
  • Swap指令:该指令的功能是交换两个字的内容。可以为每个临界资源设置一个共享布尔变量lock,初值为false;在每个进程中再设置一个局部布尔变量key,用于与lock交换信息。在进入临界区前,先利用Swap指令交换lock与key的内容,然后检查key的状态;有进程在临界区时,重复交换和检查过程,直到进程退出。
Swap(boolean *a,boolean *b){
	boolean temp;
	Temp=*a;
	*a=*b;
	*b=temp;
}

key=true;
while(key!=false){
	Swap(&lock,&key);
}
进程的临界区代码;
lock=false;
进程的其它代码;

互斥锁

互斥锁是解决临界区最简单的工具,一个进程在进入临界区时应获得锁;在退出临界区时释放锁。函数acquire()获得锁,而函数release()释放锁。每个互斥锁有一个布尔变量available,表示锁是否可用。如果锁是可用的,调用acqiure()会成功,且锁不再可用。当一个进程试图获取不可用的锁时,会被阻塞,直到锁被释放。acquire()release()的执行必须是原子操作,因此互斥锁通常采用硬件机制来实现。

acquire(){
	while(!available);
	available=false;
}
release(){
	available=true;
}

管程

系统中的各种硬件资源和软件资源,均可用数据结构抽象地描述其资源特性,即用少量信息和对资源所执行的操作来表征该资源,而忽略它们的内部结构和实现细节。利用共享数据结构抽象地表示系统中的共享资源,而把对该数据结构实施的操作定义为一组过程。进程对共享资源的申请、释放等操作,都通过这组过程来实现,这组过程还可以根据资源情况,或接受或阻塞进程的访问,确保每次仅有-一个进程使用共享资源,这样就可以统一管理对共享资源的所有访问,实现进程互斥。这个代表共享资源的数据结构,以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序,称为管程(monitor)。管程定义了一个数据结构和能为并发进程所执行(在该数据结构上)的一组操作,这组操作能同步进程和改变管程中的数据。

monitor Demo{//定义一个名为Demo的管程
	//定义一个共享数据结构,对应系统中的某种共享资源
	共享数据结构S;
	//对共享数据结构初始化
	init_code(){
		...
	}
	//申请一个资源
	take_away(){
		对共享数据机构x的一系列处理
	}
	//归还一个资源
	give_back(){
		对共享数据机构x的一系列处理
	}
}

当一个进程进入管程后被阻塞,直到阻塞的原因解除时,在此期间,如果该进程不释放管程,那么其它进程无法进入管程。为此,将阻塞原因定义为条件变量condition。通常,一个进程被阻塞的原因可以有多个,因此在管程中设置了多个条件变量。每个条件变量保存了一个等待队列,用于记录因该条件变量而阻塞的所有进程,对条件变量只能进行两种操作,即waitsignal

  • x.wait:当x对应的条件不满足时,正在调用管程的进程调用x.wait将自己插入x条件的等待队列,并释放管程。此时其它进程可以使用该管程。
  • x.signal: x对应的条件发生了变化,则调用x.signal,唤醒一个因x条件而阻塞的进程。
monitor Demo {

	共享数据结构s;
	condition X;
	//定义一个条件变量x
	
	init_ code() { 
		...
	}
	
	take_away() {
		if(S<=0) {
			x.wait();//资源不够,在条件变量x上阻塞等待
		}
		资源足够,分配资源,做一系列相应处理;
	}
	give_back(){
		归还资源,做一系列相应处理;
		if (有进程在等待){
			x.signal; / /唤醒一个阻塞进程
		} 
	}

Java并发模型

在Java中,当我们启动main()函数时其实就是启动了一个JVM 的进程,而 main() 函数所在的线程就是这个进程中的一个线程,也称主线程。Java线程如何实现并不受JVM规范的约束,它与具体的虚拟机实现相关。以HotSpot为例,它的每一个Java线程都是直接映射到一个操作系统原生线程来实现的。目前使用的两种主要线程库是:POSIX Pthreads、Windows API。Pthreads作为POSIX标准的扩展,可以提供用户级或内核级的库。Windows线程库是用于Windows操作系统的内核级线程库。Java线程API允许线程在Java程序中直接创建和管理。然而,由于JVM实例通常运行在宿主操作系统之上,Java 线程API通常采用宿主系统的线程库来实现,因此在Windows系统中Java线程通常采Windows API来实现,在类UNIX系统中采用Pthreads来实现。

线程组成

下图是一个Java进程运行时的JVM(jdk8)内存模型,一个进程中可以有多个线程,其中堆和元空间是线程共享的,程序计数器、本地方法栈和堆是每个线程都有的。
在这里插入图片描述

线程状态

Java线程在生命周期内一定属于以下六种状态中的一个:

  • NEW:线程被创建,但还没有调用start()方法。
  • RUNNABLE:调用start()方法之后,该状态包含操作系统层面的就绪状态、运行状态和阻塞状态。
  • BLOCKED:当一个线程试图获取一个内部的对象锁,但该锁被其它线程持有时,该线程就会进人该状态。当锁被释放并且线程调度器允许该线程持有它的时候,该线程将返回到原来的状态。
  • WAITING:当线程等待某个条件出现时,它自己就进入该状态。
  • TIMED_WAITING:当线程等待某个条件出现并设置了最大的等待时间时,它就进入了该状态。
  • TERMINATED:当run()方法执行结束或因其它原因终止时就进入该状态。

在这里插入图片描述

线程控制

线程创建

在Java中Thread类表示线程,创建一个线程就是实例化一个Thread类对象。

Thread(Runnable target) //接收一个任务创建一个线程
Thread(Runnable target, String name) //接收一个任务和线程名字创建一个线程
void start() //启动线程
boolean isAlive()   //判断线程是否还活着
static Thread currentThread()  //获取当前线程
void setDaemon(boolean on)  //将当前线程设置为守护线程
boolean isDaemon() //判断当前线程是否为守护线程
String getName()  //获取线程的名字
void setName(String name)  //设置线程的名字

任务

线程在创建时必须为其指定任务,Java中有以下两种任务:

  • Runnable:没有返回结果并且不会抛出异常。
  • Callable:有返回结果并且会抛出异常。
Future
boolean cancel(boolean mayInterruptIfRunning)
V get()
V get(long timeout, TimeUnit unit)
boolean isCancelled()
boolean isDone()
RunnableFuture
ScheduledFuture
Runnable
void run()
RunnableScheduledFuture
«class»
FutureTask
FutureTask(Callable callable)
FutureTask(Runnable runnable, V result)
Callable<V>
V call()

FutureTask将这两种任务包装起来。一方面,它实现了Runnable接口,可以被线程执行;另一方面,它实现了Future接口,可以表示异步计算的结果。Future提供的方法可以用于检查任务是否完成、等待任务完成和检索任务结果。例如,get()方法可以阻塞获取结果;cancel()方法可以取消任务。但是,一旦任务开始就不会被其它线程重复执行,一旦任务完成就不能被取消。

线程池

线程池改变了手动创建并管理线程的现状,它将任务和线程解耦,通过复用已有线程来执行任务,极大提高了程序性能。

Executor
ExectorService
ScheduledExecutorService
«class»
ThreadPoolExecutor
«class»
ScheduledThreadPoolExecutor

Java线程池由Executor框架实现,Executor意为执行器,用于启动线程,通过Executor来启动线程比使用 Threadstart()方法更好,除了更易管理,效率更好外,还有助于避免 this 逃逸问题。

void execute(Runnable command) 

ExecutorService是一种提供了关闭和产生Future方法的执行器。

void shutdown()//有序关闭执行器,在此过程中执行之前提交的任务,但不接受新任务。
List<Runnable> shutdownNow()//尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行的任务列表。
boolean	awaitTermination(long timeout, TimeUnit unit)
boolean	isShutdown()//如果执行器已关闭,则返回true。
boolean	isTerminated()//如果所有任务在执行器关闭后都已完成,则返回true。
<T> Future<T> submit(Callable<T> task)  
Future<?> submit(Runnable task) 
<T> Future<T> submit(Runnable task, T result)
//执行给定的任务,如果有成功完成的任务,则返回成功完成的任务的结果。
<T> T invokeAny(Collection<? extends Callable<T>> tasks)  
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)  
//执行给定的任务,在所有任务完成时返回一个Future列表,其中包含它们的状态和结果。
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)  
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) 

ScheduledExecutorService是一种可以延迟或定期执行任务的执行器。

//在delay时间后执行任务
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)
<V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit)
//在initialDelay时间后执行任务,在period时间间隔内重复执行
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit)
//在initialDelay时间后执行任务,在上个任务执行完成后延时delay继续执行下一个任务
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
线程池的构造

所有线程池都是ThreadPoolExecutor的实例,通过ThreadPoolExecutor的构造方法,可以了解线程池的构造并自定义线程池。

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
  • maximumPoolSizecorePoolSizecorePoolSize表示核心线程的数量,maximumPoolSize表示最大线程数量。ThreadPoolExecutor可以根据和两个参数设置的边界自动调节线程池的大小。当在方法execute(Runnable)中提交了一个新任务,并且运行的线程少于corePoolSize,则创建一个新线程来处理请求,即使其它工作线程处于空闲状态。如果运行的线程数大于corePoolSize但小于maximumPoolSize,则只有在队列已满时才会创建新线程。
  • keepAliveTime:如果线程池当前有超过corePoolSize的线程,并且多余的线程的空闲时间超过keepAliveTime时将被终止。
  • threadFactory:如果没有特别指定,则使用Executors.defaultThreadFactory()。它创建线程池中的所有线程,使其位于同一个ThreadGroup中,具有相同的NORM_PRIORITY优先级和非守护进程状态。
  • workQueue:用于保存未被执行任务的工作队列。
  • handler:当线程池中的workQueue已满并且线程数量已到达maximumPoolSize时,线程池就需要调用handler.rejectedExecution(Runnable r, ThreadPoolExecutor executor)方法拒绝新提交的任务。线程池预定义了以下四个拒绝策略:
    • AbortPolicy:直接抛出异常。
    • CallerRunsPolicy:使用调用execute方法的线程执行任务。
    • DiscardOldestPolicy:丢弃workQueue中最老的任务,将新任务添加到workQueue中。
    • DiscardPolicy:丢弃无法处理的任务。
工作流程

当调用execute方法提交任务时,线程池会经过以下步骤:

  • 如果运行的线程少于corePoolSize,线程池会创建一个新线程执行任务。
  • 如果corePoolSize或更多线程正在运行,线程池会将任务添加到workQueue中。
  • workQueue已满,并且线程数量未到达maximumPoolSize,继续创建线程执行任务。
  • 线程数已达maximumPoolSize,执行拒绝策略。

在这里插入图片描述

钩子方法

线程池提供了以下两个钩子方法,可以在每个任务执行之前和之后调用它们。如果钩子方法抛出异常,内部工作线程可能依次失败并突然终止。

protected void beforeExecute(Thread t, Runnable r)
protected void afterExecute(Runnable r, Throwable t)
队列的维护

方法getQueue允许访问工作队列,以便进行监视和调试。removepurge方法可用于在大量排队任务被取消时协助进行存储回收。

BlockingQueue<Runnable>	getQueue()
void purge()//尝试从工作队列中删除已取消的所有Future任务。
boolean	remove(Runnable task)
终结

程序中不再引用且没有剩余线程的池将自动关闭。但是核心线程一旦创建就不会自动死亡,如果希望即使忘记调用shutdown也能回收未使用的线程池,那么必须安排未使用的线程最终死亡,方法是使用零个核心线程或设置核心线程的空闲时间。

void allowCoreThreadTimeOut(boolean value)//设置控制核心线程是否会超时并在保持活动时间内没有任务到达时终止的策略,如果新任务到达时需要替换核心线程。当为false时,核心线程永远不会因为缺少传入的任务而终止。当为true时,应用于非核心线程的保持活动策略也应用于核心线程。
预定义线程池

Executors工具类提供了创建预定义线程池的方法:

static ExecutorService newCachedThreadPool()
static ExecutorService newFixedThreadPool(int nThreads)
static ScheduledExecutorService	newScheduledThreadPool(int corePoolSize)
static ExecutorService newSingleThreadExecutor()
static ScheduledExecutorService	newSingleThreadScheduledExecutor()
static ExecutorService newWorkStealingPool()

但最好还是使用构造函数创建线程池,因为这种方式会让线程池的使用者更好的了解线程池的构造并且会规避资源耗尽的风险。

线程中断

线程中断是指给某一线程发送一个请求中断信号,而停不停止运行完全取决于被请求线程。在Java中可以通过调用interrupt()方法对某一线程发送中断请求,当此方法被调用时,被请求线程的中断标志将被设置为true(这是每一个线程都具有的boolean标志),在每个线程内都应该不时地检测这个标志,以判断线程是否被请求中断,并决定是否接受请求。

void interrupt() //向线程发送中断请求,并将中断标志设置为true
boolean isInterrupted()  //判断是否被请求中断
static boolean interrupted() //判断是否被请求中断并清除标志位

线程阻塞

线程阻塞是指让当前线程进入TIMED_WAATINGWAITING状态,当线程进入阻塞状态时,无法响应外部中断请求,那么导致线程阻塞的方式必须解决这一问题,据此可以将阻塞方式分为以下两种:

  • 抛出异常式:顾名思义抛出异常的方式在当前线程被请求中断时会抛出一个InterruptedException异常。在抛出异常后会将中断标志清空。此类方式如下:
//Thread
static void sleep(long millis)
void join() //阻塞当前线程至调用线程结束
void join(long millis)  
//TimeUnit
void sleep(long timeout)
  • 不抛出异常式:LockSupport就是不抛出异常方式的实现,它可以在线程内任意位置让线程阻塞,并且不需要先获取某个对象的锁,也不会抛出InteruptedException异常。 LockSupport使用了一种名为Permit的许可概念来做到阻塞和唤醒线程,每个线程都只有一个许可,如果许可可用则park方法就会立即返回并在过程中消耗它,否则就会阻塞,也可以使用unpark方法获取许可。外部请求中断时park方法会立即返回,并且不会清空中断标志。
static Object getBlocker(Thread t)//返回提供给最近调用的尚未解除阻塞的park方法的阻塞对象,如果未被阻塞,则返回null。
static void	park() //阻塞当前线程
static void	parkNanos(long nanos) //阻塞指定时间
static void	parkUntil(long deadline) //最多阻塞指定时间
static void	unpark(Thread thread) //解除阻塞参数线程

线程安全

JMM

JMM(Java内存模型)类似于操作系统内存模型,它有以下两个作用:

  • 抽象了主内存(所有线程创建的对象都必须放在其中)和工作内存(JMM抽象出来的一个概念,每个线程都有一个工作内存,并且线程只能访问自己的工作内存)之间的关系。当线程需要访问共享变量时,必须将共享变量加载到工作内存并保存为一个副本,且线程对共享变量的操作都只能对工作内存中的副本进行。当多个线程同时操作一个共享变量(临界资源)时,就会引发线程安全问题。
  • 规定了从 Java 源代码到 CPU 可执行指令的这个转化过程要遵守哪些和并发相关的原则和规范。为了提升速度和性能,计算机在执行指令时会对指令进行重排序,即计算机在执行代码的时候并不一定是按照我们所写的代码的顺序依次执行。Java 源代码会经历编译器优化重排→指令并行重排→内存系统重排的过程,最终才变成操作系统可执行的指令序列。指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致线程安全问题。

在这里插入图片描述

解决线程安全的过程就是实现以下三个性质的过程:

  • 内存可见性:指某个线程修改了共享变量的值,新值对其它线程来说是立即可见的。
  • 有序性:指Java源代码不会被重排序。
  • 原子性:指某个线程对共享变量的操作一旦开始就不会被其它线程干扰。

其中,实现原子性一定可以保证线程安全,而实现内存可见性和有序性只在特定情况下才能保证线程安全。相应的,以后者实现的线程安全代价也自然低。

不可变对象

如果一个共享变量在初始化之后就不能被改变,那么这种对象就称为不可变对象,不可变对象一定是线程安全的。不可变对象的实现方式如下:

  • 如果共享变量是一个基本类型,那么只要在声明时使用final修饰,就可以保证该共享变量是不可变的。
  • 如果一个共享变量是对象类型,那么对象自身要保证其行为不会对其状态产生任何影响。例如String的实现。

ThreadLocal

如果一个变量是线程独有的、不可共享的,那么这个变量就一定是线程安全的,这种对象称之为非共享对象。通过ThreadLocal就可以实现非共享对象。每一个Thread对象内均含有一个ThreadLocalMap类型的成员变量threadLocalsThreadLocalMap定义在ThreadLocal中,它存储了以ThreadLocal为键、Object为值的键值对,threadLocals变量的作用是存储线程的非共享对象。

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal的使用流程是这样的:首先通过ThreadLocal将共享变量包装:

private static ThreadLocal<SharedVariable>threadLocal=new ThreadLocal<SharedVariable>();

然后在每个线程中使用ThreadLocal提供的getset方法设置共享变量的值。在这个过程中ThreadLocal充当当前线程中ThreadLocalMap的访问入口。set方法的执行过程如下:

public void set(T value) {
	//获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
    	//以自身为键,共享变量值为值存放到ThreadLocalMap中
        map.set(this, value);
    else
        createMap(t, value);
}

不难看出ThreadLocal通过在每个线程的ThreadLocalMap中保存当前线程设置的共享变量值的方式来实现非共享对象。ThreadLocalMap通过一个Entry来保存存入的键和值,Entry的定义如下:

 static class Entry extends WeakReference<ThreadLocal<?>> {
     /** The value associated with this ThreadLocal. */
     Object value;

     Entry(ThreadLocal<?> k, Object v) {
         super(k);
         value = v;
     }
 }

其中将键实现为弱引用,值实现为强引用,当键被GC自动回收而值不会被回收时就会造成内存泄漏问题。ThreadLocalMap 实现中已经考虑了这种情况,在调用 setgetremove方法的时候,会清理掉 keynull 的记录。因此在使用完ThreadLocal方法后最好手动调用remove方法。

Unsafe类解析

Unsafe类是一个提供底层、不安全本地方法的工具类,它通过单例模式实现并且只能由启动类加载器加载的类才能获取它的实例,在JUC包下大量的使用了它。

public final class Unsafe {
	...
    private Unsafe() {}

    private static final Unsafe theUnsafe = new Unsafe();
    
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        //判断调用此方法的类是否由启动类加载器加载,如果不是直接抛出异常
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }
    ...
}

如果我们想使用这个类,可以使用反射的方式获取它的实例:

Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);

它提供的功能如下:

  • 内存操作:直接对内存进行操作,以这种方式操作的内存属于堆外内存,在垃圾回收时不会自动回收,需要手动进行释放。
public native long allocateMemory(long bytes);
public native long reallocateMemory(long address, long bytes);
public native void setMemory(Object o, long offset, long bytes, byte value);
public native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
public native void freeMemory(long address);
  • 内存屏障
public native void loadFence();
public native void storeFence();
public native void fullFence();
  • 对象操作
public native Object getObject(Object var1, long var2);
public native void putObject(Object var1, long var2, Object var4);

public native void putOrderedObject(Object var1, long var2, Object var4);
public native void putOrderedInt(Object var1, long var2, int var4);
public native void putOrderedLong(Object var1, long var2, long var4);

public native Object getObjectVolatile(Object var1, long var2);
public native void putObjectVolatile(Object var1, long var2, Object var4);

public native long objectFieldOffset(Field var1);

public native int arrayBaseOffset(Class<?> var1);
public native int arrayIndexScale(Class<?> var1);
  • 数据操作
public native long getAddress(long var1);
public native void putAddress(long var1, long var3);

  • CAS操作
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
  • 线程调度
public native void unpark(Object var1);
public native void park(boolean var1, long var2);
  • Class操作
public native long staticFieldOffset(Field var1);
public native Object staticFieldBase(Field var1);
public native boolean shouldBeInitialized(Class<?> var1);
public native void ensureClassInitialized(Class<?> var1);
public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);
public native Class<?> defineAnonymousClass(Class<?> var1, byte[] var2, Object[] var3);
public native Object allocateInstance(Class<?> var1);
  • 系统信息
public native int addressSize();
public native int pageSize();

volatile

volatile关键字可以保证共享变量的内存可见性,当一个共享变量被volatile关键字修饰时,实际就是告诉JVM,这个共享变量是不稳定的,在使用时必须到主存中读取。volatile关键字也可以实现顺序性,当读写一个被volatile关键字修饰的共享变量时,volatile关键字会禁止指令重排序。volatile底层通过插入内存屏障的方式实现:

  • 写屏障:写屏障保证在该屏障之前对共享变量的改动都会同步到主存当中,并且不会进行指令重排。
  • 读屏障:读屏障保证在该屏障之后对共享状态的读取都是主存中的最新数据,并且不会进行指令重排。

若想只通过volatile关键字保证线程安全,必须同时满足以下条件:

  • 不能是一个组合的临界变量。
  • 运算结果并不依赖共享变量的当前值,或者只能由一条线程修改共享变量的值。

volatile关键字一个典型的应用就是单例模式,2处实例化对象的过程分为多步:分配内存空间、初始化、将对象地址赋值给INSTANCE引用变量,如果没有使用volatile关键字修饰INSTANCE的话,那么赋值步骤可能会在初始化步骤之前,此时如果另一个线程在1处访问一个未初始化的对象就会产生异常。

public class Singleton {
    
    volatile private static Singleton INSTANCE=null;
    
    public static Singleton getInstance(){
        if (INSTANCE==null){//1
            synchronized (Singleton.class){
                if (INSTANCE==null){
                    INSTANCE=new Singleton();//2
                }
            }
        }
        return INSTANCE;
    }
}

原子类

原子类是一些通过CAS保证原子性操作特征的类。CAS是Compare And Swap比较并交换的缩写,在CAS中有三个值:

  • Var:要更新的值
  • Expected:预期的值
  • New:新值

一次CAS操作的流程如下:判断V是否等于E,如果是则将V修改为N,否则就自旋重复这个过程。Java中的CAS由Unsafe类中的本地方法实现。这些本地方法原子性由操作系统或CPU实现。如果从乐观锁和悲观锁的角度对Java中的锁进行分类,那么对象锁和AQS都是悲观锁,因为它们在访问临界资源时都会先加锁,只有获得锁的线程才能访问临济资源。而CAS是乐观锁,任何线程在访问临界资源的时都不需要加锁,并且只有在满足条件的时候才能修改临界资源,如果不满足条件就会一直尝试,直到满足条件为止。但是,CAS自身还存在着一些问题:

  • ABA问题:线程一将共享状态A改成了B然后又改成了A,那么线程二将不会知道线程一的第一次修改。这就是ABA问题。

JUC包下的原子类如下所示:

  • 原子整数:
    • AtomicInteger:整型原子类
    • AtomicLong:长整型原子类
    • AtomicBoolean:布尔型原子类
  • 原子引用:
    • AtomicReference:引用类型原子类
    • AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
    • AtomicMarkableReference :原子更新带有标记位的引用类型
  • 原子数组:
    • AtomicIntegerArray:整型数组原子类
    • AtomicLongArray:长整型数组原子类
    • AtomicReferenceArray:引用类型数组原子类
  • 原子更新器:一个基于反射,支持对指定类的指定字段进行原子更新的类。使用原子更新器更新的字段必须是可访问的、必须被volatile修饰、不能被static修饰。
    • AtomicIntegerFieldUpdater:原子更新整型字段的更新器
    • AtomicLongFieldUpdater:原子更新长整型字段的更新器
    • AtomicReferenceFieldUpdater:原子更新引用类型字段的更新器
  • 原子累加器:
    • DoubleAdder:
    • LongAdder:
    • DoubleAccumulator:
    • LongAccumulator:

对象锁

Java中每个对象都有一个对象锁,每个对象的对象锁最多只能由同一个线程获得一次或多次。通过synchronized关键字即可以使用对象锁。synchronized的使用有以下三种形式:

  • synchronized修饰实例方法:进入同步代码块的线程会获得当前对象的对象锁。
synchronized public void method(){
    //synchronizedCode
}
  • synchronized修饰静态方法:进入同步代码块的线程会获得方法所在类的Class对象的对象锁。
synchronized static public void method(){
	//synchronizedCode
}
  • synchronized修饰代码块:进入同步代码块的线程会获得synchronized关键字指定对象的对象锁。
public void method(){
	//...
    synchronized (/*AnyObject*/){
        //synchronizedCode
    }
    //...
}

对象锁的实现

在JDK6之前,对象锁通过操作系统层面的管程实现,在JDK6之后,Java从JVM层面对对象锁进行了优化,通过锁记录实现。

管程实现方式

以管程实现的对象锁成为重量级锁,管程的逻辑结构如下图所示,当一个线程获得对象锁时,就会成为管程的Owner,其它尝试获取锁的线程会进入管程的EntryList。当Owner为空时会唤醒EntryList中的线程,被唤醒的线程通过调度重新获取对象锁。

在这里插入图片描述

锁记录实现方式

对象的对象头(以HotSpot为例)中有一片区域叫做Mark Word,用于存储对象运行时的一些状态,它的结构如下:

在这里插入图片描述

JDK6以后对象锁分为以下几个状态(级别由低到高):

  • 无锁状态
  • 偏向锁状态
  • 轻量级锁状态
  • 重量级锁状态

在获取对象锁的时候会伴随着一个锁升级的过程,这个过程是单向的,对象锁的每一个状态都会在Mark Word相应的标志位内体现。在优化后获取对象锁的流程如下:

  • 当只有一个线程获取对象锁时,JVM会把Mark Word中的lock标志位设置为01,把biased_lock标志位设置为1,表示进入偏向模式,同时使用CAS操作把获取到这个锁的线程ID记录到Mark Word中,如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的代码块时,JVM都不用再进行任何同步操作。如果操作失败,则需要进行锁升级。

  • 之后一旦发现其它线程尝试获取该对象锁,偏向模式会立即失败,失败后标志位恢复到可偏向(可重新获取偏向锁)或未锁定状态。如果是未锁定状态,JVM首先在当前线程内创建一个锁记录对象,用于存储对象Mark Word的拷贝:

在这里插入图片描述

  • 然后JVM使用CAS操作将对象的Mark Word替换为指向锁记录的指针。如果CAS操作成功了,该线程就获得了该对象的轻量级锁。
  • 如果CAS操作失败了,那就意味着至少存在一条线程与当前线程竞争该对象的对象锁,JVM首先会检查对象的Mark Word是否指向当前线程的锁记录,如果是,说明当前线程已经拥有了这个对象的锁;否则就说明这个对象的对象锁已经被其它线程抢占了,那么当前线程就会尝试自旋来获取锁,当自旋次数达到临界值时,轻量级锁就不再有效,必须进行锁膨胀将轻量级锁变为重量级锁。
  • 当释放轻量级锁时,如果对象的对象头仍指向当前线程的锁记录,那就使用CAS操作将对象的指向执行锁记录的指针用锁记录中的Mark Word替换。
  • 如果替换失败了,说明对象锁的状态重量级锁,此时就进入重量级锁的释放过程。

线程通信

通过以下方法进行线程通信的前提是获取对象的对象锁,此时的对象锁一定处于重量级锁状态,当一个线程成为管程的Owner时,发现某些运行条件不满足,此时可以使用wait方法使线程进入WaitSet,进入WaitSet的线程会在Owner线程调用notify方法时被唤醒,唤醒后的线程进入EntryList进行锁抢夺。

void wait() //加入锁对象的等待序列
void wait(long timeout) //计时等待
void notify() //随机挑选一个等待线程激活
void notifyAll() //激活所有的等待线程

值得注意的是调用wait方法的线程在哪里等待就会在哪里被唤醒,所以下面代码中的if语句应换成while,以免线程被唤醒时跳过if语句而产生虚假唤醒现象。

synchronized (obj){
    if (condition){
        obj.wait()
    }
}

AQS

AQS是一个用于构建锁或其它同步组件的重量级框架。

AbstractOwnableSynchronizer
AbstractQueuedSynchronizer
AbstractQueuedLongSynchronizer

在具体的实现上,它维护了一个代表临界资源状态的变量(state)和一个用于存放被阻塞线程的等待队列(CLH锁队列的变体)。

private volatile int state;
private transient volatile Node head;
private transient volatile Node tail;

在这里插入图片描述

在临界资源的访问上,它提供了以下两种方式:

  • 独占式:同一时间只允许一个线程访问临界资源。
  • 共享式:允许多个线程访问临界资源。

状态变量

状态变量在不同的临界资源访问方式上有不同的含义:

  • 独占式:表示临界资源的锁定状态,0表示未锁定,1表示已锁定。
  • 共享式:表示可以访问临界资源线程的个数。

AQS提供以下方法操作状态变量:

protected final int getState()
protected final void setState(int newState)
protected final boolean compareAndSetState(int expect, int update)

等待队列

等待队列由一个带有哨兵结点的双向链表实现,链表结点是对等待线程的封装,它包含线程本身以及结点的状态,它的实现如下:

static final class Node {
    //标记,表示节点正在共享模式中等待
    static final Node SHARED = new Node();
    
    //标记,表示节点正在独占模式下等待
    static final Node EXCLUSIVE = null;
    
    //等待状态
    volatile int waitStatus;
    static final int CANCELLED =  1;
    static final int SIGNAL    = -1;
    static final int CONDITION = -2;
    static final int PROPAGATE = -3;
    
    //等待线程
    volatile Thread thread;
    
	//前结点
    volatile Node prev;
	//后结点
    volatile Node next;
    //一个正常排队的后继节点
    Node nextWaiter;
    ...
}

waitStatus变量表示结点的等待状态:

  • 0:新结点入队时的状态。
  • CANCELLED(1):表示当前结点已取消排队(超时或被中断),会触发变更为此状态,进入该状态后的结点的状态将不会再变化。
  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  • CONDITION(-2):表示结点等待在Condition上,当其它线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE(-3):共享模式下,前驱结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。

等待队列的核心功能就是调度处于不同状态的结点,可以归结为以下几点:

  • 将等待获取临界资源的线程加入队列进行排队。
  • 清除在队列内但不参与排队的节点。
  • 让在队列内并且参与排队的节点尝试获取临界资源或阻塞。
  • 在满足条件下唤醒在队列内并且参与排队并且处于阻塞状态的节点。
  • 保存处在排队节点的中断标志,排队结束后再响应中断。

AQS核心方法分析:

//将当前线程根据指定模式加入等待队列
private Node addWaiter(Node mode) {
	//构造结点
    Node node = new Node(Thread.currentThread(), mode);
    //将node加入队尾
    ...
    return node;
}

//排队过程中尝试获取临界资源,成功获取临界资源后返回排队过程中是否被请求中断
final boolean acquireQueued(final Node node, int arg) {
	//是否成功获取资源
    boolean failed = true;
    try {
    	//自旋过程中是否被中断过
        boolean interrupted = false;
        for (;;) {
        	//前驱结点
            final Node p = node.predecessor();
            //如果前驱节点是头节点(说明自己有资格获取锁)并且成功获取锁
            if (p == head && tryAcquire(arg)) {
                setHead(node);//将当前节点设置为头节点
                p.next = null;
                failed = false;//成功获取锁
                return interrupted;//自旋过程中是否被中断
            }
            //如果可以休息那么就阻塞休息
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //如果被请求中断那么将interrupted设置为true
                interrupted = true;
        }
    } finally {
    	//自旋失败(超时或被中断)
        if (failed)
        	//取消获取锁
            cancelAcquire(node);
    }
}

//获取临界资源失败后是否可以阻塞(休息)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;//前驱节点的等待标志
    //如果前驱节点的状态为SIGNAL,那就可以阻塞了
    if (ws == Node.SIGNAL)
        return true;
    //如果前驱节点已经放弃获取临界资源了(超时、中断或被取消)
    if (ws > 0) {
    	//找到一个正常排队的前驱节点
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;//排在这个正常前驱节点的后边
    //如果前驱节点的状态不为SIGNAL并且没有放弃排队
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);//将前驱节点的等待状态设置为SIGNAL
    }
    return false;//继续排队并获取临界资源
}

//继续排队但停止获取临界资源并返回当前线程是否被请求中断
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

//取消排队
private void cancelAcquire(Node node) {
    //避免空结点
    if (node == null)
        return;
	//结点封装的线程设置为null
    node.thread = null;
    //寻找正常排队的前驱结点
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    //记录前驱节点的后继节点    
    Node predNext = pred.next;
    //将当前节点的等待状态设置为CANCELLED
    node.waitStatus = Node.CANCELLED;

    // 如果当前结点是尾结点
    if (node == tail && compareAndSetTail(node, pred)) {
    	//直接将前驱结点之后的结点清除
        compareAndSetNext(pred, predNext, null);
    } else {
    	//保存前驱结点的等待状态
        int ws;
        //如果前驱结点不是头节点
        if (pred != head &&
        	//并且前驱节点的等待状态为SIGNAL
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             //或者前驱节点正常排队并将前驱节点的等待状态成功设置为SIGNAL
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            //并且前驱节点的线程不为空
            pred.thread != null) {
            Node next = node.next;
            //如果当前节点的后继节点不为null并且正在排队
            if (next != null && next.waitStatus <= 0)
            	//将前驱节点的后继节点设置为当前节点的后继节点
                compareAndSetNext(pred, predNext, next);
        } else {
        	//如果前驱节点是头节点或者前驱节点的状态不是SIGNAL也无法设置为SIGNAL并且前驱节点的线程为null,就唤醒当前结点的后继节点
            unparkSuccessor(node);
        }

        node.next = node; // help GC
    }
}

//唤醒当前结点的后继节点
private void unparkSuccessor(Node node) {
    //记录参数节点的状态
    int ws = node.waitStatus;
    //如果参数节点正常排队
    if (ws < 0)
    	//将参数节点的状态设置为0
        compareAndSetWaitStatus(node, ws, 0);
	//记录参数节点的后继节点
    Node s = node.next;
    //如果后继节点是null或者已取消排队
    if (s == null || s.waitStatus > 0) {
        s = null;
        //遍历队列,寻找一个不为null并且正常排队的结点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
    	//唤醒该结点
        LockSupport.unpark(s.thread);
}

AQS的实现流程

AQS维护了状态变量和等待队列的操作方法,在使用AQS框架实现同步器时只需要实现以下方法即可:

protected boolean tryAcquire(int)//独占方式。尝试获取资源,成功则返回true,失败则返回false。
protected boolean tryRelease(int)//独占方式。尝试释放资源,成功则返回true,失败则返回false。
protected int tryAcquireShared(int)//共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
protected boolean tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true,失败则返回false。
protected boolean isHeldExclusively()//该线程是否正在独占资源。只有用到condition才需要去实现它。

以独占锁为例,一个具体的实现如下:

public class IdentifyLock {
    private static class Sync extends AbstractQueuedSynchronizer{
        @Override
        protected boolean tryAcquire(int arg) {
            if (compareAndSetState(0,arg)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }else {
                return false;
            }
        }

        @Override
        protected boolean tryRelease(int arg) {
            if (getState()==0||compareAndSetState(1,arg)){
                setExclusiveOwnerThread(null);
                return true;
            }else{
                return false;
            }
        }
    }
}

AQS的使用流程

通过acquire方法就可以通过AQS的某一具体实现安全的获取临界资源,该方法的源码分析如下:

public final void acquire(int arg) {
	//通过tryAcquire尝试获取临界资源,如果成功直接返回
    if (!tryAcquire(arg) &&
    	//获取失败则通过addWaiter方法将当前线程加入等待队列,并将等待队列设置为独占模式
    	//加入队列后通过acquireQueued排队获取临界资源,在排队过程中被请求中断时返回true
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //向线程发送排队过程中积攒的终端请求
        selfInterrupt();
}

acquire方法的执行流程图如下:

请添加图片描述

通过release方法就可以通过AQS的某一具体实现安全的释放临界资源,该方法的源码分析如下:

public final boolean release(int arg) {
	//使用tryRelease尝试释放临界资源,如果失败直接返回
    if (tryRelease(arg)) {
        Node h = head;
        //如果头节点不为空(那就为自己)并且还有后继节点
        if (h != null && h.waitStatus != 0)
        	//唤醒后继节点
            unparkSuccessor(h);
        //成功释放资源
        return true;
    }
    return false;
}

继续完善上文的中的IdentifyLock

public class IdentifyLock {

	...

    private final static Sync SYNC=new Sync();

    public void lock(){
        SYNC.acquire(1);
    }

    public boolean unlock(){
        return SYNC.release(0);
    }
}

以上分析都是在独占模式下,在共享模式下过程基本相同,区别在于在唤醒后继节点时,如果还有剩余获取数会继续唤醒后继节点。

线程通信

void await()
boolean	await(long time, TimeUnit unit)//使当前线程等待,直到收到信号或被中断,或指定的等待时间结束
long awaitNanos(long nanosTimeout)//返回还要等待的时间
void awaitUninterruptibly()
boolean	awaitUntil(Date deadline)
void signal()
void signalAll()

JDK中AQS的实现

  • 独占式实现:Lock定义了一种多条件的、可中断的、可定时的、可公平的以及可重入的锁,ReentrantLock是它的一个具体的实现。ReentrantLock不仅具有与synchronized相同的功能,还在此基础上提供了更高的灵活性。它与synchronized不同之处在于:

    • ReentrantLock默认是非公平锁,但可以设置为公平锁;synchronized只能是非公公平锁。
    • ReentrantLock可以与多个Condition绑定,从而提供多种等待和通知条件。
    • ReentrantLock可以在阻塞时响应中断,synchronized不可以。
    • 如果在释放锁之前出现异常,那么 ReentrantLock将不会被释放,而synchronized在出现异常时可以自动释放对象锁。因此在使用 ReentrantLock时通常把释放锁的语句放在finally代码中。
  • 共享式实现:

    • Semaphore(信号量):一次可以允许多个线程获取临界资源。
    • CountDownLatch(倒计时锁):一次可以等待多个线程访问临界资源完成。值得注意的是一个倒计时器实例只能使用一次。
    • CyclicBarrier(循环栅栏):一次可以等待多个线程到达临界资源,并且循环栅栏可以重复使用。
  • 混合式实现:

    • ReadWriteLock:在一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时进行的情况下,可以使用读写锁。读锁是一个共享锁,不支持条件变量。写锁是一个独占锁,两者都能造成死锁。它的可重入性还允许从写锁降级为读锁,先获取写锁,然后是读锁,然后释放写锁。但是,从读锁升级到写锁是不可能的。
    • StampedLock:StampedLock是读写锁的升级版,它在读写锁的基础上使得读锁与写锁之间不会相互阻塞,而是使用了乐观读锁的方式。它不支持条件变量和可重入。

集合安全

JUC在集合框架的基础上加入了一些含有CopyOnWriteConcurrent以及Blocking字段的线程安全集合,下文将选择具有代表性的线程安全集合进行源码分析。

List
CopyOnWriteArrayList
Set
CopyOnWriteArraySet
SortedSet
NavigableSet
ConcurrentSkipListSet
Queue
ConcurrentLinkedQueue
Deque
BlockingDeque
ConcurrentLinkedDeque
LinkedBlockingDeque
BlockingQueue
TransferQueue
DelayQueue
LinkedBlockingQueue
PriorityBlockingQueue
ArrayBlockingQueue
SynchronousQueue
LinkedTransferQueue
Map
SortedMap
ConcurrentMap
NavigableMap
ConcurrentNavigableMap
ConcurrentHashMap
ConcurrentSkipListMap

CopyOnWriteArrayList

CopyOnWriteArrayList所有读方法都不加锁,只有写方法加锁,并且在写方法内通过写时复制技术来保证读写(写读)共享,也就是在写操作时先复制一个内部存储结构的副本,在副本内写入,之后再将原引用指向副本:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    //加锁
    lock.lock();
    try {
    	//复制底层存储结构
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        //将先添加元素加入副本中
        newElements[len] = e;
        //将原引用指向副本
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

CopyOnWriteArrayList的优点在于实现了读读共享、写写互斥、读写(写读)共享的同步策略保证线程安全,效率高;缺点在于它只能保证最终结果的完整性,即在过程中读操作读到的数据可能不是最新加入的数据。

ConcurrentHashMap

ConcurrentXxx并不是在每个方法上都在同一个锁同步,而是使用分段锁机制来实现更大程度上的共享,在这种机制下,允许读操作和一定数量的写操作并发访问。ConcurrentXxx会存在弱一致性问题,比如在使用迭代器迭代时,虽然可以修改但是迭代的结果可能是旧的,这是一种fail-safe机制。

ConcurrentSkipListMap

BlockingQueue

阻塞队列是一个可以在线程之间共享的队列。当队列为空时,从队列获取元素的操作将被阻塞,直到其它线程添加元素。当队列为满时,向队列添加元素的操作将被阻塞,直到其它线程移除元素。使用阻塞队列的好处是我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,这一切都由阻塞队列完成。

在这里插入图片描述

在阻塞队列中提供了以下四类API来适应不同的场景:

  • 抛出异常类:
//成功添加时返回true,队列已满抛出异常
//添加元素为null时抛出NullPointerException 
boolean add(E e)
//成功删除返回true,队列为空时抛出异常
boolean remove()
//检索但不删除队列头元素,队列为空时抛出异常
E element()
  • 返回特殊值类:
//成功添加时返回true,队列已满返回false
//添加元素为null时抛出NullPointerException 
boolean offer(E e)
//成功删除返回true,队列为空返回null
E poll()
//检索但不删除队列头元素,队列为空返回null
E peek()
  • 阻塞类:
//成功添加时直接返回,队列已满时阻塞
//添加元素为null时抛出NullPointerException 
//阻塞时被中断抛出InterruptedException 
void put(E e)
//队列为空时阻塞
//阻塞时被中断抛出InterruptedException 
E take()
  • 超时类:
//成功添加返回true,队列已满时等待指定时间,等待超时返回false
//添加元素为null时抛出NullPointerException 
//阻塞时被中断抛出InterruptedException 
boolean offer(E e, long timeout, TimeUnit unit)
//队列为空等待指定时间,等待超时返回null
//阻塞时被中断抛出InterruptedException 
E poll(long timeout, TimeUnit unit)

JDK提供的实现类如下:

  • ArrayBlockingQueue:存储结构上基于定长数组实现。线程安全上基于ReentrantLock实现。
  • LinkedBlockingQueue:存储结构上基于链表实现,默认大小为Integer.MAX_VALUE。线程安全上基于ReentrantLock实现。
  • PriorityBlockingQueue:存储结构上由基于数组的平衡二叉堆实现。线程安全上基于ReentrantLock实现。
  • SynchronousQueue:同步队列中的同步指的是当一个线程向其添加一个元素时会阻塞至线程将其取出,反之亦然,因此它没有提供任何空间存储元素。线程安全上基于LockSupport实现。
  • DelayQueue:底层由由PriorityBlockingQueue实现,其中的元素只有在延时时间到达后才能取出。
  • TransferQueueTransferQueue似于BlockingQueueSynchronousQueue的组合,主要体现在transfer方法,当有读线程阻塞时,调用transfer方法的写线程就不会将元素存入队列,而是直接将元素传递给读线程;如果调用transfer方法的写线程发现没有正在等待的读线程,则会将元素加入队列,然后会阻塞等待,直到有一个读线程来获取该元素。该队列是一个接口,JDK提供了一个LinkedTransferQueue实现。
void transfer(E e)  
//传输一个值,或者尝试在给定超时时间内传输这个值,这个调用将阻塞,直到另一个线程将元素消费
boolean tryTransfer(E e, long timeout, TimeUnit unit)  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/397761.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

合作伙伴管理软件如何帮助简化您的业务流程?

随着合作伙伴数量的增加&#xff0c;企业需要处理更多的信息和数据&#xff0c;并在更多的项目上进行协调和管理。这会增加企业的复杂性和工作量&#xff0c;使管理变得更加困难。同时&#xff0c;随着合作伙伴项目数量的增加&#xff0c;企业需要与更多的合作伙伴进行协调和沟…

Echarts 水波图实现

开发的项目中需要实现这样一个水波图&#xff0c;例如下图在echarts官网中找了很久没找到&#xff0c;后面是在Echarts社区中找到的&#xff0c;实现了大部分的样式&#xff0c;但是还有一些数据的展示没有实现。水波图的数值展示是默认整数百分比&#xff0c;我的需求是需要保…

【算法数据结构体系篇class14、15】:并查集

一、并查集1)有若干个样本a、b、c、d…类型假设是V2)在并查集中一开始认为每个样本都在单独的集合里3)用户可以在任何时候调用如下两个方法&#xff1a;boolean isSameSet(V x, V y) : 查询样本x和样本y是否属于一个集合void union(Vx, V y) : 把x和y各自所在集合的所有样本合并…

带你玩转modbusTCP通信

modbus TCP Modbus TCP是一种基于TCP/IP协议的Modbus通信协议&#xff0c;它是Modbus协议的一种变体&#xff0c;用于在以太网上进行通信。Modbus TCP协议是一种开放的通信协议&#xff0c;它支持多种编程语言和操作系统&#xff0c;并且可以在不同的硬件和软件平台上进行通信…

从0开始学python -49

Python MySQL - mysql-connector 驱动 -2 插入数据 插入数据使用 “INSERT INTO” 语句&#xff1a; demo_mysql_test.py: 向 sites 表插入一条记录。 import mysql.connectormydb mysql.connector.connect(host"localhost",user"root",passwd"…

液氮恒温器概述

恒温器是直接或间接控制一个或多个热源和冷源来维持所要求的温度的一种装置。 恒温器要实现这种功能&#xff0c;就必须具有一个敏感元件和一个转换器&#xff0c;敏感元件量度出温度的变化&#xff0c;并对转换器产生所需的作用。转换器把来自敏感元件的作用转换成对改变温度…

创建型设计模式(C++)

文章目录1.简单工厂模式&静态工厂模式2.工厂方法模式3.抽象工厂模式4.原型模式5.单例模式a.饿汉式b.懒汉式6.建造者模式&#xff08;生成器模式&#xff09;创建型模式提供了创建对象的机制&#xff0c;旨在提升已有代码的灵活性和可复用性。 部分插图来自&#xff1a; ht…

20230308 Apdl lsdyna两杆撞击案例学习笔记

本次模拟使用的是ANSYS 16.0 一、设置Element type 首先打开APDL界面 添加element type 在LS-DYNA Explicit选择条件下,选择3D solid 164 二、设置材料类型 选择material models 选择Elastic-Isotropic-输入 Density:密度 EX:杨氏模量 NUXY:泊松比 三、几何模型建…

小应用记账本-第2章-数据库设计

小应用记账本-第2章-数据库设计 在上一章《小应用记账本-第1章-需求分析》已经罗列了我们需要的功能&#xff0c;因为很简单&#xff0c;所以这一章就来设计数据库吧。 Account表&#xff1a;账户表 字段名类型说明取值idint账户idaccount_namevarchar账户名称remaining_sumd…

【目标检测论文解读复现NO.33】改进YOLOv5的新能源电池集流盘缺陷检测方法

前言此前出了目标改进算法专栏&#xff0c;但是对于应用于什么场景&#xff0c;需要什么改进方法对应与自己的应用场景有效果&#xff0c;并且多少改进点能发什么水平的文章&#xff0c;为解决大家的困惑&#xff0c;此系列文章旨在给大家解读最新目标检测算法论文&#xff0c;…

python+opencv生成较真实的车牌号码图片

本文参考github代码&#xff1a;https://github.com/loveandhope/license-plate-generator 效果&#xff1a; 一、代码目录结构&#xff1a; font目录下存放车牌中文、字符的ttf字体 images目录下存放蓝色底牌、新能源绿色底牌、污渍&#xff08;噪声&#xff09;的图片 完整…

嵌入式物联网毕业设计选题智能图像识别项目-stm32mp157 linux开发板

stm32mp157开发板FS-MP1A是华清远见自主研发的一款高品质、高性价比的Linux单片机二合一的嵌入式教学级开发板。开发板搭载ST的STM32MP157高性能微处理器&#xff0c;集成2个Cortex-A7核和1个Cortex-M4 核&#xff0c;A7核上可以跑Linux操作系统&#xff0c;M4核上可以跑FreeRT…

ControlNet-有条件图文生成论文阅读

文章目录摘要算法&#xff1a;ControlNetControlNet in Image Diffusion ModelTrainingImproved Training实验Canny edgesHough linesHuman scribblesHED boundary mapOpenpifpaf poseOpenposeADE20K segmentation mapCOCO-Stuff segmentation mapDIODE normal mapDepth-to-Ima…

蓝桥冲刺31天之第六天

今天是摆子的一天&#xff0c;明天我要肝一整天的第四题&#xff01;&#xff01;&#xff01; PS&#xff1a;一个普通的排序罢了 import java.io.*; import java.util.Arrays; import java.util.Scanner;/*** ClassName 考勤刷卡* Description TODO* Author 小怂很怂* Date 2…

DataX与DB2导入导出案例

DataX与DB2导入导出案例 文章目录DataX与DB2导入导出案例0. 写在前面1. DB2介绍2. DB2数据库对象关系3. 安装前的准备3.1 安装依赖3.2 修改配置文件 sysctl.conf3.3 修改配置文件 limits.conf4. 安装4.1 预检查4.2 添加组和用户4.3 创建实例4.4 创建实例库、开启服务4.5 连接5.…

在CentOS7上静默安装Oracle19c

1.下载Oracle 官方安装包下载路径&#xff08;需要登录Oracle账号&#xff09;&#xff1a; https://www.oracle.com/database/technologies/oracle-database-software-downloads.html#19c 可选择windows/Linux平台对应的安装包&#xff0c;我选择Linux x86-64、ZIP包下载&…

分析linux内核移植中vmlinux可执行文件是如何生成的?以及 uImage/zImage/Image/vmlinx之间关系

一&#xff1a;分析linux内核移植中vmlinux可执行文件是如何生成的&#xff1f; 1&#xff1a;进入内核源码顶层目录下打开Makefile文件&#xff0c;搜索vmlinux 这里构建vmlinux的命令使用了makefile的内置函数call。这是一个比较特殊的内置函数&#xff0c;make使用它来引用…

Go语言学习编程实践:五种模式解决go中的并发问题

五种模式解决go中的并发问题For-Select-Done扇入模式从流中获取前 n 个值订阅模式地图模式过滤模式For-Select-Done 我们应该防止程序中发生任何泄露。所以我们应该对于留在程序中的go例程发送信号&#xff0c;让它知道它可以退出。 最常见的就是将for-select循环与通道结合起…

UEFI启动流程

以上是UEFI系统运行的7个阶段&#xff0c;下边是详细描述&#xff1a; SEC阶段&#xff1a;&#xff08;安全验证&#xff09; 1、接收和处理系统的启动&#xff0c;重启&#xff0c;异常信号&#xff1b; 2、SEC阶段特色功能“Cache As RAM&#xff08;CAR&#xff09;”&am…

英伦四地到底是什么关系?

英格兰、苏格兰、威尔士和北爱尔兰四地到底是什么关系&#xff0c;为何苏格兰非要独立&#xff1f;故事还要从中世纪说起。大不列颠岛位于欧洲西部&#xff0c;和欧洲大陆隔海相望。在古代&#xff0c;大不列颠岛和爱尔兰属于凯尔特人的领地。凯尔特人是欧洲西部一个庞大的族群…