Java虚拟机(jvm)

深入理解虚拟机之Java内存区域:

  1. 介绍下Java内存区域(运行时数据区)。

  2. 对象的访问定位的两种方式。

深入理解虚拟机之垃圾回收

  1. 如何判断对象是否死亡(两种方法)。

  2. 简单的介绍一下强引用、软引用、弱引用、虚引用(虚引用与软引用和弱引用的区别、使用软引用能带来的好处)。

  3. 垃圾收集有哪些算法,各自的特点?

  4. HotSpot为什么要分为新生代和老年代?

  5. 常见的垃圾回收器有那些?

  6. 介绍一下CMS,G1收集器。

  7. Minor Gc和Full GC 有什么不同呢?

虚拟机性能监控和故障处理工具

  1. JVM调优的常见命令行工具有哪些?

深入理解虚拟机之类文件结构

  1. 简单介绍一下Class类文件结构(常量池主要存放的是那两大常量?Class文件的继承关系是如何确定的?字段表、方法表、属性表主要包含那些信息?)

深入理解虚拟机之虚拟机类加载机制

  1. 简单说说类加载过程,里面执行了哪些操作?

  2. 对类加载器有了解吗?

  3. 什么是双亲委派模型?

  4. 双亲委派模型的工作过程以及使用它的好处。

推荐阅读

深入理解虚拟机之虚拟机字节码执行引擎

《深入理解 Java 内存模型》读书笔记 (非常不错的文章)

全面理解Java内存模型(JMM)及volatile关键字

全面理解Java内存模型(JMM)及volatile关键字

关联文章:

深入理解Java类型信息(Class对象)与反射机制

深入理解Java枚举类型(enum)

深入理解Java注解类型(@Annotation)

深入理解Java类加载器(ClassLoader)

深入理解Java并发之synchronized实现原理

Java并发编程-无锁CAS与Unsafe类及其并发包Atomic

深入理解Java内存模型(JMM)及volatile关键字

剖析基于并发AQS的重入锁(ReetrantLock)及其Condition实现原理

剖析基于并发AQS的共享锁的实现(基于信号量Semaphore)

并发之阻塞队列LinkedBlockingQueue与ArrayBlockingQueue

本篇主要结合博主个人对Java内存模型的理解以及相关书籍内容的分析作为前提,对JMM进行较为全面的分析,本篇的写作思路是先阐明Java内存区域划分、硬件内存架构、Java多线程的实现原理与Java内存模型的具体关系,在弄明白它们间的关系后,进一步分析Java内存模型作用以及一些必要的实现手段,以下是本篇主要内容(如有错误,欢迎留言,谢谢!)

 

 

理解Java内存区域与Java内存模型

Java内存区域

Java虚拟机在运行程序时会把其自动管理的内存划分为以上几个区域,每个区域都有的用途以及创建销毁的时机,其中蓝色部分代表的是所有线程共享的数据区域,而绿色部分代表的是每个线程的私有数据区域。

  • 方法区(Method Area):

    方法区属于线程共享的内存区域,又称Non-Heap(非堆),主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。值得注意的是在方法区中存在一个叫运行时常量池(Runtime Constant Pool)的区域,它主要用于存放编译器生成的各种字面量和符号引用,这些内容将在类加载后存放到运行时常量池中,以便后续使用。

  • JVM堆(Java Heap):

    Java 堆也是属于线程共享的内存区域,它在虚拟机启动时创建,是Java 虚拟机所管理的内存中最大的一块,主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,注意Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC 堆,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。

  • 程序计数器(Program Counter Register):

    属于线程私有的数据区域,是一小块内存空间,主要代表当前线程所执行的字节码行号指示器。字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

  • 虚拟机栈(Java Virtual Machine Stacks):

    属于线程私有的数据区域,与线程同时创建,总数与线程关联,代表Java方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。每个方法从调用直结束就对于一个栈桢在虚拟机栈中的入栈和出栈过程,如下(图有误,应该为栈桢):

  • 本地方法栈(Native Method Stacks):

    本地方法栈属于线程私有的数据区域,这部分主要与虚拟机用到的 Native 方法相关,一般情况下,我们无需关心此区域。

这里之所以简要说明这部分内容,注意是为了区别Java内存模型与Java内存区域的划分,毕竟这两种划分是属于不同层次的概念。

Java内存模型概述

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图

需要注意的是,JMM与Java内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开的(稍后会分析)。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义。关于JMM中的主内存和工作内存说明如下

  • 主内存

    主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。

  • 工作内存

    主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

弄清楚主内存和工作内存后,接了解一下主内存与工作内存的数据存储类型以及操作方式,根据虚拟机规范,对于一个实例对象中的成员方法而言,如果方法中包含本地变量是基本数据类型(boolean,byte,short,char,int,long,float,double),将直接存储在工作内存的帧栈结构中,但倘若本地变量是引用类型,那么该变量的引用会存储在功能内存的帧栈中,而对象实例将存储在主内存(共享数据区域,堆)中。但对于实例对象的成员变量,不管它是基本数据类型或者包装类型(Integer、Double等)还是引用类型,都会被存储到堆区。至于static变量以及类本身相关信息将会存储在主内存中。需要注意的是,在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存,简单示意图如下所示:

硬件内存架构与Java内存模型

硬件内存架构

正如上图所示,经过简化CPU与内存操作的简易图,实际上没有这么简单,这里为了理解方便,我们省去了南北桥并将三级缓存统一为CPU缓存(有些CPU只有二级缓存,有些CPU有三级缓存)。就目前计算机而言,一般拥有多个CPU并且每个CPU可能存在多个核心,多核是指在一枚处理器(CPU)中集成两个或多个完整的计算引擎(内核),这样就可以支持多任务并行执行,从多线程的调度来说,每个线程都会映射到各个CPU核心中并行运行。在CPU内部有一组CPU寄存器,寄存器是cpu直接访问和处理的数据,是一个临时放数据的空间。一般CPU都会从内存取数据到寄存器,然后进行处理,但由于内存的处理速度远远低于CPU,导致CPU在处理指令时往往花费很多时间在等待内存做准备工作,于是在寄存器和主内存间添加了CPU缓存,CPU缓存比较小,但访问速度比主内存快得多,如果CPU总是操作主内存中的同一址地的数据,很容易影响CPU执行速度,此时CPU缓存就可以把从内存提取的数据暂时保存起来,如果寄存器要取内存中同一位置的数据,直接从缓存中提取,无需直接从主内存取。需要注意的是,寄存器并不每次数据都可以从缓存中取得数据,万一不是同一个内存地址中的数据,那寄存器还必须直接绕过缓存从内存中取数据。所以并不每次都得到缓存中取数据,这种现象有个专业的名称叫做缓存的命中率,从缓存中取就命中,不从缓存中取从内存中取,就没命中,可见缓存命中率的高低也会影响CPU执行性能,这就是CPU、缓存以及主内存间的简要交互过程,总而言之当一个CPU需要访问主存时,会先读取一部分主存数据到CPU缓存(当然如果CPU缓存中存在需要的数据就会直接从缓存获取),进而在读取CPU缓存到寄存器,当CPU需要写数据到主存时,同样会先刷新寄存器中的数据到CPU缓存,然后再把数据刷新到主内存中。

Java线程与硬件处理器

了解完硬件的内存架构后,接着了解JVM中线程的实现原理,理解线程的实现原理,有助于我们了解Java内存模型与硬件内存架构的关系,在Window系统和Linux系统上,Java线程的实现是基于一对一的线程模型,所谓的一对一模型,实际上就是通过语言级别层面程序去间接调用系统内核的线程模型,即我们在使用Java线程时,Java虚拟机内部是转而调用当前操作系统的内核线程来完成当前任务。这里需要了解一个术语,内核线程(Kernel-Level Thread,KLT),它是由操作系统内核(Kernel)支持的线程,这种线程是由操作系统内核来完成线程切换,内核通过操作调度器进而对线程执行调度,并将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这也就是操作系统可以同时处理多任务的原因。由于我们编写的多线程程序属于语言层面的,程序一般不会直接去调用内核线程,取而代之的是一种轻量级的进程(Light Weight Process),也是通常意义上的线程,由于每个轻量级进程都会映射到一个内核线程,因此我们可以通过轻量级进程调用内核线程,进而由操作系统内核将任务映射到各个处理器,这种轻量级进程与内核线程间1对1的关系就称为一对一的线程模型。如下图

如图所示,每个线程最终都会映射到CPU中进行处理,如果CPU存在多核,那么一个CPU将可以并行执行多个线程任务。

Java内存模型与硬件内存架构的关系

通过对前面的硬件内存架构、Java内存模型以及Java多线程的实现原理的了解,我们应该已经意识到,多线程的执行最终都会映射到硬件处理器上进行执行,但Java内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存(线程私有数据区域)和主内存(堆内存)之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,并不实际存在,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。(注意对于Java内存区域划分也是同样的道理)

JMM存在的必要性

在明白了Java内存区域划分、硬件内存架构、Java多线程的实现原理与Java内存模型的具体关系后,接着来谈谈Java内存模型存在的必要性。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,线程与主内存中的变量操作必须通过工作内存间接完成,主要过程是将变量从主内存拷贝的每个线程各自的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,如果存在两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱发线程安全问题。如下图,主内存中存在一个共享变量x,现在有A和B两条线程分别对该变量x=1进行操作,A/B线程各自的工作内存中存在共享变量副本x。假设现在A线程想要修改x的值为2,而B线程却想要读取x的值,那么B线程读取到的值是A线程更新后的值2还是更新前的值1呢?答案是,不确定,即B线程有可能读取到A线程更新前的值1,也有可能读取到A线程更新后的值2,这是因为工作内存是每个线程私有的数据区域,而线程A变量x时,首先是将变量从主内存拷贝到A线程的工作内存中,然后对变量进行操作,操作完成后再将变量x写回主内,而对于B线程的也是类似的,这样就有可能造成主内存与工作内存间数据存在一致性问题,假如A线程修改完后正在将数据写回主内存,而B线程此时正在读取主内存,即将x=1拷贝到自己的工作内存中,这样B线程读取到的值就是x=1,但如果A线程已将x=2写回主内存后,B线程才开始读取的话,那么此时B线程读取到的就是x=2,但到底是哪种情况先发生呢?这是不确定的,这也就是所谓的线程安全问题。

为了解决类似上述的问题,JVM定义了一组规则,通过这组规则来决定一个线程对共享变量的写入何时对另一个线程可见,这组规则也称为Java内存模型(即JMM),JMM是围绕着程序执行的原子性、有序性、可见性展开的,下面我们看看这三个特性。

Java内存模型的承诺

这里我们先来了解几个概念,即原子性?可见性?有序性?最后再阐明JMM是如何保证这3个特性。

原子性

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。比如对于一个静态变量int x,两条线程同时对他赋值,线程A赋值为1,而线程B赋值为2,不管线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操作是没有干扰的,这就是原子性操作,不可被中断的特点。有点要注意的是,对于32位系统的来说,long类型数据和double类型数据(对于基本数据类型,byte,short,int,float,boolean,char读写是原子操作),它们的读写并非原子性的,也就是说如果存在两条线程同时对long类型或者double类型的数据进行读写是存在相互干扰的,因为对于32位虚拟机来说,每次原子读写是32位的,而long和double则是64位的存储单元,这样会导致一个线程在写时,操作完前32位的原子操作后,轮到B线程读取时,恰好只读取到了后32位的数据,这样可能会读取到一个既非原值又不是线程修改值的变量,它可能是“半个变量”的数值,即64位数据被两个线程分成了两次读取。但也不必太担心,因为读取到“半个变量”的情况比较少见,至少在目前的商用的虚拟机中,几乎都把64位的数据的读写操作作为原子操作来执行,因此对于这个问题不必太在意,知道这么回事即可。

理解指令重排

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种

  • 编译器优化的重排

    编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  • 指令并行的重排

    现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序

  • 内存系统的重排

    由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。

其中编译器优化的重排属于编译期重排,指令并行的重排和内存系统的重排属于处理器重排,在多线程环境中,这些重排优化可能会导致程序出现内存可见性问题,下面分别阐明这两种重排优化可能带来的问题

编译器重排

下面我们简单看一个编译器重排的例子:

线程 1             线程 2
1: x2 = a ;      3: x1 = b ;
2: b = 1;         4: a = 2 ;

两个线程同时执行,分别有1、2、3、4四段执行代码,其中1、2属于线程1 , 3、4属于线程2 ,从程序的执行顺序上看,似乎不太可能出现x1 = 1 和x2 = 2 的情况,但实际上这种情况是有可能发现的,因为如果编译器对这段程序代码执行重排优化后,可能出现下列情况

线程 1              线程 2
2: b = 1;          4: a = 2 ; 
1:x2 = a ;        3: x1 = b ;

这种执行顺序下就有可能出现x1 = 1 和x2 = 2 的情况,这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。

处理器指令重排

先了解一下指令重排的概念,处理器指令重排是对CPU的性能优化,从指令的执行角度来说一条指令可以分为多个步骤完成,如下

  • 取指 IF
  • 译码和取寄存器操作数 ID
  • 执行或者有效地址计算 EX
  • 存储器访问 MEM
  • 写回 WB

CPU在工作时,需要将上述指令分为多个步骤依次执行(注意硬件不同有可能不一样),由于每一个步会使用到不同的硬件操作,比如取指时会只有PC寄存器和存储器,译码时会执行到指令寄存器组,执行时会执行ALU(算术逻辑单元)、写回时使用到寄存器组。为了提高硬件利用率,CPU指令是按流水线技术来执行的,如下:

从图中可以看出当指令1还未执行完成时,第2条指令便利用空闲的硬件开始执行,这样做是有好处的,如果每个步骤花费1ms,那么如果第2条指令需要等待第1条指令执行完成后再执行的话,则需要等待5ms,但如果使用流水线技术的话,指令2只需等待1ms就可以开始执行了,这样就能大大提升CPU的执行性能。虽然流水线技术可以大大提升CPU的性能,但不幸的是一旦出现流水中断,所有硬件设备将会进入一轮停顿期,当再次弥补中断点可能需要几个周期,这样性能损失也会很大,就好比工厂组装手机的流水线,一旦某个零件组装中断,那么该零件往后的工人都有可能进入一轮或者几轮等待组装零件的过程。因此我们需要尽量阻止指令中断的情况,指令重排就是其中一种优化中断的手段,我们通过一个例子来阐明指令重排是如何阻止流水线技术中断的

a = b + c ;
d = e + f ;

下面通过汇编指令展示了上述代码在CPU执行的处理过程

  • LW指令 表示 load,其中LW R1,b表示把b的值加载到寄存器R1中
  • LW R2,c 表示把c的值加载到寄存器R2中
  • ADD 指令表示加法,把R1 、R2的值相加,并存入R3寄存器中。
  • SW 表示 store 即将 R3寄存器的值保持到变量a中
  • LW R4,e 表示把e的值加载到寄存器R4中
  • LW R5,f 表示把f的值加载到寄存器R5中
  • SUB 指令表示减法,把R4 、R5的值相减,并存入R6寄存器中。
  • SW d,R6 表示将R6寄存器的值保持到变量d中

上述便是汇编指令的执行过程,在某些指令上存在X的标志,X代表中断的含义,也就是只要有X的地方就会导致指令流水线技术停顿,同时也会影响后续指令的执行,可能需要经过1个或几个指令周期才可能恢复正常,那为什么停顿呢?这是因为部分数据还没准备好,如执行ADD指令时,需要使用到前面指令的数据R1,R2,而此时R2的MEM操作没有完成,即未拷贝到存储器中,这样加法计算就无法进行,必须等到MEM操作完成后才能执行,也就因此而停顿了,其他指令也是类似的情况。前面阐述过,停顿会造成CPU性能下降,因此我们应该想办法消除这些停顿,这时就需要使用到指令重排了,如下图,既然ADD指令需要等待,那我们就利用等待的时间做些别的事情,如把LW R4,e 和 LW R5,f 移动到前面执行,毕竟LW R4,e 和 LW R5,f执行并没有数据依赖关系,对他们有数据依赖关系的SUB R6,R5,R4指令在R4,R5加载完成后才执行的,没有影响,过程如下:

正如上图所示,所有的停顿都完美消除了,指令流水线也无需中断了,这样CPU的性能也能带来很好的提升,这就是处理器指令重排的作用。关于编译器重排以及指令重排(这两种重排我们后面统一称为指令重排)相关内容已阐述清晰了,我们必须意识到对于单线程而已指令重排几乎不会带来任何影响,比竟重排的前提是保证串行语义执行的一致性,但对于多线程环境而已,指令重排就可能导致严重的程序轮序执行问题,如下

class MixedOrder{
    int a = 0;
    boolean flag = false;
    public void writer(){
        a = 1;
        flag = true;
    }

    public void read(){
        if(flag){
            int i = a + 1;
        }
    }
}

如上述代码,同时存在线程A和线程B对该实例对象进行操作,其中A线程调用写入方法,而B线程调用读取方法,由于指令重排等原因,可能导致程序执行顺序变为如下:

 线程A                    线程B
 writer:                 read:
 1:flag = true;           1:flag = true;
 2:a = 1;                 2: a = 0 ; //误读
                          3: i = 1 ;

由于指令重排的原因,线程A的flag置为true被提前执行了,而a赋值为1的程序还未执行完,此时线程B,恰好读取flag的值为true,直接获取a的值(此时B线程并不知道a为0)并执行i赋值操作,结果i的值为1,而不是预期的2,这就是多线程环境下,指令重排导致的程序乱序执行的结果。因此,请记住,指令重排只会保证单线程中串行语义的执行的一致性,但并不会关心多线程间的语义一致性。

可见性

理解了指令重排现象后,可见性容易了,可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。对于串行程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。但在多线程环境中可就不一定了,前面我们分析过,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题,通过前面的分析,我们知道无论是编译器优化还是处理器优化的重排现象,在多线程环境下,确实会导致程序轮序执行的问题,从而也就导致可见性问题。

有序性

有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。

JMM提供的解决方案

在理解了原子性,可见性以及有序性问题后,看看JMM是如何保证的,在Java内存模型中都提供一套解决方案供Java工程师在开发过程使用,如原子性问题,除了JVM自身提供的对基本数据类型读写操作的原子性外,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性,关于synchronized的详解,看博主另外一篇文章( 深入理解Java并发之synchronized实现原理)。而工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized关键字或者volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。对于指令重排导致的可见性问题和有序性问题,则可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化,关于volatile稍后会进一步分析。除了靠sychronized和volatile关键字来保证原子性、可见性以及有序性外,JMM内部还定义一套happens-before 原则来保证多线程环境下两个操作间的原子性、可见性以及有序性。

理解JMM中的happens-before 原则

倘若在程序开发中,仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,在Java内存模型中,还提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下

  • 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。

  • 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。

  • volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。

  • 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见

  • 传递性 A先于B ,B先于C 那么A必然先于C

  • 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。

  • 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。

  • 对象终结规则 对象的构造函数执行,结束先于finalize()方法

上述8条原则无需手动添加任何同步手段(synchronized|volatile)即可达到效果,下面我们结合前面的案例演示这8条原则如何判断线程是否安全,如下:

class MixedOrder{
    int a = 0;
    boolean flag = false;
    public void writer(){
        a = 1;
        flag = true;
    }

    public void read(){
        if(flag){
            int i = a + 1;
        }
    }
}

同样的道理,存在两条线程A和B,线程A调用实例对象的writer()方法,而线程B调用实例对象的read()方法,线程A先启动而线程B后启动,那么线程B读取到的i值是多少呢?现在依据8条原则,由于存在两条线程同时调用,因此程序次序原则不合适。writer()方法和read()方法都没有使用同步手段,锁规则也不合适。没有使用volatile关键字,volatile变量原则不适应。线程启动规则、线程终止规则、线程中断规则、对象终结规则、传递性和本次测试案例也不合适。线程A和线程B的启动时间虽然有先后,但线程B执行结果却是不确定,也是说上述代码没有适合8条原则中的任意一条,也没有使用任何同步手段,所以上述的操作是线程不安全的,因此线程B读取的值自然也是不确定的。修复这个问题的方式很简单,要么给writer()方法和read()方法添加同步手段,如synchronized或者给变量flag添加volatile关键字,确保线程A修改的值对线程B总是可见。

volatile内存语义

volatile在并发编程中很常见,但也容易被滥用,现在我们就进一步分析volatile关键字的语义。volatile是Java虚拟机提供的轻量级的同步机制。volatile关键字有如下两个作用

  • 保证被volatile修饰的共享gong’x变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总数可以被其他线程立即得知。

  • 禁止指令重排序优化。

volatile的可见性

关于volatile的可见性作用,我们必须意识到被volatile修饰的变量对所有线程总数立即可见的,对volatile变量的所有写操作总是能立刻反应到其他线程中,但是对于volatile变量运算操作在多线程环境并不保证安全性,如下

public class VolatileVisibility {
    public static volatile int i =0;

    public static void increase(){
        i++;
    }
}

正如上述代码所示,i变量的任何改变都会立马反应到其他线程中,但是如此存在多条线程同时调用increase()方法的话,就会出现线程安全问题,毕竟i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全,需要注意的是一旦使用synchronized修饰方法后,由于synchronized本身也具备与volatile相同的特性,即可见性,因此在这样种情况下就完全可以省去volatile修饰变量。

public class VolatileVisibility {
    public static int i =0;

    public synchronized static void increase(){
        i++;
    }
}

现在来看另外一种场景,可以使用volatile修饰变量达到线程安全的目的,如下

public class VolatileSafe {

    volatile boolean close;

    public void close(){
        close=true;
    }

    public void doWork(){
        while (!close){
            System.out.println("safe....");
        }
    }
}

由于对于boolean变量close值的修改属于原子性操作,因此可以通过使用volatile修饰变量close,使用该变量对其他线程立即可见,从而达到线程安全的目的。那么JMM是如何实现让volatile变量对其他线程立即可见的呢?实际上,当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程将只能从主内存中重新读取共享变量。volatile变量正是通过这种写-读方式实现对其他线程可见(但其内存语义实现则是通过内存屏障,稍后会说明)。

volatile禁止重排优化

volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如何实现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)。
内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。下面看一个非常典型的禁止重排优化的例子DCL,如下:

/**
 * Created by zejian on 2017/6/11.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class DoubleCheckLock {

    private static DoubleCheckLock instance;

    private DoubleCheckLock(){}

    public static DoubleCheckLock getInstance(){

        //第一次检测
        if (instance==null){
            //同步
            synchronized (DoubleCheckLock.class){
                if (instance == null){
                    //多线程环境下可能会出现问题的地方
                    instance = new DoubleCheckLock();
                }
            }
        }
        return instance;
    }
}

上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)

memory = allocate(); //1.分配对象内存空间
instance(memory);    //2.初始化对象
instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null

由于步骤1和步骤2间可能会重排序,如下:

memory = allocate(); //1.分配对象内存空间
instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory);    //2.初始化对象

由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。

  //禁止指令重排优化
  private volatile static DoubleCheckLock instance;

ok~,到此相信我们对Java内存模型和volatile应该都有了比较全面的认识,总而言之,我们应该清楚知道,JMM就是一组规则,这组规则意在解决在并发编程可能出现的线程安全问题,并提供了内置解决方案(happen-before原则)及其外部可使用的同步手段(synchronized/volatile等),确保了程序执行在多线程环境中的应有的原子性,可视性及其有序性。

如有错误,欢迎留言,谢谢!

参考资料:
http://tutorials.jenkov.com/java-concurrency/java-memory-model.html
http://blog.csdn.net/iter_zc/article/details/41843595
http://ifeve.com/wp-content/uploads/2014/03/JSR133%E4%B8%AD%E6%96%87%E7%89%881.pdf

《深入理解JVM虚拟机》
《Java高并发程序设计》

《深入理解 Java 内存模型》读书笔记

前提

《深入理解 Java 内存模型》程晓明著,该书在以前看过一遍,现在学的东西越多,感觉那块越重要,于是又再细看一遍,于是便有了下面的读书笔记总结。全书页数虽不多,内容讲得挺深的。细看的话,也是挺花时间的,看完收获绝对挺大的。也建议 Java 开发者都去看看。里面主要有 Java 内存模型的基础、重排序、顺序一致性、Volatile 关键字、锁、final。本文参考书中内容。

关注我

如果你想查看这本书可以关注我的公众号: zhisheng ,然后里面回复关键字 JMM 可以查看我分享的百度云链接。

转载请务必注明原创地址为:http://www.54tianzhisheng.cn/2018/02/28/Java-Memory-Model/

基础

并发编程的模型分类

在并发编程需要处理的两个关键问题是:线程之间如何通信 和 线程之间如何同步

通信

通信 是指线程之间以何种机制来交换信息。在命令式编程中,线程之间的通信机制有两种:共享内存 和 消息传递

共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态隐式进行通信。

消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息显式进行通信。

同步

同步 是指程序用于控制不同线程之间操作发生相对顺序的机制。

共享内存的并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行

消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。

Java 的并发采用的是共享内存模型,Java 线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

Java 内存模型的抽象

在 Java 中,所有实例域、静态域 和 数组元素存储在堆内存中,堆内存在线程之间共享。局部变量、方法定义参数 和 异常处理器参数 不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。

Java 线程之间的通信由 Java 内存模型(JMM)控制。JMM 决定了一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM 定义了线程与主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每一个线程都有一个自己私有的本地内存,本地内存中存储了该变量以读/写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在。

JMM 抽象示意图:

从上图来看,如果线程 A 和线程 B 要通信的话,要如下两个步骤:

1、线程 A 需要将本地内存 A 中的共享变量副本刷新到主内存去

2、线程 B 去主内存读取线程 A 之前已更新过的共享变量

步骤示意图:

举个例子:

本地内存 A 和 B 有主内存共享变量 X 的副本。假设一开始时,这三个内存中 X 的值都是 0。线程 A 正执行时,把更新后的 X 值(假设为 1)临时存放在自己的本地内存 A 中。当线程 A 和 B 需要通信时,线程 A 首先会把自己本地内存 A 中修改后的 X 值刷新到主内存去,此时主内存中的 X 值变为了 1。随后,线程 B 到主内存中读取线程 A 更新后的共享变量 X 的值,此时线程 B 的本地内存的 X 值也变成了 1。

整体来看,这两个步骤实质上是线程 A 再向线程 B 发送消息,而这个通信过程必须经过主内存。JMM 通过控制主内存与每个线程的本地内存之间的交互,来为 Java 程序员提供内存可见性保证。

重排序

在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三类:

1、编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

2、指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

3、内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从 Java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:

上面的这些重排序都可能导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 Java 编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

JMM 属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。

处理器重排序

现代的处理器使用写缓冲区来临时保存向内存写入的数据。写缓冲区可以保证指令流水线持续运行,它可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟。同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用。虽然写缓冲区有这么多好处,但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!

举个例子:

假设处理器A和处理器B按程序的顺序并行执行内存访问,最终却可能得到 x = y = 0。具体的原因如下图所示:

处理器 A 和 B 同时把共享变量写入在写缓冲区中(A1、B1),然后再从内存中读取另一个共享变量(A2、B2),最后才把自己写缓冲区中保存的脏数据刷新到内存中(A3、B3)。当以这种时序执行时,程序就可以得到 x = y = 0 的结果。

从内存操作实际发生的顺序来看,直到处理器 A 执行 A3 来刷新自己的写缓存区,写操作 A1 才算真正执行了。虽然处理器 A 执行内存操作的顺序为:A1 -> A2,但内存操作实际发生的顺序却是:A2 -> A1。此时,处理器 A 的内存操作顺序被重排序了。

这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致。由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许对写-读操作重排序。

内存屏障指令

为了保证内存可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM 把内存屏障指令分为下列四类:

屏障类型指令示例说明
LoadLoad Barriers Load1; LoadLoad; Load2 确保 Load1 数据的装载,之前于 Load2 及所有后续装载指令的装载。
StoreStore Barriers Store1; StoreStore; Store2 确保 Store1 数据对其他处理器可见(刷新到内存),之前于 Store2 及所有后续存储指令的存储。
LoadStore Barriers Load1; LoadStore; Store2 确保 Load1 数据装载,之前于 Store2 及所有后续的存储指令刷新到内存。
StoreLoad Barriers Store1; StoreLoad; Load2 确保 Store1 数据对其他处理器变得可见(指刷新到内存),之前于 Load2 及所有后续装载指令的装载。StoreLoadBarriers 会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令

happens-before

JSR-133 内存模型使用 happens-before 的概念来阐述操作之间的内存可见性。在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在 happens-before 关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

与程序员密切相关的 happens-before 规则如下:

  • 程序顺序规则:一个线程中的每个操作,happens-before 于该线程中的任意后续操作。
  • 监视器锁规则:对一个监视器的解锁,happens-before 于随后对这个监视器的加锁。
  • volatile 变量规则:对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。
  • 传递性:如果 A happens-before B,且 B happens-before C,那么 A happens-before C。

注意,两个操作之间具有 happens-before 关系,并不意味着前一个操作必须要在后一个操作之前执行!happens-before 仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。

happens-before 与 JMM 的关系如下图所示:

如上图所示,一个 happens-before 规则对应于一个或多个编译器和处理器重排序规则。

数据依赖性

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。数据依赖分下列三种类型:

名称代码示例说明
写后读 a = 1; b = a; 写一个变量之后,再读这个位置。
写后写 a = 1; a = 2; 写一个变量之后,再写这个变量。
读后写 a = b; b = 1; 读一个变量之后,再写这个变量。

上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变。

前面提到过,编译器和处理器可能会对操作做重排序。编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。

注意,这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

as-if-serial 语义

as-if-serial 语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守 as-if-serial 语义。

为了遵守 as-if-serial 编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是如果操作之间没有数据依赖关系,这些操作就可能被编译器和处理器重排序。

举个例子:

1
2
3
double pi = 3.14;     //A
double r = 1.0; //B
double area = pi * r * r; //C

上面三个操作的数据依赖关系如下图所示:

如上图所示,A 和 C 之间存在数据依赖关系,同时 B 和 C 之间也存在数据依赖关系。因此在最终执行的指令序列中,C 不能被重排序到 A 和 B 的前面(C 排到 A 和 B 的前面,程序的结果将会被改变)。但 A 和 B 之间没有数据依赖关系,编译器和处理器可以重排序 A 和 B 之间的执行顺序。下图是该程序的两种执行顺序:

在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能的开发并行度。编译器和处理器遵从这一目标,从 happens-before 的定义我们可以看出,JMM 同样遵从这一目标。

重排序对多线程的影响

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Demo {
int a = 0;
boolean flag = false;

public void write() {
a = 1; //1
flag = true; //2
}

public void read() {
if(flag) { //3
int i = a * a; //4
}
}
}

由于操作 1 和 2 没有数据依赖关系,编译器和处理器可以对这两个操作重排序;操作 3 和操作 4 没有数据依赖关系,编译器和处理器也可以对这两个操作重排序。

1、当操作 1 和操作 2 重排序时,可能会产生什么效果?

如上图所示,操作 1 和操作 2 做了重排序。程序执行时,线程 A 首先写标记变量 flag,随后线程 B 读这个变量。由于条件判断为真,线程 B 将读取变量 a。此时,变量 a 还根本没有被线程 A 写入,在这里多线程程序的语义被重排序破坏了!

2、当操作 3 和操作 4 重排序时会产生什么效果(借助这个重排序,可以顺便说明控制依赖性)。

在程序中,操作 3 和操作 4 存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。为此,编译器和处理器会采用猜测(Speculation)执行来克服控制相关性对并行度的影响。以处理器的猜测执行为例,执行线程 B 的处理器可以提前读取并计算 a * a,然后把计算结果临时保存到一个名为重排序缓冲(reorder buffer ROB)的硬件缓存中。当接下来操作 3 的条件判断为真时,就把该计算结果写入变量 i 中。

从图中我们可以看出,猜测执行实质上对操作3和4做了重排序。重排序在这里破坏了多线程程序的语义!

在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是 as-if-serial 语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

顺序一致性

顺序一致性内存模型

顺序一致性内存模型有两大特性:

  • 一个线程中的所有操作必须按照程序的顺序来执行。
  • (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

顺序一致性内存模型为程序员提供的视图如下:

在概念上,顺序一致性模型有一个单一的全局内存,这个内存通过一个左右摆动的开关可以连接到任意一个线程,同时每一个线程必须按照程序的顺序来执行内存读/写操作。从上面的示意图我们可以看出,在任意时间点最多只能有一个线程可以连接到内存。当多个线程并发执行时,图中的开关装置能把所有线程的所有内存读/写操作串行化。

举个例子:

假设有两个线程 A 和 B 并发执行。其中 A 线程有三个操作,它们在程序中的顺序是:A1 -> A2 -> A3。B 线程也有三个操作,它们在程序中的顺序是:B1 -> B2 -> B3。

假设这两个线程使用监视器锁来正确同步:A 线程的三个操作执行后释放监视器锁,随后 B 线程获取同一个监视器锁。那么程序在顺序一致性模型中的执行效果将如下图所示:

现在我们再假设这两个线程没有做同步,下面是这个未同步程序在顺序一致性模型中的执行示意图:

未同步程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只能看到一个一致的整体执行顺序。以上图为例,线程 A 和 B 看到的执行顺序都是:B1 -> A1 -> A2 -> B2 -> A3 -> B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任意线程可见。

但是,在 JMM 中就没有这个保证。未同步程序在 JMM 中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。比如,在当前线程把写过的数据缓存在本地内存中,在还没有刷新到主内存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,会认为这个写操作根本还没有被当前线程执行。只有当前线程把本地内存中写过的数据刷新到主内存之后,这个写操作才能对其他线程可见。在这种情况下,当前线程和其它线程看到的操作执行顺序将不一致。

同步程序的顺序一致性效果

下面我们对前面的示例程序用锁来同步,看看正确同步的程序如何具有顺序一致性。

请看下面的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class demo {
int a = 0;
boolean flag = false;

public synchronized void write() { //获取锁
a = 1;
flag = true;
} //释放锁

public synchronized void read() { //获取锁
if(flag) {
int i = a;
}
} //释放锁
}

上面示例代码中,假设 A 线程执行 write() 方法后,B 线程执行 reade() 方法。这是一个正确同步的多线程程序。根据JMM规范,该程序的执行结果将与该程序在顺序一致性模型中的执行结果相同。下面是该程序在两个内存模型中的执行时序对比图:

在顺序一致性模型中,所有操作完全按程序的顺序执行。而在 JMM 中,临界区内的代码可以重排序(但 JMM 不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。JMM 会在退出临界区和进入临界区这两个关键时间点做一些特别处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图。虽然线程 A 在临界区内做了重排序,但由于监视器的互斥执行的特性,这里的线程 B 根本无法“观察”到线程 A 在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。

从这里我们可以看到 JMM 在具体实现上的基本方针:在不改变(正确同步的)程序执行结果的前提下,尽可能的为编译器和处理器的优化打开方便之门。

未同步程序的执行特性

未同步程序在 JMM 中的执行时,整体上是无序的,其执行结果无法预知。未同步程序在两个模型中的执行特性有下面几个差异:

  1. 顺序一致性模型保证单线程内的操作会按程序的顺序执行,而 JMM 不保证单线程内的操作会按程序的顺序执行(比如上面正确同步的多线程程序在临界区内的重排序)。
  2. 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而 JMM 不保证所有线程能看到一致的操作执行顺序。
  3. JMM 不保证对 64 位的 long 型和 double 型变量的读/写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子 。

第三个差异与处理器总线的工作机制密切相关。在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都是通过总线事务来完成的。总线事务包括读事务和写事务。读事务从内存传送数据到处理器,写事务从处理器传递数据到内存,每个事务会读/写内存中一个或多个物理上连续的字。总线会同步试图并发使用总线的事务。在一个处理器执行总线事务期间,总线会禁止其它所有的处理器和 I/O 设备执行内存的读/写。

总线的工作机制:

如上图所示,假设处理器 A、B、和 C 同时向总线发起总线事务,这时总线仲裁会对竞争作出裁决,假设总线在仲裁后判定处理器 A 在竞争中获胜(总线仲裁会确保所有处理器都能公平的访问内存)。此时处理器 A 继续它的总线事务,而其它两个处理器则要等待处理器 A 的总线事务完成后才能开始再次执行内存访问。假设在处理器 A 执行总线事务期间(不管这个总线事务是读事务还是写事务),处理器 D 向总线发起了总线事务,此时处理器 D 的这个请求会被总线禁止。

总线的这些工作机制可以把所有处理器对内存的访问以串行化的方式来执行在任意时间点,最多只能有一个处理器能访问内存。这个特性确保了单个总线事务之中的内存读/写操作具有原子性

在一些 32 位的处理器上,如果要求对 64 位数据的写操作具有原子性,会有比较大的开销。为了照顾这种处理器,Java 语言规范鼓励但不强求 JVM 对 64 位的 long 型变量和 double 型变量的写具有原子性。当 JVM 在这种处理器上运行时,会把一个 64 位 long/ double 型变量的写操作拆分为两个 32 位的写操作来执行。这两个 32 位的写操作可能会被分配到不同的总线事务中执行,此时对这个 64 位变量的写将不具有原子性。

当单个内存操作不具有原子性,将可能会产生意想不到后果。请看下面示意图:

如上图所示,假设处理器 A 写一个 long 型变量,同时处理器 B 要读这个 long 型变量。处理器 A 中 64 位的写操作被拆分为两个 32 位的写操作,且这两个 32 位的写操作被分配到不同的写事务中执行。同时处理器 B 中 64 位的读操作被分配到单个的读事务中执行。当处理器 A 和 B 按上图的时序来执行时,处理器 B 将看到仅仅被处理器 A “写了一半“的无效值。

注意,在 JSR -133 之前的旧内存模型中,一个 64 位 long/ double 型变量的读/写操作可以被拆分为两个 32 位的读/写操作来执行。从 JSR -133 内存模型开始(即从JDK5开始),仅仅只允许把一个 64 位 long/ double 型变量的操作拆分为两个 32 位的写操作来执行,任意的读操作在JSR -133中都必须具有原子性(即任意读操作必须要在单个读事务中执行)。

Volatile

Volatile 特性

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class VolatileTest {
volatile long a = 1L; // 使用 volatile 声明 64 位的 long 型

public void set(long l) {
a = l; //单个 volatile 变量的写
}

public long get() {
return a; //单个 volatile 变量的读
}

public void getAndIncreament() {
a++; // 复合(多个) volatile 变量的读 /写
}
}

假设有多个线程分别调用上面程序的三个方法,这个程序在语义上和下面程序等价:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class VolatileTest {
long a = 1L; // 64 位的 long 型普通变量

public synchronized void set(long l) { //对单个普通变量的写用同一个锁同步
a = l;
}

public synchronized long get() { //对单个普通变量的读用同一个锁同步
return a;
}

public void getAndIncreament() { //普通方法调用
long temp = get(); //调用已同步的读方法
temp += 1L; //普通写操作
set(temp); //调用已同步的写方法
}
}

如上面示例程序所示,对一个 volatile 变量的单个读/写操作,与对一个普通变量的读/写操作使用同一个锁来同步,它们之间的执行效果相同。

锁的 happens-before 规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入

锁的语义决定了临界区代码的执行具有原子性。这意味着即使是 64 位的 long 型和 double 型变量,只要它是 volatile变量,对该变量的读写就将具有原子性。如果是多个 volatile 操作或类似于 volatile++ 这种复合操作,这些操作整体上不具有原子性

简而言之,volatile 变量自身具有下列特性:

  • 可见性。对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入。
  • 原子性:对任意单个 volatile 变量的读/写具有原子性,但类似于 volatile++ 这种复合操作不具有原子性。

volatile 写-读的内存定义

  • 一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。
  • 一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

假设上面的程序 flag 变量用 volatile 修饰

volatile 内存语义的实现

下面是 JMM 针对编译器制定的 volatile 重排序规则表:

为了实现 volatile 的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

下面是基于保守策略的 JMM 内存屏障插入策略:

  • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。
  • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。
  • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。

下面是保守策略下,volatile 写操作 插入内存屏障后生成的指令序列示意图:

下面是在保守策略下,volatile 读操作 插入内存屏障后生成的指令序列示意图:

上述 volatile 写操作和 volatile 读操作的内存屏障插入策略非常保守。在实际执行时,只要不改变 volatile 写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。

锁释放和获取的内存语义

当线程释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中。

当线程获取锁时,JMM 会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量。

锁内存语义的实现

借助 ReentrantLock 来讲解,PS: 后面专门讲下这块(ReentrantLock、Synchronized、公平锁、非公平锁、AQS等),可以看看大明哥的博客:http://cmsblogs.com/?p=2210

concurrent 包的实现

如果我们仔细分析 concurrent 包的源代码实现,会发现一个通用化的实现模式:

  1. 首先,声明共享变量为 volatile;
  2. 然后,使用 CAS 的原子条件更新来实现线程之间的同步;
  3. 同时,配合以 volatile 的读/写和 CAS 所具有的 volatile 读和写的内存语义来实现线程之间的通信。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic 包中的类),这些 concurrent 包中的基础类都是使用这种模式来实现的,而 concurrent 包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent 包的实现示意图如下:

final

对于 final 域,编译器和处理器要遵守两个重排序规则:

  1. 在构造函数内对一个 final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  2. 初次读一个包含 final 域的对象的引用,与随后初次读这个 final 域,这两个操作之间不能重排序。

写 final 域的重排序规则

写 final 域的重排序规则禁止把 final 域的写重排序到构造函数之外。这个规则的实现包含下面2个方面:

  • JMM 禁止编译器把 final 域的写重排序到构造函数之外。
  • 编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把 final 域的写重排序到构造函数之外。

读 final 域的重排序规则

在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障。

final 域是引用类型

对于引用类型,写 final 域的重排序规则对编译器和处理器增加了如下约束:

在构造函数内对一个 final 引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

总结

JMM,处理器内存模型与顺序一致性内存模型之间的关系

JMM 是一个语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性内存模型是一个理论参考模型。下面是语言内存模型,处理器内存模型和顺序一致性内存模型的强弱对比示意图:

JMM 的设计示意图

JMM 的内存可见性保证

Java 程序的内存可见性保证按程序类型可以分为下列三类:

1.单线程程序。单线程程序不会出现内存可见性问题。编译器,runtime 和处理器会共同确保单线程程序的执行结果与该程序在顺序一致性模型中的执行结果相同。

2.正确同步的多线程程序。正确同步的多线程程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。这是 JMM 关注的重点,JMM通过限制编译器和处理器的重排序来为程序员提供内存可见性保证。

3.未同步/未正确同步的多线程程序。JMM 为它们提供了最小安全性保障:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)。

下图展示了这三类程序在 JMM 中与在顺序一致性内存模型中的执行结果的异同:

 

深入理解Java注解类型(@Annotation)

理解Java注解

实际上Java注解与普通修饰符(public、static、void等)的使用方式并没有多大区别,下面的例子是常见的注解:

public class AnnotationDemo {
    //@Test注解修饰方法A
    @Test
    public static void A(){
        System.out.println("Test.....");
    }

    //一个方法上可以拥有多个不同的注解
    @Deprecated
    @SuppressWarnings("uncheck")
    public static void B(){

    }
}

通过在方法上使用@Test注解后,在运行该方法时,测试框架会自动识别该方法并单独调用,@Test实际上是一种标记注解,起标记作用,运行时告诉测试框架该方法为测试方法。而对于@Deprecated和@SuppressWarnings(“uncheck”),则是Java本身内置的注解,在代码中,可以经常看见它们,但这并不是一件好事,毕竟当方法或是类上面有@Deprecated注解时,说明该方法或是类都已经过期不建议再用,@SuppressWarnings 则表示忽略指定警告,比如@SuppressWarnings(“uncheck”),这就是注解的最简单的使用方式,那么下面我们就来看看注解定义的基本语法

基本语法

声明注解与元注解

我们先来看看前面的Test注解是如何声明的:

//声明Test注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {

} 

我们使用了@interface声明了Test注解,并使用@Target注解传入ElementType.METHOD参数来标明@Test只能用于方法上,@Retention(RetentionPolicy.RUNTIME)则用来表示该注解生存期是运行时,从代码上看注解的定义很像接口的定义,确实如此,毕竟在编译后也会生成Test.class文件。对于@Target@Retention是由Java提供的元注解,所谓元注解就是标记其他注解的注解,下面分别介绍

  • @Target 用来约束注解可以应用的地方(如方法、类或字段),其中ElementType是枚举类型,其定义如下,也代表可能的取值范围

    public enum ElementType {
        /**标明该注解可以用于类、接口(包括注解类型)或enum声明*/
        TYPE,
    
        /** 标明该注解可以用于字段(域)声明,包括enum实例 */
        FIELD,
    
        /** 标明该注解可以用于方法声明 */
        METHOD,
    
        /** 标明该注解可以用于参数声明 */
        PARAMETER,
    
        /** 标明注解可以用于构造函数声明 */
        CONSTRUCTOR,
    
        /** 标明注解可以用于局部变量声明 */
        LOCAL_VARIABLE,
    
        /** 标明注解可以用于注解声明(应用于另一个注解上)*/
        ANNOTATION_TYPE,
    
        /** 标明注解可以用于包声明 */
        PACKAGE,
    
        /**
         * 标明注解可以用于类型参数声明(1.8新加入)
         * @since 1.8
         */
        TYPE_PARAMETER,
    
        /**
         * 类型使用声明(1.8新加入)
         * @since 1.8
         */
        TYPE_USE
    }

    请注意,当注解未指定Target值时,则此注解可以用于任何元素之上,多个值使用{}包含并用逗号隔开,如下:

    @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
  • @Retention用来约束注解的生命周期,分别有三个值,源码级别(source),类文件级别(class)或者运行时级别(runtime),其含有如下:

    • SOURCE:注解将被编译器丢弃(该类型的注解信息只会保留在源码里,源码经过编译后,注解信息会被丢弃,不会保留在编译好的class文件里)

    • CLASS:注解在class文件中可用,但会被VM丢弃(该类型的注解信息会保留在源码里和class文件里,在执行的时候,不会加载到虚拟机中),请注意,当注解未定义Retention值时,默认值是CLASS,如Java内置注解,@Override、@Deprecated、@SuppressWarnning等

    • RUNTIME:注解信息将在运行期(JVM)也保留,因此可以通过反射机制读取注解的信息(源码、class文件和执行的时候都有注解的信息),如SpringMvc中的@Controller、@Autowired、@RequestMapping等。

注解元素及其数据类型

通过上述对@Test注解的定义,我们了解了注解定义的过程,由于@Test内部没有定义其他元素,所以@Test也称为标记注解(marker annotation),但在自定义注解中,一般都会包含一些元素以表示某些值,方便处理器使用,这点在下面的例子将会看到:

/**
 * Created by wuzejian on 2017/5/18.
 * 对应数据表注解
 */
@Target(ElementType.TYPE)//只能应用于类上
@Retention(RetentionPolicy.RUNTIME)//保存到运行时
public @interface DBTable {
    String name() default "";
}

上述定义一个名为DBTable的注解,该用于主要用于数据库表与Bean类的映射(稍后会有完整案例分析),与前面Test注解不同的是,我们声明一个String类型的name元素,其默认值为空字符,但是必须注意到对应任何元素的声明应采用方法的声明方式,同时可选择使用default提供默认值,@DBTable使用方式如下:

//在类上使用该注解
@DBTable(name = "MEMBER")
public class Member {
    //.......
}

关于注解支持的元素数据类型除了上述的String,还支持如下数据类型

  • 所有基本类型(int,float,boolean,byte,double,char,long,short)

  • String

  • Class

  • enum

  • Annotation

  • 上述类型的数组

倘若使用了其他数据类型,编译器将会丢出一个编译错误,注意,声明注解元素时可以使用基本类型但不允许使用任何包装类型,同时还应该注意到注解也可以作为元素的类型,也就是嵌套注解,下面的代码演示了上述类型的使用过程:

package com.zejian.annotationdemo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by wuzejian on 2017/5/19.
 * 数据类型使用Demo
 */

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Reference{
    boolean next() default false;
}

public @interface AnnotationElementDemo {
    //枚举类型
    enum Status {FIXED,NORMAL};

    //声明枚举
    Status status() default Status.FIXED;

    //布尔类型
    boolean showSupport() default false;

    //String类型
    String name()default "";

    //class类型
    Class<?> testCase() default Void.class;

    //注解嵌套
    Reference reference() default @Reference(next=true);

    //数组类型
    long[] value();
}

编译器对默认值的限制

编译器对元素的默认值有些过分挑剔。首先,元素不能有不确定的值。也就是说,元素必须要么具有默认值,要么在使用注解时提供元素的值。其次,对于非基本类型的元素,无论是在源代码中声明,还是在注解接口中定义默认值,都不能以null作为值,这就是限制,没有什么利用可言,但造成一个元素的存在或缺失状态,因为每个注解的声明中,所有的元素都存在,并且都具有相应的值,为了绕开这个限制,只能定义一些特殊的值,例如空字符串或负数,表示某个元素不存在。

注解不支持继承

注解是不支持继承的,因此不能使用关键字extends来继承某个@interface,但注解在编译后,编译器会自动继承java.lang.annotation.Annotation接口,这里我们反编译前面定义的DBTable注解

package com.zejian.annotationdemo;

import java.lang.annotation.Annotation;
//反编译后的代码
public interface DBTable extends Annotation
{
    public abstract String name();
}

虽然反编译后发现DBTable注解继承了Annotation接口,请记住,即使Java的接口可以实现多继承,但定义注解时依然无法使用extends关键字继承@interface。

快捷方式

所谓的快捷方式就是注解中定义了名为value的元素,并且在使用该注解时,如果该元素是唯一需要赋值的一个元素,那么此时无需使用key=value的语法,而只需在括号内给出value元素所需的值即可。这可以应用于任何合法类型的元素,记住,这限制了元素名必须为value,简单案例如下

package com.zejian.annotationdemo;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by zejian on 2017/5/20.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
//定义注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface IntegerVaule{
    int value() default 0;
    String name() default "";
}

//使用注解
public class QuicklyWay {

    //当只想给value赋值时,可以使用以下快捷方式
    @IntegerVaule(20)
    public int age;

    //当name也需要赋值时必须采用key=value的方式赋值
    @IntegerVaule(value = 10000,name = "MONEY")
    public int money;

}

Java内置注解与其它元注解

接着看看Java提供的内置注解,主要有3个,如下:

  • @Override:用于标明此方法覆盖了父类的方法,源码如下

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }
  • @Deprecated:用于标明已经过时的方法或类,源码如下,关于@Documented稍后分析:

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
    public @interface Deprecated {
    }
  • @SuppressWarnnings:用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告,其实现源码如下:

    @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface SuppressWarnings {
        String[] value();
    }

    其内部有一个String数组,主要接收值如下:

    deprecation:使用了不赞成使用的类或方法时的警告;
    unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型; 
    fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;
    path:在类路径、源文件路径等中有不存在的路径时的警告; 
    serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告; 
    finally:任何 finally 子句不能正常完成时的警告; 
    all:关于以上所有情况的警告。

这个三个注解比较简单,看个简单案例即可:

//注明该类已过时,不建议使用
@Deprecated
class A{
    public void A(){ }

    //注明该方法已过时,不建议使用
    @Deprecated()
    public void B(){ }
}

class B extends A{

    @Override //标明覆盖父类A的A方法
    public void A() {
        super.A();
    }

    //去掉检测警告
    @SuppressWarnings({"uncheck","deprecation"})
    public void C(){ } 
    //去掉检测警告
    @SuppressWarnings("uncheck")
    public void D(){ }
}

前面我们分析了两种元注解,@Target和@Retention,除了这两种元注解,Java还提供了另外两种元注解,@Documented和@Inherited,下面分别介绍:

  • @Documented 被修饰的注解会生成到javadoc中

    /**
     * Created by zejian on 2017/5/20.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    @Documented
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DocumentA {
    }
    
    //没有使用@Documented
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DocumentB {
    }
    
    //使用注解
    @DocumentA
    @DocumentB
    public class DocumentDemo {
        public void A(){
        }
    }

    使用javadoc命令生成文档:

    zejian@zejiandeMBP annotationdemo$ javadoc DocumentDemo.java DocumentA.java DocumentB.java 

    如下:

    可以发现使用@Documented元注解定义的注解(@DocumentA)将会生成到javadoc中,而@DocumentB则没有在doc文档中出现,这就是元注解@Documented的作用。

  • @Inherited 可以让注解被继承,但这并不是真的继承,只是通过使用@Inherited,可以让子类Class对象使用getAnnotations()获取父类被@Inherited修饰的注解,如下:

    @Inherited
    @Documented
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DocumentA {
    }
    
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface DocumentB {
    }
    
    @DocumentA
    class A{ }
    
    class B extends A{ }
    
    @DocumentB
    class C{ }
    
    class D extends C{ }
    
    //测试
    public class DocumentDemo {
    
        public static void main(String... args){
            A instanceA=new B();
            System.out.println("已使用的@Inherited注解:"+Arrays.toString(instanceA.getClass().getAnnotations()));
    
            C instanceC = new D();
    
            System.out.println("没有使用的@Inherited注解:"+Arrays.toString(instanceC.getClass().getAnnotations()));
        }
    
        /**
         * 运行结果:
         已使用的@Inherited注解:[@com.zejian.annotationdemo.DocumentA()]
         没有使用的@Inherited注解:[]
         */
    }

注解与反射机制

前面经过反编译后,我们知道Java所有注解都继承了Annotation接口,也就是说 Java使用Annotation接口代表注解元素,该接口是所有Annotation类型的父接口。同时为了运行时能准确获取到注解的相关信息,Java在java.lang.reflect 反射包下新增了AnnotatedElement接口,它主要用于表示目前正在 VM 中运行的程序中已使用注解的元素,通过该接口提供的方法可以利用反射技术地读取注解的信息,如反射包的Constructor类、Field类、Method类、Package类和Class类都实现了AnnotatedElement接口,它简要含义如下(更多详细介绍可以看 深入理解Java类型信息(Class对象)与反射机制):

Class:类的Class对象定义  
Constructor:代表类的构造器定义  
Field:代表类的成员变量定义
Method:代表类的方法定义  
Package:代表类的包定义

下面是AnnotatedElement中相关的API方法,以上5个类都实现以下的方法

返回值方法名称说明
<A extends Annotation> getAnnotation(Class<A> annotationClass) 该元素如果存在指定类型的注解,则返回这些注解,否则返回 null。
Annotation[] getAnnotations() 返回此元素上存在的所有注解,包括从父类继承的
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) 如果指定类型的注解存在于此元素上,则返回 true,否则返回 false。
Annotation[] getDeclaredAnnotations() 返回直接存在于此元素上的所有注解,注意,不包括父类的注解,调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响,没有则返回长度为0的数组

简单案例演示如下:

package com.zejian.annotationdemo;

import java.lang.annotation.Annotation;
import java.util.Arrays;

/**
 * Created by zejian on 2017/5/20.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
@DocumentA
class A{ }

//继承了A类
@DocumentB
public class DocumentDemo extends A{

    public static void main(String... args){

        Class<?> clazz = DocumentDemo.class;
        //根据指定注解类型获取该注解
        DocumentA documentA=clazz.getAnnotation(DocumentA.class);
        System.out.println("A:"+documentA);

        //获取该元素上的所有注解,包含从父类继承
        Annotation[] an= clazz.getAnnotations();
        System.out.println("an:"+ Arrays.toString(an));
        //获取该元素上的所有注解,但不包含继承!
        Annotation[] an2=clazz.getDeclaredAnnotations();
        System.out.println("an2:"+ Arrays.toString(an2));

        //判断注解DocumentA是否在该元素上
        boolean b=clazz.isAnnotationPresent(DocumentA.class);
        System.out.println("b:"+b);

        /**
         * 执行结果:
         A:@com.zejian.annotationdemo.DocumentA()
         an:[@com.zejian.annotationdemo.DocumentA(), @com.zejian.annotationdemo.DocumentB()]
         an2:@com.zejian.annotationdemo.DocumentB()
         b:true
         */
    }
}

运行时注解处理器

了解完注解与反射的相关API后,现在通过一个实例(该例子是博主改编自《Tinking in Java》)来演示利用运行时注解来组装数据库SQL的构建语句的过程

/**
 * Created by wuzejian on 2017/5/18.
 * 表注解
 */
@Target(ElementType.TYPE)//只能应用于类上
@Retention(RetentionPolicy.RUNTIME)//保存到运行时
public @interface DBTable {
    String name() default "";
}


/**
 * Created by wuzejian on 2017/5/18.
 * 注解Integer类型的字段
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    //该字段对应数据库表列名
    String name() default "";
    //嵌套注解
    Constraints constraint() default @Constraints;
}


/**
 * Created by wuzejian on 2017/5/18.
 * 注解String类型的字段
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {

    //对应数据库表的列名
    String name() default "";

    //列类型分配的长度,如varchar(30)的30
    int value() default 0;

    Constraints constraint() default @Constraints;
}


/**
 * Created by wuzejian on 2017/5/18.
 * 约束注解
 */

@Target(ElementType.FIELD)//只能应用在字段上
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    //判断是否作为主键约束
    boolean primaryKey() default false;
    //判断是否允许为null
    boolean allowNull() default false;
    //判断是否唯一
    boolean unique() default false;
}

/**
 * Created by wuzejian on 2017/5/18.
 * 数据库表Member对应实例类bean
 */
@DBTable(name = "MEMBER")
public class Member {
    //主键ID
    @SQLString(name = "ID",value = 50, constraint = @Constraints(primaryKey = true))
    private String id;

    @SQLString(name = "NAME" , value = 30)
    private String name;

    @SQLInteger(name = "AGE")
    private int age;

    @SQLString(name = "DESCRIPTION" ,value = 150 , constraint = @Constraints(allowNull = true))
    private String description;//个人描述

   //省略set get.....
}

上述定义4个注解,分别是@DBTable(用于类上)、@Constraints(用于字段上)、 @SQLString(用于字段上)、@SQLString(用于字段上)并在Member类中使用这些注解,这些注解的作用的是用于帮助注解处理器生成创建数据库表MEMBER的构建语句,在这里有点需要注意的是,我们使用了嵌套注解@Constraints,该注解主要用于判断字段是否为null或者字段是否唯一。必须清楚认识到上述提供的注解生命周期必须为@Retention(RetentionPolicy.RUNTIME),即运行时,这样才可以使用反射机制获取其信息。有了上述注解和使用,剩余的就是编写上述的注解处理器了,前面我们聊了很多注解,其处理器要么是Java自身已提供、要么是框架已提供的,我们自己都没有涉及到注解处理器的编写,但上述定义处理SQL的注解,其处理器必须由我们自己编写了,如下

package com.zejian.annotationdemo;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by zejian on 2017/5/13.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 * 运行时注解处理器,构造表创建语句
 */
public class TableCreator {

  public static String createTableSql(String className) throws ClassNotFoundException {
    Class<?> cl = Class.forName(className);
    DBTable dbTable = cl.getAnnotation(DBTable.class);
    //如果没有表注解,直接返回
    if(dbTable == null) {
      System.out.println(
              "No DBTable annotations in class " + className);
      return null;
    }
    String tableName = dbTable.name();
    // If the name is empty, use the Class name:
    if(tableName.length() < 1)
      tableName = cl.getName().toUpperCase();
    List<String> columnDefs = new ArrayList<String>();
    //通过Class类API获取到所有成员字段
    for(Field field : cl.getDeclaredFields()) {
      String columnName = null;
      //获取字段上的注解
      Annotation[] anns = field.getDeclaredAnnotations();
      if(anns.length < 1)
        continue; // Not a db table column

      //判断注解类型
      if(anns[0] instanceof SQLInteger) {
        SQLInteger sInt = (SQLInteger) anns[0];
        //获取字段对应列名称,如果没有就是使用字段名称替代
        if(sInt.name().length() < 1)
          columnName = field.getName().toUpperCase();
        else
          columnName = sInt.name();
        //构建语句
        columnDefs.add(columnName + " INT" +
                getConstraints(sInt.constraint()));
      }
      //判断String类型
      if(anns[0] instanceof SQLString) {
        SQLString sString = (SQLString) anns[0];
        // Use field name if name not specified.
        if(sString.name().length() < 1)
          columnName = field.getName().toUpperCase();
        else
          columnName = sString.name();
        columnDefs.add(columnName + " VARCHAR(" +
                sString.value() + ")" +
                getConstraints(sString.constraint()));
      }


    }
    //数据库表构建语句
    StringBuilder createCommand = new StringBuilder(
            "CREATE TABLE " + tableName + "(");
    for(String columnDef : columnDefs)
      createCommand.append("\n    " + columnDef + ",");

    // Remove trailing comma
    String tableCreate = createCommand.substring(
            0, createCommand.length() - 1) + ");";
    return tableCreate;
  }


    /**
     * 判断该字段是否有其他约束
     * @param con
     * @return
     */
  private static String getConstraints(Constraints con) {
    String constraints = "";
    if(!con.allowNull())
      constraints += " NOT NULL";
    if(con.primaryKey())
      constraints += " PRIMARY KEY";
    if(con.unique())
      constraints += " UNIQUE";
    return constraints;
  }

  public static void main(String[] args) throws Exception {
    String[] arg={"com.zejian.annotationdemo.Member"};
    for(String className : arg) {
      System.out.println("Table Creation SQL for " +
              className + " is :\n" + createTableSql(className));
    }

    /**
     * 输出结果:
     Table Creation SQL for com.zejian.annotationdemo.Member is :
     CREATE TABLE MEMBER(
     ID VARCHAR(50) NOT NULL PRIMARY KEY,
     NAME VARCHAR(30) NOT NULL,
     AGE INT NOT NULL,
     DESCRIPTION VARCHAR(150)
     );
     */
  }
}

如果对反射比较熟悉的同学,上述代码就相对简单了,我们通过传递Member的全路径后通过Class.forName()方法获取到Member的class对象,然后利用Class对象中的方法获取所有成员字段Field,最后利用field.getDeclaredAnnotations()遍历每个Field上的注解再通过注解的类型判断来构建建表的SQL语句。这便是利用注解结合反射来构建SQL语句的简单的处理器模型,是否已回想起Hibernate?

Java 8中注解增强

元注解@Repeatable

元注解@Repeatable是JDK1.8新加入的,它表示在同一个位置重复相同的注解。在没有该注解前,一般是无法在同一个类型上使用相同的注解的

//Java8前无法这样使用
@FilterPath("/web/update")
@FilterPath("/web/add")
public class A {}

Java8前如果是想实现类似的功能,我们需要在定义@FilterPath注解时定义一个数组元素接收多个值如下

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface FilterPath {
    String [] value();
}

//使用
@FilterPath({"/update","/add"})
public class A { }

但在Java8新增了@Repeatable注解后就可以采用如下的方式定义并使用了

package com.zejian.annotationdemo;

import java.lang.annotation.*;

/**
 * Created by zejian on 2017/5/20.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
//使用Java8新增@Repeatable原注解
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(FilterPaths.class)//参数指明接收的注解class
public @interface FilterPath {
    String  value();
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface FilterPaths {
    FilterPath[] value();
}

//使用案例
@FilterPath("/web/update")
@FilterPath("/web/add")
@FilterPath("/web/delete")
class AA{ }

我们可以简单理解为通过使用@Repeatable后,将使用@FilterPaths注解作为接收同一个类型上重复注解的容器,而每个@FilterPath则负责保存指定的路径串。为了处理上述的新增注解,Java8还在AnnotatedElement接口新增了getDeclaredAnnotationsByType() 和 getAnnotationsByType()两个方法并在接口给出了默认实现,在指定@Repeatable的注解时,可以通过这两个方法获取到注解相关信息。但请注意,旧版API中的getDeclaredAnnotation()和 getAnnotation()是不对@Repeatable注解的处理的(除非该注解没有在同一个声明上重复出现)。注意getDeclaredAnnotationsByType方法获取到的注解不包括父类,其实当 getAnnotationsByType()方法调用时,其内部先执行了getDeclaredAnnotationsByType方法,只有当前类不存在指定注解时,getAnnotationsByType()才会继续从其父类寻找,但请注意如果@FilterPath和@FilterPaths没有使用了@Inherited的话,仍然无法获取。下面通过代码来演示:

/**
 * Created by zejian on 2017/5/20.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
//使用Java8新增@Repeatable原注解
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(FilterPaths.class)
public @interface FilterPath {
    String  value();
}


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface FilterPaths {
    FilterPath[] value();
}

@FilterPath("/web/list")
class CC { }

//使用案例
@FilterPath("/web/update")
@FilterPath("/web/add")
@FilterPath("/web/delete")
class AA extends CC{
    public static void main(String[] args) {

        Class<?> clazz = AA.class;
        //通过getAnnotationsByType方法获取所有重复注解
        FilterPath[] annotationsByType = clazz.getAnnotationsByType(FilterPath.class);
        FilterPath[] annotationsByType2 = clazz.getDeclaredAnnotationsByType(FilterPath.class);
        if (annotationsByType != null) {
            for (FilterPath filter : annotationsByType) {
                System.out.println("1:"+filter.value());
            }
        }

        System.out.println("-----------------");

        if (annotationsByType2 != null) {
            for (FilterPath filter : annotationsByType2) {
                System.out.println("2:"+filter.value());
            }
        }


        System.out.println("使用getAnnotation的结果:"+clazz.getAnnotation(FilterPath.class));


        /**
         * 执行结果(当前类拥有该注解FilterPath,则不会从CC父类寻找)
         1:/web/update
         1:/web/add
         1:/web/delete
         -----------------
         2:/web/update
         2:/web/add
         2:/web/delete
         使用getAnnotation的结果:null
         */
    }
}

从执行结果来看如果当前类拥有该注解@FilterPath,则getAnnotationsByType方法不会从CC父类寻找,下面看看另外一种情况,即AA类上没有@FilterPath注解

/**
 * Created by zejian on 2017/5/20.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
//使用Java8新增@Repeatable原注解
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited //添加可继承元注解
@Repeatable(FilterPaths.class)
public @interface FilterPath {
    String  value();
}


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited //添加可继承元注解
@interface FilterPaths {
    FilterPath[] value();
}

@FilterPath("/web/list")
@FilterPath("/web/getList")
class CC { }

//AA上不使用@FilterPath注解,getAnnotationsByType将会从父类查询
class AA extends CC{
    public static void main(String[] args) {

        Class<?> clazz = AA.class;
        //通过getAnnotationsByType方法获取所有重复注解
        FilterPath[] annotationsByType = clazz.getAnnotationsByType(FilterPath.class);
        FilterPath[] annotationsByType2 = clazz.getDeclaredAnnotationsByType(FilterPath.class);
        if (annotationsByType != null) {
            for (FilterPath filter : annotationsByType) {
                System.out.println("1:"+filter.value());
            }
        }

        System.out.println("-----------------");

        if (annotationsByType2 != null) {
            for (FilterPath filter : annotationsByType2) {
                System.out.println("2:"+filter.value());
            }
        }


        System.out.println("使用getAnnotation的结果:"+clazz.getAnnotation(FilterPath.class));


        /**
         * 执行结果(当前类没有@FilterPath,getAnnotationsByType方法从CC父类寻找)
         1:/web/list
         1:/web/getList
         -----------------
         使用getAnnotation的结果:null
         */
    }
}

注意定义@FilterPath和@FilterPath时必须指明@Inherited,getAnnotationsByType方法否则依旧无法从父类获取@FilterPath注解,这是为什么呢,不妨看看getAnnotationsByType方法的实现源码:

//接口默认实现方法
default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) {
//先调用getDeclaredAnnotationsByType方法
T[] result = getDeclaredAnnotationsByType(annotationClass);

//判断当前类获取到的注解数组是否为0
if (result.length == 0 && this instanceof Class && 
//判断定义注解上是否使用了@Inherited元注解 
 AnnotationType.getInstance(annotationClass).isInherited()) { // Inheritable
        //从父类获取
       Class<?> superClass = ((Class<?>) this).getSuperclass();
   if (superClass != null) {
      result = superClass.getAnnotationsByType(annotationClass);
       }
   }

   return result;
}

新增的两种ElementType

在Java8中 ElementType 新增两个枚举成员,TYPE_PARAMETER 和 TYPE_USE ,在Java8前注解只能标注在一个声明(如字段、类、方法)上,Java8后,新增的TYPE_PARAMETER可以用于标注类型参数,而TYPE_USE则可以用于标注任意类型(不包括class)。如下所示

//TYPE_PARAMETER 标注在类型参数上
class D<@Parameter T> { }

//TYPE_USE则可以用于标注任意类型(不包括class)
//用于父类或者接口
class Image implements @Rectangular Shape { }

//用于构造函数
new @Path String("/usr/bin")

//用于强制转换和instanceof检查,注意这些注解中用于外部工具,它们不会对类型转换或者instanceof的检查行为带来任何影响。
String path=(@Path String)input;
if(input instanceof @Path String)

//用于指定异常
public Person read() throws @Localized IOException.

//用于通配符绑定
List<@ReadOnly ? extends Person>
List<? extends @ReadOnly Person>

@NotNull String.class //非法,不能标注class
import java.lang.@NotNull String //非法,不能标注import

这里主要说明一下TYPE_USE,类型注解用来支持在Java的程序中做强类型检查,配合第三方插件工具(如Checker Framework),可以在编译期检测出runtime error(如UnsupportedOperationException、NullPointerException异常),避免异常延续到运行期才发现,从而提高代码质量,这就是类型注解的主要作用。总之Java 8 新增加了两个注解的元素类型ElementType.TYPE_USE 和ElementType.TYPE_PARAMETER ,通过它们,我们可以把注解应用到各种新场合中。

ok~,关于注解暂且聊到这,实际上还有一个大块的知识点没详细聊到,源码级注解处理器,这个话题博主打算后面另开一篇分析。

 

主要参考资料 《Thinking in Java》

深入理解Java类加载器(ClassLoader)

文章目录


类加载的机制的层次结构
每个编写的".java"拓展名类文件都存储着需要执行的程序逻辑,这些".java"文件经过Java编译器编译成拓展名为".class"的文件,".class"文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的".class"文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载,这里我们需要了解一下类加载的过程,如下:

  • 加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象

  • 验证:目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。

  • 准备:为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。

  • 解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析(这里涉及到字节码变量的引用,如需更详细了解,可参考《深入Java虚拟机》)。

  • 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。

这便是类加载的5个过程,而类加载器的任务是根据一个类的全限定名来读取此类的二进制字节流到JVM中,然后转换为一个与目标类对应的java.lang.Class对象实例,在虚拟机提供了3种类加载器,引导(Bootstrap)类加载器、扩展(Extension)类加载器、系统(System)类加载器(也称应用类加载器),下面分别介绍
##启动(Bootstrap)类加载器
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib路径下的核心类库或-Xbootclasspath参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类)。

##扩展(Extension)类加载器
扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext目录下或者由系统变量-Djava.ext.dir指定位路径中的类库,开发者可以直接使用标准扩展类加载器。

//ExtClassLoader类中获取路径的代码
private static File[] getExtDirs() {
     //加载<JAVA_HOME>/lib/ext目录中的类库
     String s = System.getProperty("java.ext.dirs");
     File[] dirs;
     if (s != null) {
         StringTokenizer st =
             new StringTokenizer(s, File.pathSeparator);
         int count = st.countTokens();
         dirs = new File[count];
         for (int i = 0; i < count; i++) {
             dirs[i] = new File(st.nextToken());
         }
     } else {
         dirs = new File[0];
     }
     return dirs;
 }

##系统(System)类加载器
也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader。它负责加载系统类路径java -classpath-D java.class.path 指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()方法可以获取到该类加载器。
  在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,在必要时,我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式即把请求交由父类处理,它一种任务委派模式,下面我们进一步了解它。
#理解双亲委派模式

##双亲委派模式工作原理
双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:

双亲委派模式是在Java 1.2后引入的,其工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式,即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己想办法去完成,这不就是传说中的实力坑爹啊?那么采用这种模式有啥用呢?

##双亲委派模式优势
采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常

java.lang.SecurityException: Prohibited package name: java.lang

所以无论如何都无法加载成功的。下面我们从代码层面了解几个Java中定义的类加载器及其双亲委派模式的实现,它们类图关系如下

从图可以看出顶层的类加载器是ClassLoader类,它是一个抽象类,其后所有的类加载器都继承自ClassLoader(不包括启动类加载器),这里我们主要介绍ClassLoader中几个比较重要的方法。

  • loadClass(String)

    该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。:

    protected Class<?> loadClass(String name, boolean resolve)
          throws ClassNotFoundException
      {
          synchronized (getClassLoadingLock(name)) {
              // 先从缓存查找该class对象,找到就不用重新加载
              Class<?> c = findLoadedClass(name);
              if (c == null) {
                  long t0 = System.nanoTime();
                  try {
                      if (parent != null) {
                          //如果找不到,则委托给父类加载器去加载
                          c = parent.loadClass(name, false);
                      } else {
                      //如果没有父类,则委托给启动加载器去加载
                          c = findBootstrapClassOrNull(name);
                      }
                  } catch (ClassNotFoundException e) {
                      // ClassNotFoundException thrown if class not found
                      // from the non-null parent class loader
                  }
    
                  if (c == null) {
                      // If still not found, then invoke findClass in order
                      // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                      c = findClass(name);
    
                      // this is the defining class loader; record the stats
                      sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                      sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                      sun.misc.PerfCounter.getFindClasses().increment();
                  }
              }
              if (resolve) {//是否需要在加载时进行解析
                  resolveClass(c);
              }
              return c;
          }
      }
    

    正如loadClass方法所展示的,当类加载请求到来时,先从缓存中查找该类对象,如果存在直接返回,如果不存在则交给该类加载去的父加载器去加载,倘若没有父加载则交给顶级启动类加载器去加载,最后倘若仍没有找到,则使用findClass()方法去加载(关于findClass()稍后会进一步介绍)。从loadClass实现也可以知道如果不想重新定义加载类的规则,也没有复杂的逻辑,只想在运行时加载自己指定的类,那么我们可以直接使用this.getClass().getClassLoder.loadClass("className"),这样就可以直接调用ClassLoader的loadClass方法获取到class对象。

  • findClass(String)
    在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的(稍后会分析),ClassLoader类中findClass()方法源码如下:

    //直接抛出异常
    protected Class<?> findClass(String name) throws ClassNotFoundException {
            throw new ClassNotFoundException(name);
    }
    
  • defineClass(byte[] b, int off, int len)
    defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象,简单例子如下:

    protected Class<?> findClass(String name) throws ClassNotFoundException {
    	  // 获取类的字节数组
          byte[] classData = getClassData(name);  
          if (classData == null) {
              throw new ClassNotFoundException();
          } else {
    	      //使用defineClass生成class对象
              return defineClass(name, classData, 0, classData.length);
          }
      }
    

需要注意的是,如果直接调用defineClass()方法生成类的Class对象,这个类的Class对象并没有解析(也可以理解为链接阶段,毕竟解析是链接的最后一步),其解析操作需要等待初始化阶段进行。

  • resolveClass(Class≺?≻ c)
    使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。

上述4个方法是ClassLoader类中的比较重要的方法,也是我们可能会经常用到的方法。接看SercureClassLoader扩展了 ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接跟这个类打交道,更多是与它的子类URLClassLoader有所关联,前面说过,ClassLoader是一个抽象类,很多方法是空的没有实现,比如 findClass()、findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现,并新增了URLClassPath类协助取得Class字节码流等功能,在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁,下面是URLClassLoader的类图(利用IDEA生成的类图)

从类图结构看出URLClassLoader中存在一个URLClassPath类,通过这个类就可以找到要加载的字节码流,也就是说URLClassPath类负责找到要加载的字节码,再读取成字节流,最后通过defineClass()方法创建类的Class对象。从URLClassLoader类的结构图可以看出其构造方法都有一个必须传递的参数URL[],该参数的元素是代表字节码文件的路径,换句话说在创建URLClassLoader对象时必须要指定这个类加载器的到那个目录下找class文件。同时也应该注意URL[]也是URLClassPath类的必传参数,在创建URLClassPath对象时,会根据传递过来的URL数组中的路径判断是文件还是jar包,然后根据不同的路径创建FileLoader或者JarLoader或默认Loader类去加载相应路径下的class文件,而当JVM调用findClass()方法时,就由这3个加载器中的一个将class文件的字节码流加载到内存中,最后利用字节码流创建类的class对象。请记住,如果我们在定义类加载器时选择继承ClassLoader类而非URLClassLoader,必须手动编写findclass()方法的加载逻辑以及获取字节码流的逻辑。了解完URLClassLoader后接着看看剩余的两个类加载器,即拓展类加载器ExtClassLoader和系统类加载器AppClassLoader,这两个类都继承自URLClassLoader,是sun.misc.Launcher的静态内部类。sun.misc.Launcher主要被系统用于启动主应用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher创建的,其类主要类结构如下:

它们间的关系正如前面所阐述的那样,同时我们发现ExtClassLoader并没有重写loadClass()方法,这足矣说明其遵循双亲委派模式,而AppClassLoader重载了loadCass()方法,但最终调用的还是父类loadClass()方法,因此依然遵守双亲委派模式,重载方法源码如下:

 /**
  * Override loadClass 方法,新增包权限检测功能
  */
 public Class loadClass(String name, boolean resolve)
     throws ClassNotFoundException
 {
     int i = name.lastIndexOf('.');
     if (i != -1) {
         SecurityManager sm = System.getSecurityManager();
         if (sm != null) {
             sm.checkPackageAccess(name.substring(0, i));
         }
     }
     //依然调用父类的方法
     return (super.loadClass(name, resolve));
 }

其实无论是ExtClassLoader还是AppClassLoader都继承URLClassLoader类,因此它们都遵守双亲委托模型,这点是毋庸置疑的。ok,到此我们对ClassLoader、URLClassLoader、ExtClassLoader、AppClassLoader以及Launcher类间的关系有了比较清晰的了解,同时对一些主要的方法也有一定的认识,这里并没有对这些类的源码进行详细的分析,毕竟没有那个必要,因为我们主要弄得类与类间的关系和常用的方法同时搞清楚双亲委托模式的实现过程,为编写自定义类加载器做铺垫就足够了。ok,前面出现了很多父类加载器的说法,但每个类加载器的父类到底是谁,一直没有阐明,下面我们就通过代码验证的方式来阐明这答案。
##类加载器间的关系
我们进一步了解类加载器间的关系(并非指继承关系),主要可以分为以下4点

  • 启动类加载器,由C++实现,没有父类。

  • 拓展类加载器(ExtClassLoader),由Java语言实现,父类加载器为null

  • 系统类加载器(AppClassLoader),由Java语言实现,父类加载器为ExtClassLoader

  • 自定义类加载器,父类加载器肯定为AppClassLoader。

下面我们通过程序来验证上述阐述的观点

/**
 * Created by zejian on 2017/6/18.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
//自定义ClassLoader,完整代码稍后分析
class FileClassLoader extends  ClassLoader{
    private String rootDir;

    public FileClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }
    // 编写获取类的字节码并创建class对象的逻辑
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
       //...省略逻辑代码
    }
	//编写读取字节流的方法
    private byte[] getClassData(String className) {
        // 读取类文件的字节
        //省略代码....
    }
}

public class ClassLoaderTest {

    public static void main(String[] args) throws ClassNotFoundException {
       
			 FileClassLoader loader1 = new FileClassLoader(rootDir);
			
			  System.out.println("自定义类加载器的父加载器: "+loader1.getParent());
			  System.out.println("系统默认的AppClassLoader: "+ClassLoader.getSystemClassLoader());
			  System.out.println("AppClassLoader的父类加载器: "+ClassLoader.getSystemClassLoader().getParent());
			  System.out.println("ExtClassLoader的父类加载器: "+ClassLoader.getSystemClassLoader().getParent().getParent());
			
			/**
			输出结果:
			    自定义类加载器的父加载器: sun.misc.Launcher$AppClassLoader@29453f44
			    系统默认的AppClassLoader: sun.misc.Launcher$AppClassLoader@29453f44
			    AppClassLoader的父类加载器: sun.misc.Launcher$ExtClassLoader@6f94fa3e
			    ExtClassLoader的父类加载器: null
			*/

    }
}

代码中,我们自定义了一个FileClassLoader,这里我们继承了ClassLoader而非URLClassLoader,因此需要自己编写findClass()方法逻辑以及加载字节码的逻辑,关于自定义类加载器我们稍后会分析,这里仅需要知道FileClassLoader是自定义加载器即可,接着在main方法中,通过ClassLoader.getSystemClassLoader()获取到系统默认类加载器,通过获取其父类加载器及其父父类加载器,同时还获取了自定义类加载器的父类加载器,最终输出结果正如我们所预料的,AppClassLoader的父类加载器为ExtClassLoader,而ExtClassLoader没有父类加载器。如果我们实现自己的类加载器,它的父加载器都只会是AppClassLoader。这里我们不妨看看Lancher的构造器源码

public Launcher() {
        // 首先创建拓展类加载器
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader");
        }

        // Now create the class loader to use to launch the application
        try {
	        //再创建AppClassLoader并把extcl作为父加载器传递给AppClassLoader
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader");
        }

        //设置线程上下文类加载器,稍后分析
        Thread.currentThread().setContextClassLoader(loader);
//省略其他没必要的代码......
        }
    }

显然Lancher初始化时首先会创建ExtClassLoader类加载器,然后再创建AppClassLoader并把ExtClassLoader传递给它作为父类加载器,这里还把AppClassLoader默认设置为线程上下文类加载器,关于线程上下文类加载器稍后会分析。那ExtClassLoader类加载器为什么是null呢?看下面的源码创建过程就明白,在创建ExtClassLoader强制设置了其父加载器为null。

//Lancher中创建ExtClassLoader
extcl = ExtClassLoader.getExtClassLoader();

//getExtClassLoader()方法
public static ExtClassLoader getExtClassLoader() throws IOException{

  //........省略其他代码 
  return new ExtClassLoader(dirs);                     
  // .........
}

//构造方法
public ExtClassLoader(File[] dirs) throws IOException {
   //调用父类构造URLClassLoader传递null作为parent
   super(getExtURLs(dirs), null, factory);
}

//URLClassLoader构造
public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {

显然ExtClassLoader的父类为null,而AppClassLoader的父加载器为ExtClassLoader,所有自定义的类加载器其父加载器只会是AppClassLoader,注意这里所指的父类并不是Java继承关系中的那种父子关系。

#类与类加载器
##类与类加载器
在JVM中表示两个class对象是否为同一个类对象存在两个必要条件

  • 类的完整类名必须一致,包括包名。
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。

也就是说,在JVM中,即使这个两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载,但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的,这是因为不同的ClassLoader实例对象都拥有不同的独立的类名称空间,所以加载的class对象也会存在不同的类名空间中,但前提是覆写loadclass方法,从前面双亲委派模式对loadClass()方法的源码分析中可以知,在方法第一步会通过Class<?> c = findLoadedClass(name);从缓存查找,类名完整名称相同则不会再次被加载,因此我们必须绕过缓存查询才能重新加载class对象。当然也可直接调用findClass()方法,这样也避免从缓存查找,如下

String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
//创建两个不同的自定义类加载器实例
FileClassLoader loader1 = new FileClassLoader(rootDir);
FileClassLoader loader2 = new FileClassLoader(rootDir);
//通过findClass创建类的Class对象
Class<?> object1=loader1.findClass("com.zejian.classloader.DemoObj");
Class<?> object2=loader2.findClass("com.zejian.classloader.DemoObj");

System.out.println("findClass->obj1:"+object1.hashCode());
System.out.println("findClass->obj2:"+object2.hashCode());

/**
  * 直接调用findClass方法输出结果:
  * findClass->obj1:723074861
    findClass->obj2:895328852
    生成不同的实例
  */

如果调用父类的loadClass方法,结果如下,除非重写loadClass()方法去掉缓存查找步骤,不过现在一般都不建议重写loadClass()方法。

//直接调用父类的loadClass()方法
Class<?> obj1 =loader1.loadClass("com.zejian.classloader.DemoObj");
Class<?> obj2 =loader2.loadClass("com.zejian.classloader.DemoObj");

//不同实例对象的自定义类加载器
System.out.println("loadClass->obj1:"+obj1.hashCode());
System.out.println("loadClass->obj2:"+obj2.hashCode());
//系统类加载器
System.out.println("Class->obj3:"+DemoObj.class.hashCode());

/**
* 直接调用loadClass方法的输出结果,注意并没有重写loadClass方法
* loadClass->obj1:1872034366
  loadClass->obj2:1872034366
  Class->    obj3:1872034366
  都是同一个实例
*/

所以如果不从缓存查询相同完全类名的class对象,那么只有ClassLoader的实例对象不同,同一字节码文件创建的class对象自然也不会相同。
##了解class文件的显示加载与隐式加载的概念
所谓class文件的显示加载与隐式加载的方式是指JVM加载class文件到内存的方式,显示加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)this.getClass().getClassLoader().loadClass()加载class对象。而隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。在日常开发以上两种方式一般会混合使用,这里我们知道有这么回事即可。
#编写自己的类加载器
通过前面的分析可知,实现自定义类加载器需要继承ClassLoader或者URLClassLoader,继承ClassLoader则需要自己重写findClass()方法并编写加载逻辑,继承URLClassLoader则可以省去编写findClass()方法以及class文件加载转换成字节码流的代码。那么编写自定义类加载器的意义何在呢?

  • 当class文件不在ClassPath路径下,默认系统类加载器无法找到该class文件,在这种情况下我们需要实现一个自定义的ClassLoader来加载特定路径下的class文件生成class对象。

  • 当一个class文件是通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存中,这种情况下也需要编写自定义的ClassLoader并实现相应的逻辑。

  • 当需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑。

##自定义File类加载器
这里我们继承ClassLoader实现自定义的特定路径下的文件类加载器并加载编译后DemoObj.class,源码代码如下

public class DemoObj {
    @Override
    public String toString() {
        return "I am DemoObj";
    }
}
package com.zejian.classloader;

import java.io.*;

/**
 * Created by zejian on 2017/6/21.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class FileClassLoader extends ClassLoader {
    private String rootDir;

    public FileClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    /**
     * 编写findClass方法的逻辑
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 获取类的class文件字节数组
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            //直接生成class对象
            return defineClass(name, classData, 0, classData.length);
        }
    }

    /**
     * 编写获取class文件并转换为字节码流的逻辑
     * @param className
     * @return
     */
    private byte[] getClassData(String className) {
        // 读取类文件的字节
        String path = classNameToPath(className);
        try {
            InputStream ins = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            // 读取类文件的字节码
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 类文件的完全路径
     * @param className
     * @return
     */
    private String classNameToPath(String className) {
        return rootDir + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }

    public static void main(String[] args) throws ClassNotFoundException {
        String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
        //创建自定义文件类加载器
        FileClassLoader loader = new FileClassLoader(rootDir);

        try {
            //加载指定的class文件
            Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");
            System.out.println(object1.newInstance().toString());
          
            //输出结果:I am DemoObj
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

显然我们通过getClassData()方法找到class文件并转换为字节流,并重写findClass()方法,利用defineClass()方法创建了类的class对象。在main方法中调用了loadClass()方法加载指定路径下的class文件,由于启动类加载器、拓展类加载器以及系统类加载器都无法在其路径下找到该类,因此最终将有自定义类加载器加载,即调用findClass()方法进行加载。如果继承URLClassLoader实现,那代码就更简洁了,如下:

/**
 * Created by zejian on 2017/6/21.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class FileUrlClassLoader extends URLClassLoader {

    public FileUrlClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public FileUrlClassLoader(URL[] urls) {
        super(urls);
    }

    public FileUrlClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(urls, parent, factory);
    }


    public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {
        String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
        //创建自定义文件类加载器
        File file = new File(rootDir);
        //File to URI
        URI uri=file.toURI();
        URL[] urls={uri.toURL()};

        FileUrlClassLoader loader = new FileUrlClassLoader(urls);

        try {
            //加载指定的class文件
            Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");
            System.out.println(object1.newInstance().toString());
          
            //输出结果:I am DemoObj
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

非常简洁除了需要重写构造器外无需编写findClass()方法及其class文件的字节流转换逻辑。
##自定义网络类加载器
自定义网络类加载器,主要用于读取通过网络传递的class文件(在这里我们省略class文件的解密过程),并将其转换成字节流生成对应的class对象,如下

/**
 * Created by zejian on 2017/6/21.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class NetClassLoader extends ClassLoader {

    private String url;//class文件的URL

    public NetClassLoader(String url) {
        this.url = url;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassDataFromNet(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    /**
     * 从网络获取class文件
     * @param className
     * @return
     */
    private byte[] getClassDataFromNet(String className) {
        String path = classNameToPath(className);
        try {
            URL url = new URL(path);
            InputStream ins = url.openStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            // 读取类文件的字节
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            //这里省略解密的过程.......
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private String classNameToPath(String className) {
        // 得到类文件的URL
        return url + "/" + className.replace('.', '/') + ".class";
    }

}

比较简单,主要是在获取字节码流时的区别,从网络直接获取到字节流再转车字节数组然后利用defineClass方法创建class对象,如果继承URLClassLoader类则和前面文件路径的实现是类似的,无需担心路径是filePath还是Url,因为URLClassLoader内的URLClassPath对象会根据传递过来的URL数组中的路径判断是文件还是jar包,然后根据不同的路径创建FileLoader或者JarLoader或默认类Loader去读取对于的路径或者url下的class文件。
##热部署类加载器
所谓的热部署就是利用同一个class文件不同的类加载器在内存创建出两个不同的class对象(关于这点的原因前面已分析过,即利用不同的类加载实例),由于JVM在加载类之前会检测请求的类是否已加载过(即在loadClass()方法中调用findLoadedClass()方法),如果被加载过,则直接从缓存获取,不会重新加载。注意同一个类加载器的实例和同一个class文件只能被加载器一次,多次加载将报错,因此我们实现的热部署必须让同一个class文件可以根据不同的类加载器重复加载,以实现所谓的热部署。实际上前面的实现的FileClassLoader和FileUrlClassLoader已具备这个功能,但前提是直接调用findClass()方法,而不是调用loadClass()方法,因为ClassLoader中loadClass()方法体中调用findLoadedClass()方法进行了检测是否已被加载,因此我们直接调用findClass()方法就可以绕过这个问题,当然也可以重新loadClass方法,但强烈不建议这么干。利用FileClassLoader类测试代码如下:

 public static void main(String[] args) throws ClassNotFoundException {
        String rootDir="/Users/zejian/Downloads/Java8_Action/src/main/java/";
        //创建自定义文件类加载器
        FileClassLoader loader = new FileClassLoader(rootDir);
        FileClassLoader loader2 = new FileClassLoader(rootDir);

        try {
            //加载指定的class文件,调用loadClass()
            Class<?> object1=loader.loadClass("com.zejian.classloader.DemoObj");
            Class<?> object2=loader2.loadClass("com.zejian.classloader.DemoObj");

            System.out.println("loadClass->obj1:"+object1.hashCode());
            System.out.println("loadClass->obj2:"+object2.hashCode());

            //加载指定的class文件,直接调用findClass(),绕过检测机制,创建不同class对象。
            Class<?> object3=loader.findClass("com.zejian.classloader.DemoObj");
            Class<?> object4=loader2.findClass("com.zejian.classloader.DemoObj");

            System.out.println("loadClass->obj3:"+object3.hashCode());
            System.out.println("loadClass->obj4:"+object4.hashCode());

            /**
             * 输出结果:
             * loadClass->obj1:644117698
               loadClass->obj2:644117698
               findClass->obj3:723074861
               findClass->obj4:895328852
             */

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

#双亲委派模型的破坏者-线程上下文类加载器

    在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader加载器SPI的实现类。在这种情况下,我们就需要一种特殊的类加载器来加载第三方的类库,而线程上下文类加载器就是很好的选择。
    线程上下文类加载器(contextClassLoader)是从 JDK 1.2 开始引入的,我们可以通过java.lang.Thread类中的getContextClassLoader()和 setContextClassLoader(ClassLoader cl)方法来获取和设置线程的上下文类加载器。如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类和资源,如下图所示,以jdbc.jar加载为例

从图可知rt.jar核心包是有Bootstrap类加载器加载的,其内包含SPI核心接口类,由于SPI中的类经常需要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得Java类加载器变得更加灵活。为了进一步证实这种场景,不妨看看DriverManager类的源码,DriverManager是Java核心rt.jar包中的类,该类用来管理不同数据库的实现驱动即Driver,它们都实现了Java核心包中的java.sql.Driver接口,如mysql驱动包中的com.mysql.jdbc.Driver,这里主要看看如何加载外部实现类,在DriverManager初始化时会执行如下代码

//DriverManager是Java核心包rt.jar的类
public class DriverManager {
	//省略不必要的代码
    static {
        loadInitialDrivers();//执行该方法
        println("JDBC DriverManager initialized");
    }

//loadInitialDrivers方法
 private static void loadInitialDrivers() {
     sun.misc.Providers()
     AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
				//加载外部的Driver的实现类
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
              //省略不必要的代码......
            }
        });
    }

在DriverManager类初始化时执行了loadInitialDrivers()方法,在该方法中通过ServiceLoader.load(Driver.class);去加载外部实现的驱动类,ServiceLoader类会去读取mysql的jdbc.jar下META-INF文件的内容,如下所示

而com.mysql.jdbc.Driver继承类如下:

public class Driver extends com.mysql.cj.jdbc.Driver {
    public Driver() throws SQLException {
        super();
    }

    static {
        System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. "
                + "The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
    }
}

从注释可以看出平常我们使用com.mysql.jdbc.Driver已被丢弃了,取而代之的是com.mysql.cj.jdbc.Driver,也就是说官方不再建议我们使用如下代码注册mysql驱动

//不建议使用该方式注册驱动类
Class.forName("com.mysql.jdbc.Driver");
String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");
      

而是直接去掉注册步骤,如下即可

String url = "jdbc:mysql://localhost:3306/cm-storylocker?characterEncoding=UTF-8";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root@555");
      

这样ServiceLoader会帮助我们处理一切,并最终通过load()方法加载,看看load()方法实现

public static <S> ServiceLoader<S> load(Class<S> service) {
	 //通过线程上下文类加载器加载
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      return ServiceLoader.load(service, cl);
  }

很明显了确实通过线程上下文类加载器加载的,实际上核心包的SPI类对外部实现类的加载都是基于线程上下文类加载器执行的,通过这种方式实现了Java核心代码内部去调用外部实现类。我们知道线程上下文类加载器默认情况下就是AppClassLoader,那为什么不直接通过getSystemClassLoader()获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()方法获取AppClassLoader加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader,而是Java Web应用服自家的类加载器,类加载器不同。,所以我们应用该少用getSystemClassLoader()。总之不同的服务使用的可能默认ClassLoader是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader,从而避免不必要的问题。ok~.关于线程上下文类加载器暂且聊到这,前面阐述的DriverManager类,大家可以自行看看源码,相信会有更多的体会,另外关于ServiceLoader本篇并没有过多的阐述,毕竟我们主题是类加载器,但ServiceLoader是个很不错的解耦机制,大家可以自行查阅其相关用法。

ok~,本篇到此告一段落,如有误处,欢迎留言,谢谢。

参考资料:
http://blog.csdn.net/yangcheng33/article/details/52631940
http://ifeve.com/wp-content/uploads/2014/03/JSR133中文版1.pdf

《深入理解JVM虚拟机》
《深入分析Java Web 技术内幕》

深入理解Java并发之synchronized实现原理

线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据。因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),这点确实也是很重要的。

synchronized的三种应用方式

synchronized关键字最主要有以下3种应用方式,下面分别介绍

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized作用于实例方法

所谓的实例对象锁就是用synchronized修饰实例对象中的实例方法,注意是实例方法不包括静态方法,如下

public class AccountingSync implements Runnable{
    //共享资源(临界资源)
    static int i=0;

    /**
     * synchronized 修饰实例方法
     */
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(i);
    }
    /**
     * 输出结果:
     * 2000000
     */
}

上述代码中,我们开启两个线程操作同一个共享资源即变量i,由于i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。此时我们应该注意到synchronized修饰的是实例方法increase,在这样的情况下,当前线程的锁便是实例对象instance,注意Java中的线程同步锁可以是任意对象。从代码执行结果来看确实是正确的,倘若我们没有使用synchronized关键字,其最终输出结果就很可能小于2000000,这便是synchronized关键字的作用。这里我们还需要意识到,当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj2),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了,如下代码将演示出该现象

public class AccountingSyncBad implements Runnable{
    static int i=0;
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncBad());
        //new新实例
        Thread t2=new Thread(new AccountingSyncBad());
        t1.start();
        t2.start();
        //join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

上述代码与前面不同的是我们同时创建了两个新实例AccountingSyncBad,然后启动两个不同的线程对共享变量i进行操作,但很遗憾操作结果是1452317而不是期望结果2000000,因为上述代码犯了严重的错误,虽然我们使用synchronized修饰了increase方法,但却new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2都会进入各自的对象锁,也就是说t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。解决这种困境的的方式是将synchronized作用于静态的increase方法,这样的话,对象锁就当前类对象,由于无论创建多少个实例对象,但对于的类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的。下面我们看看如何使用将synchronized作用于静态的increase方法。

synchronized作用于静态方法

当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁,看如下代码

public class AccountingSyncClass implements Runnable{
    static int i=0;

    /**
     * 作用于静态方法,锁是当前class对象,也就是
     * AccountingSyncClass类对应的class对象
     */
    public static synchronized void increase(){
        i++;
    }

    /**
     * 非静态,访问时锁不一样不会发生互斥
     */
    public synchronized void increase4Obj(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncClass());
        //new心事了
        Thread t2=new Thread(new AccountingSyncClass());
        //启动线程
        t1.start();t2.start();

        t1.join();t2.join();
        System.out.println(i);
    }
}

由于synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象。注意代码中的increase4Obj方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同,但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。

synchronized同步代码块

除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        //省略其他耗时操作....
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                    i++;
              }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:

//this,当前实例对象锁
synchronized(this){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

//class对象锁
synchronized(AccountingSync.class){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

了解完synchronized的基本含义及其使用方式后,下面我们将进一步深入理解synchronized的底层实现原理。

synchronized底层语义原理

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。下面先来了解一个概念Java对象头,这对深入理解synchronized实现原理非常关键。

理解Java对象头与Monitor

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下:

  • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

  • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

而对于顶部,则是Java头对象,它实现synchronized的锁对象的基础,这点我们重点分析它,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字来存储对象头(如果对象是数组则会分配3个字,多出来的1个字记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:

虚拟机位数头对象结构说明
32/64bit Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bit Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构

锁状态25bit4bit1bit是否是偏向锁2bit 锁标志位
无锁状态 对象HashCode 对象分代年龄 0 01

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:

其中轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增加的,稍后我们会简要分析。这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示

由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析),ok~,有了上述知识基础后,下面我们将进一步分析synchronized在字节码层面的具体语义实现。

synchronized代码块底层原理

现在我们重新定义一个synchronized修饰的同步代码块,在代码块中操作共享变量i,如下

public class SyncCodeBlock {

   public int i;

   public void syncTask(){
       //同步代码库
       synchronized (this){
           i++;
       }
   }
}

编译上述代码并使用javap反编译后得到字节码如下(这里我们省略一部分没有必要的信息):

Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
  Last modified 2017-6-2; size 426 bytes
  MD5 checksum c80bc322c87b312de760942820b4fed5
  Compiled from "SyncCodeBlock.java"
public class com.zejian.concurrencys.SyncCodeBlock
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
  //........省略常量池中数据
  //构造函数
  public com.zejian.concurrencys.SyncCodeBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
  //===========主要看看syncTask方法实现================
  public void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter  //注意此处,进入同步方法
         4: aload_0
         5: dup
         6: getfield      #2             // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2            // Field i:I
        14: aload_1
        15: monitorexit   //注意此处,退出同步方法
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit //注意此处,退出同步方法
        22: aload_2
        23: athrow
        24: return
      Exception table:
      //省略其他字节码.......
}
SourceFile: "SyncCodeBlock.java"

我们主要关注字节码中的如下代码

3: monitorenter  //进入同步方法
//..........省略其他  
15: monitorexit   //退出同步方法
16: goto          24
//省略其他.......
21: monitorexit //退出同步方法

从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

synchronized方法底层原理

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。下面我们看看字节码层面如何实现:

public class SyncMethod {

   public int i;

   public synchronized void syncTask(){
           i++;
   }
}

使用javap反编译后的字节码如下:

Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class
  Last modified 2017-6-2; size 308 bytes
  MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
  Compiled from "SyncMethod.java"
public class com.zejian.concurrencys.SyncMethod
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool;

   //省略没必要的字节码
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}
SourceFile: "SyncMethod.java"

从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,接下来我们将简单了解一下Java官方在JVM层面对synchronized锁的优化。

Java虚拟机对synchronized的优化

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段,这里并不打算深入到每个锁的实现和转换过程更多地是阐述Java虚拟机所提供的每个锁的核心优化思想,毕竟涉及到具体过程比较繁琐,如需了解详细过程可以查阅《深入理解Java虚拟机原理》。

偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

/**
 * Created by zejian on 2017/6/4.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 * 消除StringBuffer同步锁
 */
public class StringBufferRemoveSync {

    public void add(String str1, String str2) {
        //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
        //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
        for (int i = 0; i < 10000000; i++) {
            rmsync.add("abc", "123");
        }
    }

}

关于synchronized 可能需要了解的关键点

synchronized的可重入性

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。如下:

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){

            //this,当前实例对象锁
            synchronized(this){
                i++;
                increase();//synchronized的可重入性
            }
        }
    }

    public synchronized void increase(){
        j++;
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

正如代码所演示的,在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现,需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。

线程中断与synchronized

线程中断

正如中断二字所表达的意义,在线程运行(run方法)中间打断它,在Java中,提供了以下3个有关线程中断的方法

//中断线程(实例方法)
public void Thread.interrupt();

//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();

//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();

当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,使用Thread.interrupt()方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态),如下代码将演示该过程:

public class InterruputSleepThread3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                //while在try中,通过异常中断就可以退出run循环
                try {
                    while (true) {
                        //当前线程处于阻塞状态,异常必须捕捉处理,无法往外抛出
                        TimeUnit.SECONDS.sleep(2);
                    }
                } catch (InterruptedException e) {
                    System.out.println("Interruted When Sleep");
                    boolean interrupt = this.isInterrupted();
                    //中断状态被复位
                    System.out.println("interrupt:"+interrupt);
                }
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        //中断处于阻塞状态的线程
        t1.interrupt();

        /**
         * 输出结果:
           Interruted When Sleep
           interrupt:false
         */
    }
}

如上述代码所示,我们创建一个线程,并在线程中调用了sleep方法从而使用线程进入阻塞状态,启动线程后,调用线程实例对象的interrupt方法中断阻塞异常,并抛出InterruptedException异常,此时中断状态也将被复位。这里有些人可能会诧异,为什么不用Thread.sleep(2000);而是用TimeUnit.SECONDS.sleep(2);其实原因很简单,前者使用时并没有明确的单位说明,而后者非常明确表达秒的单位,事实上后者的内部实现最终还是调用了Thread.sleep(2000);,但为了编写的代码语义更清晰,建议使用TimeUnit.SECONDS.sleep(2);的方式,注意TimeUnit是个枚举类型。ok~,除了阻塞中断的情景,我们还可能会遇到处于运行期且非阻塞的状态的线程,这种情况下,直接调用Thread.interrupt()中断线程是不会得到任响应的,如下代码,将无法中断非阻塞状态下的线程:

public class InterruputThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    System.out.println("未被中断");
                }
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /**
         * 输出结果(无限执行):
             未被中断
             未被中断
             未被中断
             ......
         */
    }
}

虽然我们调用了interrupt方法,但线程t1并未被中断,因为处于非阻塞状态的线程需要我们手动进行中断检测并结束程序,改进后代码如下:

public class InterruputThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    //判断当前线程是否被中断
                    if (this.isInterrupted()){
                        System.out.println("线程中断");
                        break;
                    }
                }

                System.out.println("已跳出循环,线程中断!");
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /**
         * 输出结果:
            线程中断
            已跳出循环,线程中断!
         */
    }
}

是的,我们在代码中使用了实例方法isInterrupted判断线程是否已被中断,如果被中断将跳出循环以此结束线程,注意非阻塞状态调用interrupt()并不会导致中断状态重置。综合所述,可以简单总结一下中断两种情况,一种是当线程处于阻塞状态或者试图执行一个阻塞操作时,我们可以使用实例方法interrupt()进行线程中断,执行中断操作后将会抛出interruptException异常(该异常必须捕捉无法向外抛出)并将中断状态复位,另外一种是当线程处于运行状态时,我们也可调用实例方法interrupt()进行线程中断,但同时必须手动判断中断状态,并编写中断线程的代码(其实就是结束run方法体的代码)。有时我们在编码时可能需要兼顾以上两种情况,那么就可以如下编写:

public void run(){
    try {
    //判断当前线程是否已中断,注意interrupted方法是静态的,执行后会对中断状态进行复位
    while (!Thread.interrupted()) {
        TimeUnit.SECONDS.sleep(2);
    }
    } catch (InterruptedException e) {

    }
}

中断与synchronized

事实上线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,也就是对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。演示代码如下

/**
 * Created by zejian on 2017/6/2.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class SynchronizedBlocked implements Runnable{

    public synchronized void f() {
        System.out.println("Trying to call f()");
        while(true) // Never releases lock
            Thread.yield();
    }

    /**
     * 在构造器中创建新线程并启动获取对象锁
     */
    public SynchronizedBlocked() {
        //该线程已持有当前实例锁
        new Thread() {
            public void run() {
                f(); // Lock acquired by this thread
            }
        }.start();
    }
    public void run() {
        //中断判断
        while (true) {
            if (Thread.interrupted()) {
                System.out.println("中断线程!!");
                break;
            } else {
                f();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        SynchronizedBlocked sync = new SynchronizedBlocked();
        Thread t = new Thread(sync);
        //启动后调用f()方法,无法获取当前实例锁处于等待状态
        t.start();
        TimeUnit.SECONDS.sleep(1);
        //中断线程,无法生效
        t.interrupt();
    }
}

我们在SynchronizedBlocked构造函数中创建一个新线程并启动获取调用f()获取到当前实例锁,由于SynchronizedBlocked自身也是线程,启动后在其run方法中也调用了f(),但由于对象锁被其他线程占用,导致t线程只能等到锁,此时我们调用了t.interrupt();但并不能中断线程。

等待唤醒机制与synchronized

所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。

synchronized (obj) {
       obj.wait();
       obj.notify();
       obj.notifyAll();         
 }

需要特别理解的一点是,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。

ok~,篇幅已比较长了,关于synchronized,我们就暂且聊到这。如有错误,欢迎指正,谢谢~~。

本篇的主要参考资料:
《Java编程思想》
《深入理解Java虚拟机》
《实战Java高并发程序设计》

Java并发编程-无锁CAS与Unsafe类及其并发包Atomic

无锁的概念

在谈论无锁概念时,总会关联起乐观派与悲观派,对于乐观派而言,他们认为事情总会往好的方向发展,总是认为坏的情况发生的概率特别小,可以无所顾忌地做事,但对于悲观派而已,他们总会认为发展事态如果不及时控制,以后就无法挽回了,即使无法挽回的局面几乎不可能发生。这两种派系映射到并发编程中就如同加锁与无锁的策略,即加锁是一种悲观策略,无锁是一种乐观策略,因为对于加锁的并发程序来说,它们总是认为每次访问共享资源时总会发生冲突,因此必须对每一次数据操作实施加锁策略。而无锁则总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,这项CAS技术就是无锁策略实现的关键,下面我们进一步了解CAS技术的奇妙之处。

无锁的执行者-CAS

CAS

CAS的全称是Compare And Swap 即比较交换,其算法核心思想如下

执行函数:CAS(V,E,N)

其包含3个参数

  • V表示要更新的变量

  • E表示预期值

  • N表示新值

如果V值等于E值,则将V的值设为N。若V值和E值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。通俗的理解就是CAS操作需要我们提供一个期望值,当期望值与当前线程的变量值相同时,说明还没线程修改该值,当前线程可以进行修改,也就是执行CAS操作,但如果期望值与当前线程不符,则说明该值已被其他线程修改,此时不执行更新操作,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作,原理图如下

这里写图片描述

由于CAS操作属于乐观派,它总认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作,这点从图中也可以看出来。基于这样的原理,CAS操作即使没有锁,同样知道其他线程对共享资源操作影响,并执行相应的处理措施。同时从这点也可以看出,由于无锁操作中没有锁的存在,因此不可能出现死锁的情况,也就是说无锁操作天生免疫死锁。

CPU指令对CAS的支持

或许我们可能会有这样的疑问,假设存在多个线程执行CAS操作并且CAS的步骤很多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?答案是否定的,因为CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

鲜为人知的指针: Unsafe类

Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,单从名称看来就可以知道该类是非安全的,毕竟Unsafe拥有着类似于C的指针操作,因此总是不应该首先使用Unsafe类,Java官方也不建议直接使用的Unsafe类,据说Oracle正在计划从Java 9中去掉Unsafe类,但我们还是很有必要了解该类,因为Java中CAS操作的执行依赖于Unsafe类的方法,注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,关于Unsafe类的主要功能点如下:

  • 内存管理,Unsafe类中存在直接操作内存的方法

    //分配内存指定大小的内存
    public native long allocateMemory(long bytes);
    //根据给定的内存地址address设置重新分配指定大小的内存
    public native long reallocateMemory(long address, long bytes);
    //用于释放allocateMemory和reallocateMemory申请的内存
    public native void freeMemory(long address);
    //将指定对象的给定offset偏移量内存块中的所有字节设置为固定值
    public native void setMemory(Object o, long offset, long bytes, byte value);
    //设置给定内存地址的值
    public native void putAddress(long address, long x);
    //获取指定内存地址的值
    public native long getAddress(long address);
    
    //设置给定内存地址的long值
    public native void putLong(long address, long x);
    //获取指定内存地址的long值
    public native long getLong(long address);
    //设置或获取指定内存的byte值
    public native byte  getByte(long address);
    public native void  putByte(long address, byte x);
    //其他基本数据类型(long,char,float,double,short等)的操作与putByte及getByte相同
    
    //操作系统的内存页大小
    public native int pageSize();
  • 提供实例对象新途径。

    //传入一个对象的class并创建该实例对象,但不会调用构造方法
    public native Object allocateInstance(Class cls) throws InstantiationException;
  • 类和实例对象以及变量的操作,主要方法如下

    //获取字段f在实例对象中的偏移量
    public native long objectFieldOffset(Field f);
    //静态属性的偏移量,用于在对应的Class对象中读写静态属性
    public native long staticFieldOffset(Field f);
    //返回值就是f.getDeclaringClass()
    public native Object staticFieldBase(Field f);
    
    
    //获得给定对象偏移量上的int值,所谓的偏移量可以简单理解为指针指向该变量的内存地址,
    //通过偏移量便可得到该对象的变量,进行各种操作
    public native int getInt(Object o, long offset);
    //设置给定对象上偏移量的int值
    public native void putInt(Object o, long offset, int x);
    
    //获得给定对象偏移量上的引用类型的值
    public native Object getObject(Object o, long offset);
    //设置给定对象偏移量上的引用类型的值
    public native void putObject(Object o, long offset, Object x);
    //其他基本数据类型(long,char,byte,float,double)的操作与getInthe及putInt相同
    
    //设置给定对象的int值,使用volatile语义,即设置后立马更新到内存对其他线程可见
    public native void  putIntVolatile(Object o, long offset, int x);
    //获得给定对象的指定偏移量offset的int值,使用volatile语义,总能获取到最新的int值。
    public native int getIntVolatile(Object o, long offset);
    
    //其他基本数据类型(long,char,byte,float,double)的操作与putIntVolatile及getIntVolatile相同,引用类型putObjectVolatile也一样。
    
    //与putIntVolatile一样,但要求被操作字段必须有volatile修饰
    public native void putOrderedInt(Object o,long offset,int x);

    下面通过一个简单的Demo来演示上述的一些方法以便加深对Unsafe类的理解

    public class UnSafeDemo {
    
        public  static  void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
            // 通过反射得到theUnsafe对应的Field对象
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            // 设置该Field为可访问
            field.setAccessible(true);
            // 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
            Unsafe unsafe = (Unsafe) field.get(null);
            System.out.println(unsafe);
    
            //通过allocateInstance直接创建对象
            User user = (User) unsafe.allocateInstance(User.class);
    
            Class userClass = user.getClass();
            Field name = userClass.getDeclaredField("name");
            Field age = userClass.getDeclaredField("age");
            Field id = userClass.getDeclaredField("id");
    
            //获取实例变量name和age在对象内存中的偏移量并设置值
            unsafe.putInt(user,unsafe.objectFieldOffset(age),18);
            unsafe.putObject(user,unsafe.objectFieldOffset(name),"android TV");
    
            // 这里返回 User.class,
            Object staticBase = unsafe.staticFieldBase(id);
            System.out.println("staticBase:"+staticBase);
    
            //获取静态变量id的偏移量staticOffset
            long staticOffset = unsafe.staticFieldOffset(userClass.getDeclaredField("id"));
            //获取静态变量的值
            System.out.println("设置前的ID:"+unsafe.getObject(staticBase,staticOffset));
            //设置值
            unsafe.putObject(staticBase,staticOffset,"SSSSSSSS");
            //获取静态变量的值
            System.out.println("设置前的ID:"+unsafe.getObject(staticBase,staticOffset));
            //输出USER
            System.out.println("输出USER:"+user.toString());
    
            long data = 1000;
            byte size = 1;//单位字节
    
            //调用allocateMemory分配内存,并获取内存地址memoryAddress
            long memoryAddress = unsafe.allocateMemory(size);
            //直接往内存写入数据
            unsafe.putAddress(memoryAddress, data);
            //获取指定内存地址的数据
            long addrData=unsafe.getAddress(memoryAddress);
            System.out.println("addrData:"+addrData);
    
            /**
             * 输出结果:
             sun.misc.Unsafe@6f94fa3e
             staticBase:class geym.conc.ch4.atomic.User
             设置前的ID:USER_ID
             设置前的ID:SSSSSSSS
             输出USER:User{name='android TV', age=18', id=SSSSSSSS'}
             addrData:1000
             */
    
        }
    }
    
    class User{
        public User(){
            System.out.println("user 构造方法被调用");
        }
        private String name;
        private int age;
        private static String id="USER_ID";
    
        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +'\'' +
                    ", id=" + id +'\'' +
                    '}';
        }
    }

    虽然在Unsafe类中存在getUnsafe()方法,但该方法只提供给高级的Bootstrap类加载器使用,普通用户调用将抛出异常,所以我们在Demo中使用了反射技术获取了Unsafe实例对象并进行相关操作。

    public static Unsafe getUnsafe() {
          Class cc = sun.reflect.Reflection.getCallerClass(2);
          if (cc.getClassLoader() != null)
              throw new SecurityException("Unsafe");
          return theUnsafe;
      }
  • 数组操作

    //获取数组第一个元素的偏移地址
    public native int arrayBaseOffset(Class arrayClass);
    //数组中一个元素占据的内存空间,arrayBaseOffset与arrayIndexScale配合使用,可定位数组中每个元素在内存中的位置
    public native int arrayIndexScale(Class arrayClass);
    
  • CAS 操作相关
    CAS是一些CPU直接支持的指令,也就是我们前面分析的无锁操作,在Java中无锁操作CAS基于以下3个方法实现,在稍后讲解Atomic系列内部方法是基于下述方法的实现的。

    //第一个参数o为给定对象,offset为对象内存的偏移量,通过这个偏移量迅速定位字段并设置或获取该字段的值,
    //expected表示期望值,x表示要设置的值,下面3个方法都通过CAS原子指令执行操作。
    public final native boolean compareAndSwapObject(Object o, long offset,Object expected, Object x);                                                                                                  
    
    public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
    
    public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

    这里还需介绍Unsafe类中JDK 1.8新增的几个方法,它们的实现是基于上述的CAS方法,如下

    
     //1.8新增,给定对象o,根据获取内存偏移量指向的字段,将其增加delta,
     //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
     public final int getAndAddInt(Object o, long offset, int delta) {
         int v;
         do {
             //获取内存中最新值
             v = getIntVolatile(o, offset);
           //通过CAS操作
         } while (!compareAndSwapInt(o, offset, v, v + delta));
         return v;
     }
    
    //1.8新增,方法作用同上,只不过这里操作的long类型数据
     public final long getAndAddLong(Object o, long offset, long delta) {
         long v;
         do {
             v = getLongVolatile(o, offset);
         } while (!compareAndSwapLong(o, offset, v, v + delta));
         return v;
     }
    
     //1.8新增,给定对象o,根据获取内存偏移量对于字段,将其 设置为新值newValue,
     //这是一个CAS操作过程,直到设置成功方能退出循环,返回旧值
     public final int getAndSetInt(Object o, long offset, int newValue) {
         int v;
         do {
             v = getIntVolatile(o, offset);
         } while (!compareAndSwapInt(o, offset, v, newValue));
         return v;
     }
    
    // 1.8新增,同上,操作的是long类型
     public final long getAndSetLong(Object o, long offset, long newValue) {
         long v;
         do {
             v = getLongVolatile(o, offset);
         } while (!compareAndSwapLong(o, offset, v, newValue));
         return v;
     }
    
     //1.8新增,同上,操作的是引用类型数据
     public final Object getAndSetObject(Object o, long offset, Object newValue) {
         Object v;
         do {
             v = getObjectVolatile(o, offset);
         } while (!compareAndSwapObject(o, offset, v, newValue));
         return v;
     }

    上述的方法我们在稍后的Atomic系列分析中还会见到它们的身影。

  • 挂起与恢复
    将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。Java对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,其底层实现最终还是使用Unsafe.park()方法和Unsafe.unpark()方法

    //线程调用该方法,线程将一直阻塞直到超时,或者是中断条件出现。  
    public native void park(boolean isAbsolute, long time);  
    
    //终止挂起的线程,恢复正常.java.util.concurrent包中挂起操作都是在LockSupport类实现的,其底层正是使用这两个方法,  
    public native void unpark(Object thread); 
  • 内存屏障

    这里主要包括了loadFence、storeFence、fullFence等方法,这些方法是在Java 8新引入的,用于定义内存屏障,避免代码重排序,与Java内存模型相关,感兴趣的可以看博主的另一篇博文全面理解Java内存模型(JMM)及volatile关键字,这里就不展开了

    //在该方法之前的所有读操作,一定在load屏障之前执行完成
    public native void loadFence();
    //在该方法之前的所有写操作,一定在store屏障之前执行完成
    public native void storeFence();
    //在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个的合体功能
    public native void fullFence();
  • 其他操作

    //获取持有锁,已不建议使用
    @Deprecated
    public native void monitorEnter(Object var1);
    //释放锁,已不建议使用
    @Deprecated
    public native void monitorExit(Object var1);
    //尝试获取锁,已不建议使用
    @Deprecated
    public native boolean tryMonitorEnter(Object var1);
    
    //获取本机内存的页数,这个值永远都是2的幂次方  
    public native int pageSize();  
    
    //告诉虚拟机定义了一个没有安全检查的类,默认情况下这个类加载器和保护域来着调用者类  
    public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);  
    
    //加载一个匿名类
    public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);
    //判断是否需要加载一个类
    public native boolean shouldBeInitialized(Class<?> c);
    //确保类一定被加载 
    public native  void ensureClassInitialized(Class<?> c)

并发包中的原子操作类(Atomic系列)

通过前面的分析我们已基本理解了无锁CAS的原理并对Java中的指针类Unsafe类有了比较全面的认识,下面进一步分析CAS在Java中的应用,即并发包中的原子操作类(Atomic系列),从JDK 1.5开始提供了java.util.concurrent.atomic包,在该包中提供了许多基于CAS实现的原子操作类,用法方便,性能高效,主要分以下4种类型。

原子更新基本类型

原子更新基本类型主要包括3个类:

  • AtomicBoolean:原子更新布尔类型
  • AtomicInteger:原子更新整型
  • AtomicLong:原子更新长整型

这3个类的实现原理和使用方式几乎是一样的,这里我们以AtomicInteger为例进行分析,AtomicInteger主要是针对int类型的数据执行原子操作,它提供了原子自增方法、原子自减方法以及原子赋值方法等,鉴于AtomicInteger的源码不多,我们直接看源码

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 获取指针类Unsafe
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    //下述变量value在AtomicInteger实例对象内的内存偏移量
    private static final long valueOffset;

    static {
        try {
           //通过unsafe类的objectFieldOffset()方法,获取value变量在对象内存中的偏移
           //通过该偏移量valueOffset,unsafe类的内部方法可以获取到变量value对其进行取值或赋值操作
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
   //当前AtomicInteger封装的int变量value
    private volatile int value;

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    public AtomicInteger() {
    }
   //获取当前最新值,
    public final int get() {
        return value;
    }
    //设置当前值,具备volatile效果,方法用final修饰是为了更进一步的保证线程安全。
    public final void set(int newValue) {
        value = newValue;
    }
    //最终会设置成newValue,使用该方法后可能导致其他线程在之后的一小段时间内可以获取到旧值,有点类似于延迟加载
    public final void lazySet(int newValue) {
        unsafe.putOrderedInt(this, valueOffset, newValue);
    }
   //设置新值并获取旧值,底层调用的是CAS操作即unsafe.compareAndSwapInt()方法
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
   //如果当前值为expect,则设置为update(当前值指的是value变量)
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    //当前值加1返回旧值,底层CAS操作
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    //当前值减1,返回旧值,底层CAS操作
    public final int getAndDecrement() {
        return unsafe.getAndAddInt(this, valueOffset, -1);
    }
   //当前值增加delta,返回旧值,底层CAS操作
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    //当前值加1,返回新值,底层CAS操作
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    //当前值减1,返回新值,底层CAS操作
    public final int decrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
    }
   //当前值增加delta,返回新值,底层CAS操作
    public final int addAndGet(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
    }
   //省略一些不常用的方法....
}

通过上述的分析,可以发现AtomicInteger原子类的内部几乎是基于前面分析过Unsafe类中的CAS相关操作的方法实现的,这也同时证明AtomicInteger是基于无锁实现的,这里重点分析自增操作实现过程,其他方法自增实现原理一样。

//当前值加1,返回新值,底层CAS操作
public final int incrementAndGet() {
     return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
 }

我们发现AtomicInteger类中所有自增或自减的方法都间接调用Unsafe类中的getAndAddInt()方法实现了CAS操作,从而保证了线程安全,关于getAndAddInt其实前面已分析过,它是Unsafe类中1.8新增的方法,源码如下

//Unsafe类中的getAndAddInt方法
public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }

可看出getAndAddInt通过一个while循环不断的重试更新要设置的值,直到成功为止,调用的是Unsafe类中的compareAndSwapInt方法,是一个CAS操作方法。这里需要注意的是,上述源码分析是基于JDK1.8的,如果是1.8之前的方法,AtomicInteger源码实现有所不同,是基于for死循环的,如下

//JDK 1.7的源码,由for的死循环实现,并且直接在AtomicInteger实现该方法,
//JDK1.8后,该方法实现已移动到Unsafe类中,直接调用getAndAddInt方法即可
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

ok~,下面简单看个Demo,感受一下AtomicInteger使用方式

public class AtomicIntegerDemo {
    //创建AtomicInteger,用于自增操作
    static AtomicInteger i=new AtomicInteger();

    public static class AddThread implements Runnable{
        public void run(){
           for(int k=0;k<10000;k++)
               i.incrementAndGet();
        }

    }
    public static void main(String[] args) throws InterruptedException {
        Thread[] ts=new Thread[10];
        //开启10条线程同时执行i的自增操作
        for(int k=0;k<10;k++){
            ts[k]=new Thread(new AddThread());
        }
        //启动线程
        for(int k=0;k<10;k++){ts[k].start();}

        for(int k=0;k<10;k++){ts[k].join();}

        System.out.println(i);//输出结果:100000
    }
}

在Demo中,使用原子类型AtomicInteger替换普通int类型执行自增的原子操作,保证了线程安全。至于AtomicBoolean和AtomicLong的使用方式以及实现原理是一样,大家可以自行查阅源码。

原子更新引用

原子更新引用类型可以同时更新引用类型,这里主要分析一下AtomicReference原子类,即原子更新引用类型。先看看其使用方式,如下

public class AtomicReferenceDemo2 {

    public static AtomicReference<User> atomicUserRef = new AtomicReference<User>();

    public static void main(String[] args) {
        User user = new User("zejian", 18);
        atomicUserRef.set(user);
        User updateUser = new User("Shine", 25);
        atomicUserRef.compareAndSet(user, updateUser);
        //执行结果:User{name='Shine', age=25}
              System.out.println(atomicUserRef.get().toString());  
    }

    static class User {
        public String name;
        private int age;

        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        @Override
        public String toString() {
            return "User{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

那么AtomicReference原子类内部是如何实现CAS操作的呢?

public class AtomicReference<V> implements java.io.Serializable {
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicReference.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    //内部变量value,Unsafe类通过valueOffset内存偏移量即可获取该变量
    private volatile V value;

//CAS方法,间接调用unsafe.compareAndSwapObject(),它是一个
//实现了CAS操作的native方法
public final boolean compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

//设置并获取旧值
public final V getAndSet(V newValue) {
        return (V)unsafe.getAndSetObject(this, valueOffset, newValue);
    }
    //省略其他代码......
}

//Unsafe类中的getAndSetObject方法,实际调用还是CAS操作
public final Object getAndSetObject(Object o, long offset, Object newValue) {
      Object v;
      do {
          v = getObjectVolatile(o, offset);
      } while (!compareAndSwapObject(o, offset, v, newValue));
      return v;
  }

从源码看来,AtomicReference与AtomicInteger的实现原理基本是一样的,最终执行的还是Unsafe类,关于AtomicReference的其他方法也是一样的,如下

红框内的方法是Java8新增的,可以基于Lambda表达式对传递进来的期望值或要更新的值进行其他操作后再进行CAS操作,说白了就是对期望值或要更新的值进行额外修改后再执行CAS更新,在所有的Atomic原子类中几乎都存在这几个方法。

原子更新数组

原子更新数组指的是通过原子的方式更新数组里的某个元素,主要有以下3个类

  • AtomicIntegerArray:原子更新整数数组里的元素
  • AtomicLongArray:原子更新长整数数组里的元素
  • AtomicReferenceArray:原子更新引用类型数组里的元素

这里以AtomicIntegerArray为例进行分析,其余两个使用方式和实现原理基本一样,简单案例如下,

public class AtomicIntegerArrayDemo {
    static AtomicIntegerArray arr = new AtomicIntegerArray(10);

    public static class AddThread implements Runnable{
        public void run(){
           for(int k=0;k<10000;k++)
               //执行数组中元素自增操作,参数为index,即数组下标
               arr.getAndIncrement(k%arr.length());
        }
    }
    public static void main(String[] args) throws InterruptedException {

        Thread[] ts=new Thread[10];
        //创建10条线程
        for(int k=0;k<10;k++){
            ts[k]=new Thread(new AddThread());
        }
        //启动10条线程
        for(int k=0;k<10;k++){ts[k].start();}
        for(int k=0;k<10;k++){ts[k].join();}
        //执行结果
        //[10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000, 10000]
        System.out.println(arr);
    }
}

启动10条线程对数组中的元素进行自增操作,执行结果符合预期。使用方式比较简单,接着看看AtomicIntegerArray内部是如何实现,先看看部分源码

public class AtomicIntegerArray implements java.io.Serializable {
    //获取unsafe类的实例对象
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //获取数组的第一个元素内存起始地址
    private static final int base = unsafe.arrayBaseOffset(int[].class);

    private static final int shift;
    //内部数组
    private final int[] array;

    static {
        //获取数组中一个元素占据的内存空间
        int scale = unsafe.arrayIndexScale(int[].class);
        //判断是否为2的次幂,一般为2的次幂否则抛异常
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        //
        shift = 31 - Integer.numberOfLeadingZeros(scale);
    }

    private long checkedByteOffset(int i) {
        if (i < 0 || i >= array.length)
            throw new IndexOutOfBoundsException("index " + i);

        return byteOffset(i);
    }
    //计算数组中每个元素的的内存地址
    private static long byteOffset(int i) {
        return ((long) i << shift) + base;
    }
    //省略其他代码......
}

通过前面对Unsafe类的分析,我们知道arrayBaseOffset方法可以获取数组的第一个元素起始地址,而arrayIndexScale方法可以获取每个数组元素占用的内存空间,由于这里是Int类型,而Java中一个int类型占用4个字节,也就是scale的值为4,那么如何根据数组下标值计算每个元素的内存地址呢?显然应该是

每个数组元素的内存地址=起始地址+元素下标 * 每个元素所占用的内存空间

与该方法原理相同

//计算数组中每个元素的的内存地址
private static long byteOffset(int i) {
     return ((long) i << shift) + base;
 }

这是为什么,首先来计算出shift的值

 shift = 31 - Integer.numberOfLeadingZeros(scale);

其中Integer.numberOfLeadingZeros(scale)是计算出scale的前导零个数(必须是连续的),scale=4,转成二进制为
00000000 00000000 00000000 00000100
即前导零数为29,也就是shift=2,然后利用shift来定位数组中的内存位置,在数组不越界时,计算出前3个数组元素内存地址

//第一个数组元素,index=0 , 其中base为起始地址,4代表int类型占用的字节数 
address = base + 0 * 4 即address= base + 0 << 2
//第二个数组元素,index=1
address = base + 1 * 4 即address= base + 1 << 2
//第三个数组元素,index=2
address = base + 2 * 4 即address= base + 2 << 2
//........

显然shift=2,替换去就是

address= base + i << shift

这就是 byteOffset(int i) 方法的计算原理。因此byteOffset(int)方法可以根据数组下标计算出每个元素的内存地址。至于其他方法就比较简单了,都是间接调用Unsafe类的CAS原子操作方法,如下简单看其中几个常用方法

//执行自增操作,返回旧值,i是指数组元素下标
public final int getAndIncrement(int i) {
      return getAndAdd(i, 1);
}
//指定下标元素执行自增操作,并返回新值
public final int incrementAndGet(int i) {
    return getAndAdd(i, 1) + 1;
}

//指定下标元素执行自减操作,并返回新值
public final int decrementAndGet(int i) {
    return getAndAdd(i, -1) - 1;
}
//间接调用unsafe.getAndAddInt()方法
public final int getAndAdd(int i, int delta) {
    return unsafe.getAndAddInt(array, checkedByteOffset(i), delta);
}

//Unsafe类中的getAndAddInt方法,执行CAS操作
public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = getIntVolatile(o, offset);
        } while (!compareAndSwapInt(o, offset, v, v + delta));
        return v;
    }

至于AtomicLongArray和AtomicReferenceArray原子类,使用方式和实现原理基本一样。

原子更新属性

如果我们只需要某个类里的某个字段,也就是说让普通的变量也享受原子操作,可以使用原子更新字段类,如在某些时候由于项目前期考虑不周全,项目需求又发生变化,使得某个类中的变量需要执行多线程操作,由于该变量多处使用,改动起来比较麻烦,而且原来使用的地方无需使用线程安全,只要求新场景需要使用时,可以借助原子更新器处理这种场景,Atomic并发包提供了以下三个类:

  • AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater:原子更新长整型字段的更新器。
  • AtomicReferenceFieldUpdater:原子更新引用类型里的字段。

请注意原子更新器的使用存在比较苛刻的条件如下

  • 操作的字段不能是static类型。

  • 操作的字段不能是final类型的,因为final根本没法修改。

  • 字段必须是volatile修饰的,也就是数据本身是读一致的。

  • 属性必须对当前的Updater所在的区域是可见的,如果不是当前类内部进行原子更新器操作不能使用private,protected子类操作父类时修饰符必须是protect权限及以上,如果在同一个package下则必须是default权限及以上,也就是说无论何时都应该保证操作类与被操作类间的可见性。

下面看看AtomicIntegerFieldUpdater和AtomicReferenceFieldUpdater的简单使用方式

public class AtomicIntegerFieldUpdaterDemo {
    public static class Candidate{
        int id;
        volatile int score;
    }

    public static class Game{
        int id;
        volatile String name;

        public Game(int id, String name) {
            this.id = id;
            this.name = name;
        }

        @Override
        public String toString() {
            return "Game{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    '}';
        }
    }

    static AtomicIntegerFieldUpdater<Candidate> atIntegerUpdater
        = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");

    static AtomicReferenceFieldUpdater<Game,String> atRefUpdate =
            AtomicReferenceFieldUpdater.newUpdater(Game.class,String.class,"name");


    //用于验证分数是否正确
    public static AtomicInteger allScore=new AtomicInteger(0);


    public static void main(String[] args) throws InterruptedException {
        final Candidate stu=new Candidate();
        Thread[] t=new Thread[10000];
        //开启10000个线程
        for(int i = 0 ; i < 10000 ; i++) {
            t[i]=new Thread() {
                public void run() {
                    if(Math.random()>0.4){
                        atIntegerUpdater.incrementAndGet(stu);
                        allScore.incrementAndGet();
                    }
                }
            };
            t[i].start();
        }

        for(int i = 0 ; i < 10000 ; i++) {  t[i].join();}
        System.out.println("最终分数score="+stu.score);
        System.out.println("校验分数allScore="+allScore);

        //AtomicReferenceFieldUpdater 简单的使用
        Game game = new Game(2,"zh");
        atRefUpdate.compareAndSet(game,game.name,"JAVA-HHH");
        System.out.println(game.toString());

        /**
         * 输出结果:
         * 最终分数score=5976
           校验分数allScore=5976
           Game{id=2, name='JAVA-HHH'}
         */
    }
}

我们使用AtomicIntegerFieldUpdater更新候选人(Candidate)的分数score,开启了10000条线程投票,当随机值大于0.4时算一票,分数自增一次,其中allScore用于验证分数是否正确(其实用于验证AtomicIntegerFieldUpdater更新的字段是否线程安全),当allScore与score相同时,则说明投票结果无误,也代表AtomicIntegerFieldUpdater能正确更新字段score的值,是线程安全的。对于AtomicReferenceFieldUpdater,我们在代码中简单演示了其使用方式,注意在AtomicReferenceFieldUpdater注明泛型时需要两个泛型参数,一个是修改的类类型,一个修改字段的类型。至于AtomicLongFieldUpdater则与AtomicIntegerFieldUpdater类似,不再介绍。接着简单了解一下AtomicIntegerFieldUpdater的实现原理,实际就是反射和Unsafe类结合,AtomicIntegerFieldUpdater是个抽象类,实际实现类为AtomicIntegerFieldUpdaterImpl

public abstract class AtomicIntegerFieldUpdater<T> {

    public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass,
                                                              String fieldName) {
         //实际实现类AtomicIntegerFieldUpdaterImpl                                          
        return new AtomicIntegerFieldUpdaterImpl<U>
            (tclass, fieldName, Reflection.getCallerClass());
    }
 }

看看AtomicIntegerFieldUpdaterImpl

 private static class AtomicIntegerFieldUpdaterImpl<T>
            extends AtomicIntegerFieldUpdater<T> {
        private static final Unsafe unsafe = Unsafe.getUnsafe();
        private final long offset;//内存偏移量
        private final Class<T> tclass;
        private final Class<?> cclass;

        AtomicIntegerFieldUpdaterImpl(final Class<T> tclass,
                                      final String fieldName,
                                      final Class<?> caller) {
            final Field field;//要修改的字段
            final int modifiers;//字段修饰符
            try {
                field = AccessController.doPrivileged(
                    new PrivilegedExceptionAction<Field>() {
                        public Field run() throws NoSuchFieldException {
                            return tclass.getDeclaredField(fieldName);//反射获取字段对象
                        }
                    });
                    //获取字段修饰符
                modifiers = field.getModifiers();
            //对字段的访问权限进行检查,不在访问范围内抛异常
                sun.reflect.misc.ReflectUtil.ensureMemberAccess(
                    caller, tclass, null, modifiers);
                ClassLoader cl = tclass.getClassLoader();
                ClassLoader ccl = caller.getClassLoader();
                if ((ccl != null) && (ccl != cl) &&
                    ((cl == null) || !isAncestor(cl, ccl))) {
              sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass);
                }
            } catch (PrivilegedActionException pae) {
                throw new RuntimeException(pae.getException());
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }

            Class<?> fieldt = field.getType();
            //判断是否为int类型
            if (fieldt != int.class)
                throw new IllegalArgumentException("Must be integer type");
            //判断是否被volatile修饰
            if (!Modifier.isVolatile(modifiers))
                throw new IllegalArgumentException("Must be volatile type");

            this.cclass = (Modifier.isProtected(modifiers) &&
                           caller != tclass) ? caller : null;
            this.tclass = tclass;
            //获取该字段的在对象内存的偏移量,通过内存偏移量可以获取或者修改该字段的值
            offset = unsafe.objectFieldOffset(field);
        }
        }

从AtomicIntegerFieldUpdaterImpl的构造器也可以看出更新器为什么会有这么多限制条件了,当然最终其CAS操作肯定是通过unsafe完成的,简单看一个方法

public int incrementAndGet(T obj) {
        int prev, next;
        do {
            prev = get(obj);
            next = prev + 1;
            //CAS操作
        } while (!compareAndSet(obj, prev, next));
        return next;
}

//最终调用的还是unsafe.compareAndSwapInt()方法
public boolean compareAndSet(T obj, int expect, int update) {
            if (obj == null || obj.getClass() != tclass || cclass != null) fullCheck(obj);
            return unsafe.compareAndSwapInt(obj, offset, expect, update);
        }

CAS的ABA问题及其解决方案

假设这样一种场景,当第一个线程执行CAS(V,E,U)操作,在获取到当前变量V,准备修改为新值U前,另外两个线程已连续修改了两次变量V的值,使得该值又恢复为旧值,这样的话,我们就无法正确判断这个变量是否已被修改过,如下图

这就是典型的CAS的ABA问题,一般情况这种情况发现的概率比较小,可能发生了也不会造成什么问题,比如说我们对某个做加减法,不关心数字的过程,那么发生ABA问题也没啥关系。但是在某些情况下还是需要防止的,那么该如何解决呢?在Java中解决ABA问题,我们可以使用以下两个原子类

  • AtomicStampedReference

    AtomicStampedReference原子类是一个带有时间戳的对象引用,在每次修改后,AtomicStampedReference不仅会设置新值而且还会记录更改的时间。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值才能写入成功,这也就解决了反复读写时,无法预知值是否已被修改的窘境,测试demo如下

    /**
     * Created by zejian on 2017/7/2.
     * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
     */
    public class ABADemo {
    
        static AtomicInteger atIn = new AtomicInteger(100);
    
        //初始化时需要传入一个初始值和初始时间
        static AtomicStampedReference<Integer> atomicStampedR =
                new AtomicStampedReference<Integer>(200,0);
    
    
        static Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //更新为200
                atIn.compareAndSet(100, 200);
                //更新为100
                atIn.compareAndSet(200, 100);
            }
        });
    
    
        static Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean flag=atIn.compareAndSet(100,500);
                System.out.println("flag:"+flag+",newValue:"+atIn);
            }
        });
    
    
        static Thread t3 = new Thread(new Runnable() {
            @Override
            public void run() {
                int time=atomicStampedR.getStamp();
                //更新为200
                atomicStampedR.compareAndSet(100, 200,time,time+1);
                //更新为100
                int time2=atomicStampedR.getStamp();
                atomicStampedR.compareAndSet(200, 100,time2,time2+1);
            }
        });
    
    
        static Thread t4 = new Thread(new Runnable() {
            @Override
            public void run() {
                int time = atomicStampedR.getStamp();
                System.out.println("sleep 前 t4 time:"+time);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean flag=atomicStampedR.compareAndSet(100,500,time,time+1);
                System.out.println("flag:"+flag+",newValue:"+atomicStampedR.getReference());
            }
        });
    
        public static  void  main(String[] args) throws InterruptedException {
            t1.start();
            t2.start();
            t1.join();
            t2.join();
    
            t3.start();
            t4.start();
            /**
             * 输出结果:
             flag:true,newValue:500
             sleep 前 t4 time:0
             flag:false,newValue:200
             */
        }
    }

    对比输出结果可知,AtomicStampedReference类确实解决了ABA的问题,下面我们简单看看其内部实现原理

    public class AtomicStampedReference<V> {
        //通过Pair内部类存储数据和时间戳
        private static class Pair<T> {
            final T reference;
            final int stamp;
            private Pair(T reference, int stamp) {
                this.reference = reference;
                this.stamp = stamp;
            }
            static <T> Pair<T> of(T reference, int stamp) {
                return new Pair<T>(reference, stamp);
            }
        }
        //存储数值和时间的内部类
        private volatile Pair<V> pair;
    
        //构造器,创建时需传入初始值和时间初始值
        public AtomicStampedReference(V initialRef, int initialStamp) {
            pair = Pair.of(initialRef, initialStamp);
        }
    }

    接着看看其compareAndSet方法的实现:

    public boolean compareAndSet(V   expectedReference,
                                     V   newReference,
                                     int expectedStamp,
                                     int newStamp) {
            Pair<V> current = pair;
            return
                expectedReference == current.reference &&
                expectedStamp == current.stamp &&
                ((newReference == current.reference &&
                  newStamp == current.stamp) ||
                 casPair(current, Pair.of(newReference, newStamp)));
        }

    同时对当前数据和当前时间进行比较,只有两者都相等是才会执行casPair()方法,单从该方法的名称就可知是一个CAS方法,最终调用的还是Unsafe类中的compareAndSwapObject方法

    private boolean casPair(Pair<V> cmp, Pair<V> val) {
            return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
        }

    到这我们就很清晰AtomicStampedReference的内部实现思想了,通过一个键值对Pair存储数据和时间戳,在更新时对数据和时间戳进行比较,只有两者都符合预期才会调用Unsafe的compareAndSwapObject方法执行数值和时间戳替换,也就避免了ABA的问题。

  • AtomicMarkableReference类

    AtomicMarkableReference与AtomicStampedReference不同的是,AtomicMarkableReference维护的是一个boolean值的标识,也就是说至于true和false两种切换状态,经过博主测试,这种方式并不能完全防止ABA问题的发生,只能减少ABA问题发生的概率。

    public class ABADemo {
        static AtomicMarkableReference<Integer> atMarkRef =
                  new AtomicMarkableReference<Integer>(100,false);
    
     static Thread t5 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean mark=atMarkRef.isMarked();
                System.out.println("mark:"+mark);
                //更新为200
                System.out.println("t5 result:"+atMarkRef.compareAndSet(atMarkRef.getReference(), 200,mark,!mark));
            }
        });
    
        static Thread t6 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean mark2=atMarkRef.isMarked();
                System.out.println("mark2:"+mark2);
                System.out.println("t6 result:"+atMarkRef.compareAndSet(atMarkRef.getReference(), 100,mark2,!mark2));
            }
        });
    
        static Thread t7 = new Thread(new Runnable() {
            @Override
            public void run() {
                boolean mark=atMarkRef.isMarked();
                System.out.println("sleep 前 t7 mark:"+mark);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean flag=atMarkRef.compareAndSet(100,500,mark,!mark);
                System.out.println("flag:"+flag+",newValue:"+atMarkRef.getReference());
            }
        });
    
        public static  void  main(String[] args) throws InterruptedException {        
            t5.start();t5.join();
            t6.start();t6.join();
            t7.start();
    
            /**
             * 输出结果:
             mark:false
             t5 result:true
             mark2:true
             t6 result:true
             sleep 前 t5 mark:false
             flag:true,newValue:500 ---->成功了.....说明还是发生ABA问题
             */
        }
    }

    AtomicMarkableReference的实现原理与AtomicStampedReference类似,这里不再介绍。到此,我们也明白了如果要完全杜绝ABA问题的发生,我们应该使用AtomicStampedReference原子类更新对象,而对于AtomicMarkableReference来说只能减少ABA问题的发生概率,并不能杜绝。

再谈自旋锁

自旋锁是一种假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这种方式确实也是可以提升效率的。但问题是当线程越来越多竞争很激烈时,占用CPU的时间变长会导致性能急剧下降,因此Java虚拟机内部一般对于自旋锁有一定的次数限制,可能是50或者100次循环后就放弃,直接挂起线程,让出CPU资源。如下通过AtomicReference可实现简单的自旋锁。

public class SpinLock {
  private AtomicReference<Thread> sign =new AtomicReference<>();

  public void lock(){
    Thread current = Thread.currentThread();
    while(!sign .compareAndSet(null, current)){
    }
  }

  public void unlock (){
    Thread current = Thread.currentThread();
    sign .compareAndSet(current, null);
  }
}

使用CAS原子操作作为底层实现,lock()方法将要更新的值设置为当前线程,并将预期值设置为null。unlock()函数将要更新的值设置为null,并预期值设置为当前线程。然后我们通过lock()和unlock来控制自旋锁的开启与关闭,注意这是一种非公平锁。事实上AtomicInteger(或者AtomicLong)原子类内部的CAS操作也是通过不断的自循环(while循环)实现,不过这种循环的结束条件是线程成功更新对于的值,但也是自旋锁的一种。

ok~,到此关于无锁并发的知识点暂且了解到这,本篇到此告一段落。

主要参考资料
《Java高并发程序设计》
《Java编程思想》

全面理解Java内存模型(JMM)及volatile关键字

理解Java内存区域与Java内存模型

Java内存区域

Java虚拟机在运行程序时会把其自动管理的内存划分为以上几个区域,每个区域都有的用途以及创建销毁的时机,其中蓝色部分代表的是所有线程共享的数据区域,而绿色部分代表的是每个线程的私有数据区域。

  • 方法区(Method Area):

    方法区属于线程共享的内存区域,又称Non-Heap(非堆),主要用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据,根据Java 虚拟机规范的规定,当方法区无法满足内存分配需求时,将抛出OutOfMemoryError 异常。值得注意的是在方法区中存在一个叫运行时常量池(Runtime Constant Pool)的区域,它主要用于存放编译器生成的各种字面量和符号引用,这些内容将在类加载后存放到运行时常量池中,以便后续使用。

  • JVM堆(Java Heap):

    Java 堆也是属于线程共享的内存区域,它在虚拟机启动时创建,是Java 虚拟机所管理的内存中最大的一块,主要用于存放对象实例,几乎所有的对象实例都在这里分配内存,注意Java 堆是垃圾收集器管理的主要区域,因此很多时候也被称做GC 堆,如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出OutOfMemoryError 异常。

  • 程序计数器(Program Counter Register):

    属于线程私有的数据区域,是一小块内存空间,主要代表当前线程所执行的字节码行号指示器。字节码解释器工作时,通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

  • 虚拟机栈(Java Virtual Machine Stacks):

    属于线程私有的数据区域,与线程同时创建,总数与线程关联,代表Java方法执行的内存模型。每个方法执行时都会创建一个栈桢来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。每个方法从调用直结束就对于一个栈桢在虚拟机栈中的入栈和出栈过程,如下(图有误,应该为栈桢):

  • 本地方法栈(Native Method Stacks):

    本地方法栈属于线程私有的数据区域,这部分主要与虚拟机用到的 Native 方法相关,一般情况下,我们无需关心此区域。

这里之所以简要说明这部分内容,注意是为了区别Java内存模型与Java内存区域的划分,毕竟这两种划分是属于不同层次的概念。

Java内存模型概述

Java内存模型(即Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图

需要注意的是,JMM与Java内存区域的划分是不同的概念层次,更恰当说JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性,有序性、可见性展开的(稍后会分析)。JMM与Java内存区域唯一相似点,都存在共享数据区域和私有数据区域,在JMM中主内存属于共享数据区域,从某个程度上讲应该包括了堆和方法区,而工作内存数据线程私有数据区域,从某个程度上讲则应该包括程序计数器、虚拟机栈以及本地方法栈。或许在某些地方,我们可能会看见主内存被描述为堆内存,工作内存被称为线程栈,实际上他们表达的都是同一个含义。关于JMM中的主内存和工作内存说明如下

  • 主内存

    主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。由于是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。

  • 工作内存

    主要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。注意由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。

弄清楚主内存和工作内存后,接了解一下主内存与工作内存的数据存储类型以及操作方式,根据虚拟机规范,对于一个实例对象中的成员方法而言,如果方法中包含本地变量是基本数据类型(boolean,byte,short,char,int,long,float,double),将直接存储在工作内存的帧栈结构中,但倘若本地变量是引用类型,那么该变量的引用会存储在功能内存的帧栈中,而对象实例将存储在主内存(共享数据区域,堆)中。但对于实例对象的成员变量,不管它是基本数据类型或者包装类型(Integer、Double等)还是引用类型,都会被存储到堆区。至于static变量以及类本身相关信息将会存储在主内存中。需要注意的是,在主内存中的实例对象可以被多线程共享,倘若两个线程同时调用了同一个对象的同一个方法,那么两条线程会将要操作的数据拷贝一份到自己的工作内存中,执行完成操作后才刷新到主内存,简单示意图如下所示:

硬件内存架构与Java内存模型

硬件内存架构

正如上图所示,经过简化CPU与内存操作的简易图,实际上没有这么简单,这里为了理解方便,我们省去了南北桥并将三级缓存统一为CPU缓存(有些CPU只有二级缓存,有些CPU有三级缓存)。就目前计算机而言,一般拥有多个CPU并且每个CPU可能存在多个核心,多核是指在一枚处理器(CPU)中集成两个或多个完整的计算引擎(内核),这样就可以支持多任务并行执行,从多线程的调度来说,每个线程都会映射到各个CPU核心中并行运行。在CPU内部有一组CPU寄存器,寄存器是cpu直接访问和处理的数据,是一个临时放数据的空间。一般CPU都会从内存取数据到寄存器,然后进行处理,但由于内存的处理速度远远低于CPU,导致CPU在处理指令时往往花费很多时间在等待内存做准备工作,于是在寄存器和主内存间添加了CPU缓存,CPU缓存比较小,但访问速度比主内存快得多,如果CPU总是操作主内存中的同一址地的数据,很容易影响CPU执行速度,此时CPU缓存就可以把从内存提取的数据暂时保存起来,如果寄存器要取内存中同一位置的数据,直接从缓存中提取,无需直接从主内存取。需要注意的是,寄存器并不每次数据都可以从缓存中取得数据,万一不是同一个内存地址中的数据,那寄存器还必须直接绕过缓存从内存中取数据。所以并不每次都得到缓存中取数据,这种现象有个专业的名称叫做缓存的命中率,从缓存中取就命中,不从缓存中取从内存中取,就没命中,可见缓存命中率的高低也会影响CPU执行性能,这就是CPU、缓存以及主内存间的简要交互过程,总而言之当一个CPU需要访问主存时,会先读取一部分主存数据到CPU缓存(当然如果CPU缓存中存在需要的数据就会直接从缓存获取),进而在读取CPU缓存到寄存器,当CPU需要写数据到主存时,同样会先刷新寄存器中的数据到CPU缓存,然后再把数据刷新到主内存中。

Java线程与硬件处理器

了解完硬件的内存架构后,接着了解JVM中线程的实现原理,理解线程的实现原理,有助于我们了解Java内存模型与硬件内存架构的关系,在Window系统和Linux系统上,Java线程的实现是基于一对一的线程模型,所谓的一对一模型,实际上就是通过语言级别层面程序去间接调用系统内核的线程模型,即我们在使用Java线程时,Java虚拟机内部是转而调用当前操作系统的内核线程来完成当前任务。这里需要了解一个术语,内核线程(Kernel-Level Thread,KLT),它是由操作系统内核(Kernel)支持的线程,这种线程是由操作系统内核来完成线程切换,内核通过操作调度器进而对线程执行调度,并将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这也就是操作系统可以同时处理多任务的原因。由于我们编写的多线程程序属于语言层面的,程序一般不会直接去调用内核线程,取而代之的是一种轻量级的进程(Light Weight Process),也是通常意义上的线程,由于每个轻量级进程都会映射到一个内核线程,因此我们可以通过轻量级进程调用内核线程,进而由操作系统内核将任务映射到各个处理器,这种轻量级进程与内核线程间1对1的关系就称为一对一的线程模型。如下图

如图所示,每个线程最终都会映射到CPU中进行处理,如果CPU存在多核,那么一个CPU将可以并行执行多个线程任务。

Java内存模型与硬件内存架构的关系

通过对前面的硬件内存架构、Java内存模型以及Java多线程的实现原理的了解,我们应该已经意识到,多线程的执行最终都会映射到硬件处理器上进行执行,但Java内存模型和硬件内存架构并不完全一致。对于硬件内存来说只有寄存器、缓存内存、主内存的概念,并没有工作内存(线程私有数据区域)和主内存(堆内存)之分,也就是说Java内存模型对内存的划分对硬件内存并没有任何影响,因为JMM只是一种抽象的概念,是一组规则,并不实际存在,不管是工作内存的数据还是主内存的数据,对于计算机硬件来说都会存储在计算机主内存中,当然也有可能存储到CPU缓存或者寄存器中,因此总体上来说,Java内存模型和计算机硬件内存架构是一个相互交叉的关系,是一种抽象概念划分与真实物理硬件的交叉。(注意对于Java内存区域划分也是同样的道理)

JMM存在的必要性

在明白了Java内存区域划分、硬件内存架构、Java多线程的实现原理与Java内存模型的具体关系后,接着来谈谈Java内存模型存在的必要性。由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,线程与主内存中的变量操作必须通过工作内存间接完成,主要过程是将变量从主内存拷贝的每个线程各自的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,如果存在两个线程同时对一个主内存中的实例对象的变量进行操作就有可能诱发线程安全问题。如下图,主内存中存在一个共享变量x,现在有A和B两条线程分别对该变量x=1进行操作,A/B线程各自的工作内存中存在共享变量副本x。假设现在A线程想要修改x的值为2,而B线程却想要读取x的值,那么B线程读取到的值是A线程更新后的值2还是更新前的值1呢?答案是,不确定,即B线程有可能读取到A线程更新前的值1,也有可能读取到A线程更新后的值2,这是因为工作内存是每个线程私有的数据区域,而线程A变量x时,首先是将变量从主内存拷贝到A线程的工作内存中,然后对变量进行操作,操作完成后再将变量x写回主内,而对于B线程的也是类似的,这样就有可能造成主内存与工作内存间数据存在一致性问题,假如A线程修改完后正在将数据写回主内存,而B线程此时正在读取主内存,即将x=1拷贝到自己的工作内存中,这样B线程读取到的值就是x=1,但如果A线程已将x=2写回主内存后,B线程才开始读取的话,那么此时B线程读取到的就是x=2,但到底是哪种情况先发生呢?这是不确定的,这也就是所谓的线程安全问题。

为了解决类似上述的问题,JVM定义了一组规则,通过这组规则来决定一个线程对共享变量的写入何时对另一个线程可见,这组规则也称为Java内存模型(即JMM),JMM是围绕着程序执行的原子性、有序性、可见性展开的,下面我们看看这三个特性。

Java内存模型的承诺

这里我们先来了解几个概念,即原子性?可见性?有序性?最后再阐明JMM是如何保证这3个特性。

原子性

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。比如对于一个静态变量int x,两条线程同时对他赋值,线程A赋值为1,而线程B赋值为2,不管线程如何运行,最终x的值要么是1,要么是2,线程A和线程B间的操作是没有干扰的,这就是原子性操作,不可被中断的特点。有点要注意的是,对于32位系统的来说,long类型数据和double类型数据(对于基本数据类型,byte,short,int,float,boolean,char读写是原子操作),它们的读写并非原子性的,也就是说如果存在两条线程同时对long类型或者double类型的数据进行读写是存在相互干扰的,因为对于32位虚拟机来说,每次原子读写是32位的,而long和double则是64位的存储单元,这样会导致一个线程在写时,操作完前32位的原子操作后,轮到B线程读取时,恰好只读取到了后32位的数据,这样可能会读取到一个既非原值又不是线程修改值的变量,它可能是“半个变量”的数值,即64位数据被两个线程分成了两次读取。但也不必太担心,因为读取到“半个变量”的情况比较少见,至少在目前的商用的虚拟机中,几乎都把64位的数据的读写操作作为原子操作来执行,因此对于这个问题不必太在意,知道这么回事即可。

理解指令重排

计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种

  • 编译器优化的重排

    编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  • 指令并行的重排

    现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序

  • 内存系统的重排

    由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。

其中编译器优化的重排属于编译期重排,指令并行的重排和内存系统的重排属于处理器重排,在多线程环境中,这些重排优化可能会导致程序出现内存可见性问题,下面分别阐明这两种重排优化可能带来的问题

编译器重排

下面我们简单看一个编译器重排的例子:

线程 1             线程 2
1: x2 = a ;      3: x1 = b ;
2: b = 1;         4: a = 2 ;

两个线程同时执行,分别有1、2、3、4四段执行代码,其中1、2属于线程1 , 3、4属于线程2 ,从程序的执行顺序上看,似乎不太可能出现x1 = 1 和x2 = 2 的情况,但实际上这种情况是有可能发现的,因为如果编译器对这段程序代码执行重排优化后,可能出现下列情况

线程 1              线程 2
2: b = 1;          4: a = 2 ; 
1:x2 = a ;        3: x1 = b ;

这种执行顺序下就有可能出现x1 = 1 和x2 = 2 的情况,这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。

处理器指令重排

先了解一下指令重排的概念,处理器指令重排是对CPU的性能优化,从指令的执行角度来说一条指令可以分为多个步骤完成,如下

  • 取指 IF
  • 译码和取寄存器操作数 ID
  • 执行或者有效地址计算 EX
  • 存储器访问 MEM
  • 写回 WB

CPU在工作时,需要将上述指令分为多个步骤依次执行(注意硬件不同有可能不一样),由于每一个步会使用到不同的硬件操作,比如取指时会只有PC寄存器和存储器,译码时会执行到指令寄存器组,执行时会执行ALU(算术逻辑单元)、写回时使用到寄存器组。为了提高硬件利用率,CPU指令是按流水线技术来执行的,如下:

从图中可以看出当指令1还未执行完成时,第2条指令便利用空闲的硬件开始执行,这样做是有好处的,如果每个步骤花费1ms,那么如果第2条指令需要等待第1条指令执行完成后再执行的话,则需要等待5ms,但如果使用流水线技术的话,指令2只需等待1ms就可以开始执行了,这样就能大大提升CPU的执行性能。虽然流水线技术可以大大提升CPU的性能,但不幸的是一旦出现流水中断,所有硬件设备将会进入一轮停顿期,当再次弥补中断点可能需要几个周期,这样性能损失也会很大,就好比工厂组装手机的流水线,一旦某个零件组装中断,那么该零件往后的工人都有可能进入一轮或者几轮等待组装零件的过程。因此我们需要尽量阻止指令中断的情况,指令重排就是其中一种优化中断的手段,我们通过一个例子来阐明指令重排是如何阻止流水线技术中断的

a = b + c ;
d = e + f ;

下面通过汇编指令展示了上述代码在CPU执行的处理过程

  • LW指令 表示 load,其中LW R1,b表示把b的值加载到寄存器R1中
  • LW R2,c 表示把c的值加载到寄存器R2中
  • ADD 指令表示加法,把R1 、R2的值相加,并存入R3寄存器中。
  • SW 表示 store 即将 R3寄存器的值保持到变量a中
  • LW R4,e 表示把e的值加载到寄存器R4中
  • LW R5,f 表示把f的值加载到寄存器R5中
  • SUB 指令表示减法,把R4 、R5的值相减,并存入R6寄存器中。
  • SW d,R6 表示将R6寄存器的值保持到变量d中

上述便是汇编指令的执行过程,在某些指令上存在X的标志,X代表中断的含义,也就是只要有X的地方就会导致指令流水线技术停顿,同时也会影响后续指令的执行,可能需要经过1个或几个指令周期才可能恢复正常,那为什么停顿呢?这是因为部分数据还没准备好,如执行ADD指令时,需要使用到前面指令的数据R1,R2,而此时R2的MEM操作没有完成,即未拷贝到存储器中,这样加法计算就无法进行,必须等到MEM操作完成后才能执行,也就因此而停顿了,其他指令也是类似的情况。前面阐述过,停顿会造成CPU性能下降,因此我们应该想办法消除这些停顿,这时就需要使用到指令重排了,如下图,既然ADD指令需要等待,那我们就利用等待的时间做些别的事情,如把LW R4,e 和 LW R5,f 移动到前面执行,毕竟LW R4,e 和 LW R5,f执行并没有数据依赖关系,对他们有数据依赖关系的SUB R6,R5,R4指令在R4,R5加载完成后才执行的,没有影响,过程如下:

正如上图所示,所有的停顿都完美消除了,指令流水线也无需中断了,这样CPU的性能也能带来很好的提升,这就是处理器指令重排的作用。关于编译器重排以及指令重排(这两种重排我们后面统一称为指令重排)相关内容已阐述清晰了,我们必须意识到对于单线程而已指令重排几乎不会带来任何影响,比竟重排的前提是保证串行语义执行的一致性,但对于多线程环境而已,指令重排就可能导致严重的程序轮序执行问题,如下

class MixedOrder{
    int a = 0;
    boolean flag = false;
    public void writer(){
        a = 1;
        flag = true;
    }

    public void read(){
        if(flag){
            int i = a + 1;
        }
    }
}

如上述代码,同时存在线程A和线程B对该实例对象进行操作,其中A线程调用写入方法,而B线程调用读取方法,由于指令重排等原因,可能导致程序执行顺序变为如下:

 线程A                    线程B
 writer:                 read:
 1:flag = true;           1:flag = true;
 2:a = 1;                 2: a = 0 ; //误读
                          3: i = 1 ;

由于指令重排的原因,线程A的flag置为true被提前执行了,而a赋值为1的程序还未执行完,此时线程B,恰好读取flag的值为true,直接获取a的值(此时B线程并不知道a为0)并执行i赋值操作,结果i的值为1,而不是预期的2,这就是多线程环境下,指令重排导致的程序乱序执行的结果。因此,请记住,指令重排只会保证单线程中串行语义的执行的一致性,但并不会关心多线程间的语义一致性。

可见性

理解了指令重排现象后,可见性容易了,可见性指的是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值。对于串行程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。但在多线程环境中可就不一定了,前面我们分析过,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题,另外指令重排以及编译器优化也可能导致可见性问题,通过前面的分析,我们知道无论是编译器优化还是处理器优化的重排现象,在多线程环境下,确实会导致程序轮序执行的问题,从而也就导致可见性问题。

有序性

有序性是指对于单线程的执行代码,我们总是认为代码的执行是按顺序依次执行的,这样的理解并没有毛病,毕竟对于单线程而言确实如此,但对于多线程环境,则可能出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致,要明白的是,在Java程序中,倘若在本线程内,所有操作都视为有序行为,如果是多线程环境下,一个线程中观察另外一个线程,所有操作都是无序的,前半句指的是单线程内保证串行语义执行的一致性,后半句则指指令重排现象和工作内存与主内存同步延迟现象。

JMM提供的解决方案

在理解了原子性,可见性以及有序性问题后,看看JMM是如何保证的,在Java内存模型中都提供一套解决方案供Java工程师在开发过程使用,如原子性问题,除了JVM自身提供的对基本数据类型读写操作的原子性外,对于方法级别或者代码块级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性,关于synchronized的详解,看博主另外一篇文章( 深入理解Java并发之synchronized实现原理)。而工作内存与主内存同步延迟现象导致的可见性问题,可以使用synchronized关键字或者volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。对于指令重排导致的可见性问题和有序性问题,则可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化,关于volatile稍后会进一步分析。除了靠sychronized和volatile关键字来保证原子性、可见性以及有序性外,JMM内部还定义一套happens-before 原则来保证多线程环境下两个操作间的原子性、可见性以及有序性。

理解JMM中的happens-before 原则

倘若在程序开发中,仅靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,在Java内存模型中,还提供了happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下

  • 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。

  • 锁规则 解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。

  • volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。

  • 线程启动规则 线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见

  • 传递性 A先于B ,B先于C 那么A必然先于C

  • 线程终止规则 线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。

  • 线程中断规则 对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。

  • 对象终结规则 对象的构造函数执行,结束先于finalize()方法

上述8条原则无需手动添加任何同步手段(synchronized|volatile)即可达到效果,下面我们结合前面的案例演示这8条原则如何判断线程是否安全,如下:

class MixedOrder{
    int a = 0;
    boolean flag = false;
    public void writer(){
        a = 1;
        flag = true;
    }

    public void read(){
        if(flag){
            int i = a + 1;
        }
    }
}

同样的道理,存在两条线程A和B,线程A调用实例对象的writer()方法,而线程B调用实例对象的read()方法,线程A先启动而线程B后启动,那么线程B读取到的i值是多少呢?现在依据8条原则,由于存在两条线程同时调用,因此程序次序原则不合适。writer()方法和read()方法都没有使用同步手段,锁规则也不合适。没有使用volatile关键字,volatile变量原则不适应。线程启动规则、线程终止规则、线程中断规则、对象终结规则、传递性和本次测试案例也不合适。线程A和线程B的启动时间虽然有先后,但线程B执行结果却是不确定,也是说上述代码没有适合8条原则中的任意一条,也没有使用任何同步手段,所以上述的操作是线程不安全的,因此线程B读取的值自然也是不确定的。修复这个问题的方式很简单,要么给writer()方法和read()方法添加同步手段,如synchronized或者给变量flag添加volatile关键字,确保线程A修改的值对线程B总是可见。

volatile内存语义

volatile在并发编程中很常见,但也容易被滥用,现在我们就进一步分析volatile关键字的语义。volatile是Java虚拟机提供的轻量级的同步机制。volatile关键字有如下两个作用

  • 保证被volatile修饰的共享gong’x变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总数可以被其他线程立即得知。

  • 禁止指令重排序优化。

volatile的可见性

关于volatile的可见性作用,我们必须意识到被volatile修饰的变量对所有线程总数立即可见的,对volatile变量的所有写操作总是能立刻反应到其他线程中,但是对于volatile变量运算操作在多线程环境并不保证安全性,如下

public class VolatileVisibility {
    public static volatile int i =0;

    public static void increase(){
        i++;
    }
}

正如上述代码所示,i变量的任何改变都会立马反应到其他线程中,但是如此存在多条线程同时调用increase()方法的话,就会出现线程安全问题,毕竟i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全,需要注意的是一旦使用synchronized修饰方法后,由于synchronized本身也具备与volatile相同的特性,即可见性,因此在这样种情况下就完全可以省去volatile修饰变量。

public class VolatileVisibility {
    public static int i =0;

    public synchronized static void increase(){
        i++;
    }
}

现在来看另外一种场景,可以使用volatile修饰变量达到线程安全的目的,如下

public class VolatileSafe {

    volatile boolean close;

    public void close(){
        close=true;
    }

    public void doWork(){
        while (!close){
            System.out.println("safe....");
        }
    }
}

由于对于boolean变量close值的修改属于原子性操作,因此可以通过使用volatile修饰变量close,使用该变量对其他线程立即可见,从而达到线程安全的目的。那么JMM是如何实现让volatile变量对其他线程立即可见的呢?实际上,当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程将只能从主内存中重新读取共享变量。volatile变量正是通过这种写-读方式实现对其他线程可见(但其内存语义实现则是通过内存屏障,稍后会说明)。

volatile禁止重排优化

volatile关键字另一个作用就是禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象,关于指令重排优化前面已详细分析过,这里主要简单说明一下volatile是如何实现禁止指令重排优化的。先了解一个概念,内存屏障(Memory Barrier)。
内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行顺序,二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之,volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化。下面看一个非常典型的禁止重排优化的例子DCL,如下:

/**
 * Created by zejian on 2017/6/11.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class DoubleCheckLock {

    private static DoubleCheckLock instance;

    private DoubleCheckLock(){}

    public static DoubleCheckLock getInstance(){

        //第一次检测
        if (instance==null){
            //同步
            synchronized (DoubleCheckLock.class){
                if (instance == null){
                    //多线程环境下可能会出现问题的地方
                    instance = new DoubleCheckLock();
                }
            }
        }
        return instance;
    }
}

上述代码一个经典的单例的双重检测的代码,这段代码在单线程环境下并没有什么问题,但如果在多线程环境下就可以出现线程安全问题。原因在于某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)

memory = allocate(); //1.分配对象内存空间
instance(memory);    //2.初始化对象
instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null

由于步骤1和步骤2间可能会重排序,如下:

memory = allocate(); //1.分配对象内存空间
instance = memory;   //3.设置instance指向刚分配的内存地址,此时instance!=null,但是对象还没有初始化完成!
instance(memory);    //2.初始化对象

由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null时,由于instance实例未必已初始化完成,也就造成了线程安全问题。那么该如何解决呢,很简单,我们使用volatile禁止instance变量被执行指令重排优化即可。

  //禁止指令重排优化
  private volatile static DoubleCheckLock instance;

ok~,到此相信我们对Java内存模型和volatile应该都有了比较全面的认识,总而言之,我们应该清楚知道,JMM就是一组规则,这组规则意在解决在并发编程可能出现的线程安全问题,并提供了内置解决方案(happen-before原则)及其外部可使用的同步手段(synchronized/volatile等),确保了程序执行在多线程环境中的应有的原子性,可视性及其有序性。

如有错误,欢迎留言,谢谢!

参考资料:
http://tutorials.jenkov.com/java-concurrency/java-memory-model.html
http://blog.csdn.net/iter_zc/article/details/41843595
http://ifeve.com/wp-content/uploads/2014/03/JSR133%E4%B8%AD%E6%96%87%E7%89%881.pdf

《深入理解JVM虚拟机》
《Java高并发程序设计》

深入剖析基于并发AQS的(独占锁)重入锁(ReetrantLock)及其Condition实现原理

Lock接口

前面我们详谈过解决多线程同步问题的关键字synchronized,synchronized属于隐式锁,即锁的持有与释放都是隐式的,我们无需干预,而本篇我们要讲解的是显式锁,即锁的持有和释放都必须由我们手动编写。在Java 1.5中,官方在concurrent并发包中加入了Lock接口,该接口中提供了lock()方法和unLock()方法对显式加锁和显式释放锁操作进行支持,简单了解一下代码编写,如下:

Lock lock = new ReentrantLock();
lock.lock();
try{
    //临界区......
}finally{
    lock.unlock();
}

正如代码所显示(ReentrantLock是Lock的实现类,稍后分析),当前线程使用lock()方法与unlock()对临界区进行包围,其他线程由于无法持有锁将无法进入临界区直到当前线程释放锁,注意unlock()操作必须在finally代码块中,这样可以确保即使临界区执行抛出异常,线程最终也能正常释放锁,Lock接口还提供了锁以下相关方法

public interface Lock {
    //加锁
    void lock();

    //解锁
    void unlock();

    //可中断获取锁,与lock()不同之处在于可响应中断操作,即在获
    //取锁的过程中可中断,注意synchronized在获取锁时是不可中断的
    void lockInterruptibly() throws InterruptedException;

    //尝试非阻塞获取锁,调用该方法后立即返回结果,如果能够获取则返回true,否则返回false
    boolean tryLock();

    //根据传入的时间段获取锁,在指定时间内没有获取锁则返回false,如果在指定时间内当前线程未被中并断获取到锁则返回true
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    //获取等待通知组件,该组件与当前锁绑定,当前线程只有获得了锁
    //才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
    Condition newCondition();

可见Lock对象锁还提供了synchronized所不具备的其他同步特性,如可中断锁的获取(synchronized在等待获取锁时是不可中的),超时中断锁的获取,等待唤醒机制的多条件变量Condition等,这也使得Lock锁在使用上具有更大的灵活性。下面进一步分析Lock的实现类重入锁ReetrantLock。

重入锁ReetrantLock

重入锁ReetrantLock,JDK 1.5新增的类,实现了Lock接口,作用与synchronized关键字相当,但比synchronized更加灵活。ReetrantLock本身也是一种支持重进入的锁,即该锁可以支持一个线程对资源重复加锁,同时也支持公平锁与非公平锁。所谓的公平与非公平指的是在请求先后顺序上,先对锁进行请求的就一定先获取到锁,那么这就是公平锁,反之,如果对于锁的获取并没有时间上的先后顺序,如后请求的线程可能先获取到锁,这就是非公平锁,一般而言非,非公平锁机制的效率往往会胜过公平锁的机制,但在某些场景下,可能更注重时间先后顺序,那么公平锁自然是很好的选择。需要注意的是ReetrantLock支持对同一线程重加锁,但是加锁多少次,就必须解锁多少次,这样才可以成功释放锁。下面看看ReetrantLock的简单使用案例:

import java.util.concurrent.locks.ReentrantLock;

public class ReenterLock implements Runnable{
    public static ReentrantLock lock=new ReentrantLock();
    public static int i=0;
    @Override
    public void run() {
        for(int j=0;j<10000000;j++){
            lock.lock();
            //支持重入锁
            lock.lock();
            try{
                i++;
            }finally{
                //执行两次解锁
                lock.unlock();
                lock.unlock();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ReenterLock tl=new ReenterLock();
        Thread t1=new Thread(tl);
        Thread t2=new Thread(tl);
        t1.start();t2.start();
        t1.join();t2.join();
        //输出结果:20000000
        System.out.println(i);
    }
}

代码非常简单,我们使用两个线程同时操作临界资源i,执行自增操作,使用ReenterLock进行加锁,解决线程安全问题,这里进行了两次重复加锁,由于ReenterLock支持重入,因此这样是没有问题的,需要注意的是在finally代码块中,需执行两次解锁操作才能真正成功地让当前执行线程释放锁,从这里看ReenterLock的用法还是非常简单的,除了实现Lock接口的方法,ReenterLock其他方法说明如下

//查询当前线程保持此锁的次数。
int getHoldCount() 

//返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。      
protected  Thread   getOwner(); 

//返回一个 collection,它包含可能正等待获取此锁的线程,其内部维持一个队列,这点稍后会分析。      
protected  Collection<Thread>   getQueuedThreads(); 

//返回正等待获取此锁的线程估计数。   
int getQueueLength();

// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
protected  Collection<Thread>   getWaitingThreads(Condition condition); 

//返回等待与此锁相关的给定条件的线程估计数。       
int getWaitQueueLength(Condition condition);

// 查询给定线程是否正在等待获取此锁。     
boolean hasQueuedThread(Thread thread); 

//查询是否有些线程正在等待获取此锁。     
boolean hasQueuedThreads();

//查询是否有些线程正在等待与此锁有关的给定条件。     
boolean hasWaiters(Condition condition); 

//如果此锁的公平设置为 true,则返回 true。     
boolean isFair() 

//查询当前线程是否保持此锁。      
boolean isHeldByCurrentThread() 

//查询此锁是否由任意线程保持。        
boolean isLocked()       

由于ReetrantLock锁在使用上还是比较简单的,也就暂且打住,下面着重分析一下ReetrantLock的内部实现原理,这才是本篇博文的重点。实际上ReetrantLock是基于AQS并发框架实现的,我们先深入了解AQS,然后一步步揭开ReetrantLock的内部实现原理。

并发基础组件AQS与ReetrantLock

AQS工作原理概要

AbstractQueuedSynchronizer又称为队列同步器(后面简称AQS),它是用来构建锁或其他同步组件的基础框架,内部通过一个int类型的成员变量state来控制同步状态,当state=0时,则说明没有任何线程占有共享资源的锁,当state=1时,则说明有线程目前正在使用共享变量,其他线程必须加入同步队列进行等待,AQS内部通过内部类Node构成FIFO的同步队列来完成线程获取锁的排队工作,同时利用内部类ConditionObject构建等待队列,当Condition调用wait()方法后,线程将会加入等待队列中,而当Condition调用signal()方法后,线程将从等待队列转移动同步队列中进行锁竞争。注意这里涉及到两种队列,一种的同步队列,当线程请求锁而等待的后将加入同步队列等待,而另一种则是等待队列(可有多个),通过Condition调用await()方法释放锁后,将加入等待队列。关于Condition的等待队列我们后面再分析,这里我们先来看看AQS中的同步队列模型,如下

/**
 * AQS抽象类
 */
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{
//指向同步队列队头
private transient volatile Node head;

//指向同步的队尾
private transient volatile Node tail;

//同步状态,0代表锁未被占用,1代表锁已被占用
private volatile int state;

//省略其他代码......
}

head和tail分别是AQS中的变量,其中head指向同步队列的头部,注意head为空结点,不存储信息。而tail则是同步队列的队尾,同步队列采用的是双向链表的结构这样可方便队列进行结点增删操作。state变量则是代表同步状态,执行当线程调用lock方法进行加锁后,如果此时state的值为0,则说明当前线程可以获取到锁(在本篇文章中,锁和同步状态代表同一个意思),同时将state设置为1,表示获取成功。如果state已为1,也就是当前锁已被其他线程持有,那么当前执行线程将被封装为Node结点加入同步队列等待。其中Node结点是对每一个访问同步代码的线程的封装,从图中的Node的数据结构也可看出,其包含了需要同步的线程本身以及线程的状态,如是否被阻塞,是否等待唤醒,是否已经被取消等。每个Node结点内部关联其前继结点prev和后继结点next,这样可以方便线程释放锁后快速唤醒下一个在等待的线程,Node是AQS的内部类,其数据结构如下:

static final class Node {
    //共享模式
    static final Node SHARED = new Node();
    //独占模式
    static final Node EXCLUSIVE = null;

    //标识线程已处于结束状态
    static final int CANCELLED =  1;
    //等待被唤醒状态
    static final int SIGNAL    = -1;
    //条件状态,
    static final int CONDITION = -2;
    //在共享模式中使用表示获得的同步状态会被传播
    static final int PROPAGATE = -3;

    //等待状态,存在CANCELLED、SIGNAL、CONDITION、PROPAGATE 4种
    volatile int waitStatus;

    //同步队列中前驱结点
    volatile Node prev;

    //同步队列中后继结点
    volatile Node next;

    //请求锁的线程
    volatile Thread thread;

    //等待队列中的后继结点,这个与Condition有关,稍后会分析
    Node nextWaiter;

    //判断是否为共享模式
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    //获取前驱结点
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    //.....
}

其中SHARED和EXCLUSIVE常量分别代表共享模式和独占模式,所谓共享模式是一个锁允许多条线程同时操作,如信号量Semaphore采用的就是基于AQS的共享模式实现的,而独占模式则是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待,如ReentranLock。变量waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。

  • CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。

  • SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。

  • CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。

  • PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。

  • 0状态:值为0,代表初始化状态。

pre和next,分别指向当前Node结点的前驱结点和后继结点,thread变量存储的请求锁的线程。nextWaiter,与Condition相关,代表等待队列中的后继结点,关于这点这里暂不深入,后续会有更详细的分析,嗯,到此我们对Node结点的数据结构也就比较清晰了。总之呢,AQS作为基础组件,对于锁的实现存在两种不同的模式,即共享模式(如Semaphore)和独占模式(如ReetrantLock),无论是共享模式还是独占模式的实现类,其内部都是基于AQS实现的,也都维持着一个虚拟的同步队列,当请求锁的线程超过现有模式的限制时,会将线程包装成Node结点并将线程当前必要的信息存储到node结点中,然后加入同步队列等会获取锁,而这系列操作都有AQS协助我们完成,这也是作为基础组件的原因,无论是Semaphore还是ReetrantLock,其内部绝大多数方法都是间接调用AQS完成的,下面是AQS整体类图结构

这里以ReentrantLock为例,简单讲解ReentrantLock与AQS的关系

  • AbstractOwnableSynchronizer:抽象类,定义了存储独占当前锁的线程和获取的方法

  • AbstractQueuedSynchronizer:抽象类,AQS框架核心类,其内部以虚拟队列的方式管理线程的锁获取与锁释放,其中获取锁(tryAcquire方法)和释放锁(tryRelease方法)并没有提供默认实现,需要子类重写这两个方法实现具体逻辑,目的是使开发人员可以自由定义获取锁以及释放锁的方式。

  • Node:AbstractQueuedSynchronizer 的内部类,用于构建虚拟队列(链表双向链表),管理需要获取锁的线程。

  • Sync:抽象类,是ReentrantLock的内部类,继承自AbstractQueuedSynchronizer,实现了释放锁的操作(tryRelease()方法),并提供了lock抽象方法,由其子类实现。

  • NonfairSync:是ReentrantLock的内部类,继承自Sync,非公平锁的实现类。

  • FairSync:是ReentrantLock的内部类,继承自Sync,公平锁的实现类。

  • ReentrantLock:实现了Lock接口的,其内部类有Sync、NonfairSync、FairSync,在创建时可以根据fair参数决定创建NonfairSync(默认非公平锁)还是FairSync。

ReentrantLock内部存在3个实现类,分别是SyncNonfairSyncFairSync,其中Sync继承自AQS实现了解锁tryRelease()方法,而NonfairSync(非公平锁)、 FairSync(公平锁)则继承自Sync,实现了获取锁的tryAcquire()方法,ReentrantLock的所有方法调用都通过间接调用AQS和Sync类及其子类来完成的。从上述类图可以看出AQS是一个抽象类,但请注意其源码中并没一个抽象的方法,这是因为AQS只是作为一个基础组件,并不希望直接作为直接操作类对外输出,而更倾向于作为基础组件,为真正的实现类提供基础设施,如构建同步队列,控制同步状态等,事实上,从设计模式角度来看,AQS采用的模板模式的方式构建的,其内部除了提供并发操作核心方法以及同步队列操作外,还提供了一些模板方法让子类自己实现,如加锁操作以及解锁操作,为什么这么做?这是因为AQS作为基础组件,封装的是核心并发操作,但是实现上分为两种模式,即共享模式与独占模式,而这两种模式的加锁与解锁实现方式是不一样的,但AQS只关注内部公共方法实现并不关心外部不同模式的实现,所以提供了模板方法给子类使用,也就是说实现独占锁,如ReentrantLock需要自己实现tryAcquire()方法和tryRelease()方法,而实现共享模式的Semaphore,则需要实现tryAcquireShared()方法和tryReleaseShared()方法,这样做的好处是显而易见的,无论是共享模式还是独占模式,其基础的实现都是同一套组件(AQS),只不过是加锁解锁的逻辑不同罢了,更重要的是如果我们需要自定义锁的话,也变得非常简单,只需要选择不同的模式实现不同的加锁和解锁的模板方法即可,AQS提供给独占模式和共享模式的模板方法如下

//AQS中提供的主要模板方法,由子类实现。
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer{

    //独占模式下获取锁的方法
    protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
    }

    //独占模式下解锁的方法
    protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
    }

    //共享模式下获取锁的方法
    protected int tryAcquireShared(int arg) {
        throw new UnsupportedOperationException();
    }

    //共享模式下解锁的方法
    protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
    }
    //判断是否为持有独占锁
    protected boolean isHeldExclusively() {
        throw new UnsupportedOperationException();
    }

}

在了解AQS的原理概要后,下面我们就基于ReetrantLock进一步分析AQS的实现过程,这也是ReetrantLock的内部实现原理。

基于ReetrantLock分析AQS独占模式实现过程

ReetrantLock中非公平锁

AQS同步器的实现依赖于内部的同步队列(FIFO的双向链表对列)完成对同步状态(state)的管理,当前线程获取锁(同步状态)失败时,AQS会将该线程以及相关等待信息包装成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会将头结点head中的线程唤醒,让其尝试获取同步状态。关于同步队列和Node结点,前面我们已进行了较为详细的分析,这里重点分析一下获取同步状态和释放同步状态以及如何加入队列的具体操作,这里从ReetrantLock入手分析AQS的具体实现,我们先以非公平锁为例进行分析。

//默认构造,创建非公平锁NonfairSync
public ReentrantLock() {
    sync = new NonfairSync();
}
//根据传入参数创建锁类型
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

//加锁操作
public void lock() {
     sync.lock();
}

前面说过sync是个抽象类,存在两个不同的实现子类,这里从非公平锁入手,看看其实现

/**
 * 非公平锁实现
 */
static final class NonfairSync extends Sync {
    //加锁
    final void lock() {
        //执行CAS操作,获取同步状态
        if (compareAndSetState(0, 1))
       //成功则将独占锁线程设置为当前线程  
          setExclusiveOwnerThread(Thread.currentThread());
        else
            //否则再次请求同步状态
            acquire(1);
    }
}

这里获取锁时,首先对同步状态执行CAS操作,尝试把state的状态从0设置为1,如果返回true则代表获取同步状态成功,也就是当前线程获取锁成,可操作临界资源,如果返回false,则表示已有线程持有该同步状态(其值为1),获取锁失败,注意这里存在并发的情景,也就是可能同时存在多个线程设置state变量,因此是CAS操作保证了state变量操作的原子性。返回false后,执行 acquire(1)方法,该方法是AQS中的方法,它对中断不敏感,即使线程获取同步状态失败,进入同步队列,后续对该线程执行中断操作也不会从同步队列中移出,方法如下

public final void acquire(int arg) {
    //再次尝试获取同步状态
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这里传入参数arg表示要获取同步状态后设置的值(即要设置state的值),因为要获取锁,而status为0时是释放锁,1则是获取锁,所以这里一般传递参数为1,进入方法后首先会执行tryAcquire(arg)方法,在前面分析过该方法在AQS中并没有具体实现,而是交由子类实现,因此该方法是由ReetrantLock类内部实现的

//NonfairSync类
static final class NonfairSync extends Sync {

    protected final boolean tryAcquire(int acquires) {
         return nonfairTryAcquire(acquires);
     }
 }

//Sync类
abstract static class Sync extends AbstractQueuedSynchronizer {

  //nonfairTryAcquire方法
  final boolean nonfairTryAcquire(int acquires) {
      final Thread current = Thread.currentThread();
      int c = getState();
      //判断同步状态是否为0,并尝试再次获取同步状态
      if (c == 0) {
          //执行CAS操作
          if (compareAndSetState(0, acquires)) {
              setExclusiveOwnerThread(current);
              return true;
          }
      }
      //如果当前线程已获取锁,属于重入锁,再次获取锁后将status值加1
      else if (current == getExclusiveOwnerThread()) {
          int nextc = c + acquires;
          if (nextc < 0) // overflow
              throw new Error("Maximum lock count exceeded");
          //设置当前同步状态,当前只有一个线程持有锁,因为不会发生线程安全问题,可以直接执行 setState(nextc);
          setState(nextc);
          return true;
      }
      return false;
  }
  //省略其他代码
}

从代码执行流程可以看出,这里做了两件事,一是尝试再次获取同步状态,如果获取成功则将当前线程设置为OwnerThread,否则失败,二是判断当前线程current是否为OwnerThread,如果是则属于重入锁,state自增1,并获取锁成功,返回true,反之失败,返回false,也就是tryAcquire(arg)执行失败,返回false。需要注意的是nonfairTryAcquire(int acquires)内部使用的是CAS原子性操作设置state值,可以保证state的更改是线程安全的,因此只要任意一个线程调用nonfairTryAcquire(int acquires)方法并设置成功即可获取锁,不管该线程是新到来的还是已在同步队列的线程,毕竟这是非公平锁,并不保证同步队列中的线程一定比新到来线程请求(可能是head结点刚释放同步状态然后新到来的线程恰好获取到同步状态)先获取到锁,这点跟后面还会讲到的公平锁不同。ok~,接着看之前的方法acquire(int arg)

public final void acquire(int arg) {
    //再次尝试获取同步状态
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

如果tryAcquire(arg)返回true,acquireQueued自然不会执行,这是最理想的,因为毕竟当前线程已获取到锁,如果tryAcquire(arg)返回false,则会执行addWaiter(Node.EXCLUSIVE)进行入队操作,由于ReentrantLock属于独占锁,因此结点类型为Node.EXCLUSIVE,下面看看addWaiter方法具体实现

private Node addWaiter(Node mode) {
    //将请求同步状态失败的线程封装成结点
    Node node = new Node(Thread.currentThread(), mode);

    Node pred = tail;
    //如果是第一个结点加入肯定为空,跳过。
    //如果非第一个结点则直接执行CAS入队操作,尝试在尾部快速添加
    if (pred != null) {
        node.prev = pred;
        //使用CAS执行尾部结点替换,尝试在尾部快速添加
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果第一次加入或者CAS操作没有成功执行enq入队操作
    enq(node);
    return node;
}

创建了一个Node.EXCLUSIVE类型Node结点用于封装线程及其相关信息,其中tail是AQS的成员变量,指向队尾(这点前面的我们分析过AQS维持的是一个双向的链表结构同步队列),如果是第一个结点,则为tail肯定为空,那么将执行enq(node)操作,如果非第一个结点即tail指向不为null,直接尝试执行CAS操作加入队尾,如果CAS操作失败还是会执行enq(node),继续看enq(node)

private Node enq(final Node node) {
    //死循环
    for (;;) {
         Node t = tail;
         //如果队列为null,即没有头结点
         if (t == null) { // Must initialize
             //创建并使用CAS设置头结点
             if (compareAndSetHead(new Node()))
                 tail = head;
         } else {//队尾添加新结点
             node.prev = t;
             if (compareAndSetTail(t, node)) {
                 t.next = node;
                 return t;
             }
         }
     }
    }

这个方法使用一个死循环进行CAS操作,可以解决多线程并发问题。这里做了两件事,一是如果还没有初始同步队列则创建新结点并使用compareAndSetHead设置头结点,tail也指向head,二是队列已存在,则将新结点node添加到队尾。注意这两个步骤都存在同一时间多个线程操作的可能,如果有一个线程修改head和tail成功,那么其他线程将继续循环,直到修改成功,这里使用CAS原子操作进行头结点设置和尾结点tail替换可以保证线程安全,从这里也可以看出head结点本身不存在任何数据,它只是作为一个牵头结点,而tail永远指向尾部结点(前提是队列不为null)。

添加到同步队列后,结点就会进入一个自旋过程,即每个结点都在观察时机待条件满足获取同步状态,然后从同步队列退出并结束自旋,回到之前的acquire()方法,自旋过程是在acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法中执行的,代码如下

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        //自旋,死循环
        for (;;) {
            //获取前驱结点
            final Node p = node.predecessor();
            当且仅当p为头结点才尝试获取同步状态
            if (p == head && tryAcquire(arg)) {
                //将node设置为头结点
                setHead(node);
                //清空原来头结点的引用便于GC
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //如果前驱结点不是head,判断是否挂起线程
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            //最终都没能获取同步状态,结束该线程的请求
            cancelAcquire(node);
    }
}

当前线程在自旋(死循环)中获取同步状态,当且仅当前驱结点为头结点才尝试获取同步状态,这符合FIFO的规则,即先进先出,其次head是当前获取同步状态的线程结点,只有当head释放同步状态唤醒后继结点,后继结点才有可能获取到同步状态,因此后继结点在其前继结点为head时,才进行尝试获取同步状态,其他时刻将被挂起。进入if语句后调用setHead(node)方法,将当前线程结点设置为head

//设置为头结点
private void setHead(Node node) {
        head = node;
        //清空结点数据
        node.thread = null;
        node.prev = null;
}

设置为node结点被设置为head后,其thread信息和前驱结点将被清空,因为该线程已获取到同步状态(锁),正在执行了,也就没有必要存储相关信息了,head只有保存指向后继结点的指针即可,便于head结点释放同步状态后唤醒后继结点,执行结果如下图

从图可知更新head结点的指向,将后继结点的线程唤醒并获取同步状态,调用setHead(node)将其替换为head结点,清除相关无用数据。当然如果前驱结点不是head,那么执行如下

//如果前驱结点不是head,判断是否挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())

      interrupted = true;
}

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //获取当前结点的等待状态
        int ws = pred.waitStatus;
        //如果为等待唤醒(SIGNAL)状态则返回true
        if (ws == Node.SIGNAL)
            return true;
        //如果ws>0 则说明是结束状态,
        //遍历前驱结点直到找到没有结束状态的结点
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //如果ws小于0又不是SIGNAL状态,
            //则将其设置为SIGNAL状态,代表该结点的线程正在等待唤醒。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

private final boolean parkAndCheckInterrupt() {
        //将当前线程挂起
        LockSupport.park(this);
        //获取线程中断状态,interrupted()是判断当前中断状态,
        //并非中断线程,因此可能true也可能false,并返回
        return Thread.interrupted();
}

shouldParkAfterFailedAcquire()方法的作用是判断当前结点的前驱结点是否为SIGNAL状态(即等待唤醒状态),如果是则返回true。如果结点的ws为CANCELLED状态(值为1>0),即结束状态,则说明该前驱结点已没有用应该从同步队列移除,执行while循环,直到寻找到非CANCELLED状态的结点。倘若前驱结点的ws值不为CANCELLED,也不为SIGNAL(当从Condition的条件等待队列转移到同步队列时,结点状态为CONDITION因此需要转换为SIGNAL),那么将其转换为SIGNAL状态,等待被唤醒。
shouldParkAfterFailedAcquire()方法返回true,即前驱结点为SIGNAL状态同时又不是head结点,那么使用parkAndCheckInterrupt()方法挂起当前线程,称为WAITING状态,需要等待一个unpark()操作来唤醒它,到此ReetrantLock内部间接通过AQS的FIFO的同步队列就完成了lock()操作,这里我们总结成逻辑流程图

关于获取锁的操作,这里看看另外一种可中断的获取方式,即调用ReentrantLock类的lockInterruptibly()或者tryLock()方法,最终它们都间接调用到doAcquireInterruptibly()

 private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //直接抛异常,中断线程的同步状态请求
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

最大的不同是

if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
     //直接抛异常,中断线程的同步状态请求
       throw new InterruptedException();

检测到线程的中断操作后,直接抛出异常,从而中断线程的同步状态请求,移除同步队列,ok~,加锁流程到此。下面接着看unlock()操作

//ReentrantLock类的unlock
public void unlock() {
    sync.release(1);
}

//AQS类的release()方法
public final boolean release(int arg) {
    //尝试释放锁
    if (tryRelease(arg)) {

        Node h = head;
        if (h != null && h.waitStatus != 0)
            //唤醒后继结点的线程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

//ReentrantLock类中的内部类Sync实现的tryRelease(int releases) 
protected final boolean tryRelease(int releases) {

      int c = getState() - releases;
      if (Thread.currentThread() != getExclusiveOwnerThread())
          throw new IllegalMonitorStateException();
      boolean free = false;
      //判断状态是否为0,如果是则说明已释放同步状态
      if (c == 0) {
          free = true;
          //设置Owner为null
          setExclusiveOwnerThread(null);
      }
      //设置更新同步状态
      setState(c);
      return free;
  }

释放同步状态的操作相对简单些,tryRelease(int releases)方法是ReentrantLock类中内部类自己实现的,因为AQS对于释放锁并没有提供具体实现,必须由子类自己实现。释放同步状态后会使用unparkSuccessor(h)唤醒后继结点的线程,这里看看unparkSuccessor(h)

private void unparkSuccessor(Node node) {
    //这里,node一般为当前线程所在的结点。
    int ws = node.waitStatus;
    if (ws < 0)//置零当前线程所在的结点状态,允许失败。
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;//找到下一个需要唤醒的结点s
    if (s == null || s.waitStatus > 0) {//如果为空或已取消
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);//唤醒
}

从代码执行操作来看,这里主要作用是用unpark()唤醒同步队列中最前边未放弃线程(也就是状态为CANCELLED的线程结点s)。此时,回忆前面分析进入自旋的函数acquireQueued(),s结点的线程被唤醒后,会进入acquireQueued()函数的if (p == head && tryAcquire(arg))的判断,如果p!=head也不会有影响,因为它会执行shouldParkAfterFailedAcquire(),由于s通过unparkSuccessor()操作后已是同步队列中最前边未放弃的线程结点,那么通过shouldParkAfterFailedAcquire()内部对结点状态的调整,s也必然会成为head的next结点,因此再次自旋时p==head就成立了,然后s把自己设置成head结点,表示自己已经获取到资源了,最终acquire()也返回了,这就是独占锁释放的过程。
ok~,关于独占模式的加锁和释放锁的过程到这就分析完,总之呢,在AQS同步器中维护着一个同步队列,当线程获取同步状态失败后,将会被封装成Node结点,加入到同步队列中并进行自旋操作,当当前线程结点的前驱结点为head时,将尝试获取同步状态,获取成功将自己设置为head结点。在释放同步状态时,则通过调用子类(ReetrantLock中的Sync内部类)的tryRelease(int releases)方法释放同步状态,释放成功则唤醒后继结点的线程。

ReetrantLock中公平锁

了解完ReetrantLock中非公平锁的实现后,我们再来看看公平锁。与非公平锁不同的是,在获取锁的时,公平锁的获取顺序是完全遵循时间上的FIFO规则,也就是说先请求的线程一定会先获取锁,后来的线程肯定需要排队,这点与前面我们分析非公平锁的nonfairTryAcquire(int acquires)方法实现有锁不同,下面是公平锁中tryAcquire()方法的实现

//公平锁FairSync类中的实现
protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            //注意!!这里先判断同步队列是否存在结点
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

该方法与nonfairTryAcquire(int acquires)方法唯一的不同是在使用CAS设置尝试设置state值前,调用了hasQueuedPredecessors()判断同步队列是否存在结点,如果存在必须先执行完同步队列中结点的线程,当前线程进入等待状态。这就是非公平锁与公平锁最大的区别,即公平锁在线程请求到来时先会判断同步队列是否存在结点,如果存在先执行同步队列中的结点线程,当前线程将封装成node加入同步队列等待。而非公平锁呢,当线程请求到来时,不管同步队列是否存在线程结点,直接尝试获取同步状态,获取成功直接访问共享资源,但请注意在绝大多数情况下,非公平锁才是我们理想的选择,毕竟从效率上来说非公平锁总是胜于公平锁。
    以上便是ReentrantLock的内部实现原理,这里我们简单进行小结,重入锁ReentrantLock,是一个基于AQS并发框架的并发控制类,其内部实现了3个类,分别是Sync、NoFairSync以及FairSync类,其中Sync继承自AQS,实现了释放锁的模板方法tryRelease(int),而NoFairSync和FairSync都继承自Sync,实现各种获取锁的方法tryAcquire(int)。ReentrantLock的所有方法实现几乎都间接调用了这3个类,因此当我们在使用ReentrantLock时,大部分使用都是在间接调用AQS同步器中的方法,这就是ReentrantLock的内部实现原理,最后给出张类图结构

关于synchronized 与ReentrantLock

在JDK 1.6之后,虚拟机对于synchronized关键字进行整体优化后,在性能上synchronized与ReentrantLock已没有明显差距,因此在使用选择上,需要根据场景而定,大部分情况下我们依然建议是synchronized关键字,原因之一是使用方便语义清晰,二是性能上虚拟机已为我们自动优化。而ReentrantLock提供了多样化的同步特性,如超时获取锁、可以被中断获取锁(synchronized的同步是不能中断的)、等待唤醒机制的多个条件变量(Condition)等,因此当我们确实需要使用到这些功能是,可以选择ReentrantLock

神奇的Condition

关于Condition接口

在并发编程中,每个Java对象都存在一组监视器方法,如wait()notify()以及notifyAll()方法,通过这些方法,我们可以实现线程间通信与协作(也称为等待唤醒机制),如生产者-消费者模式,而且这些方法必须配合着synchronized关键字使用,关于这点,如果想有更深入的理解,可观看博主另外一篇博文【 深入理解Java并发之synchronized实现原理】,与synchronized的等待唤醒机制相比Condition具有更多的灵活性以及精确性,这是因为notify()在唤醒线程时是随机(同一个锁),而Condition则可通过多个Condition实例对象建立更加精细的线程控制,也就带来了更多灵活性了,我们可以简单理解为以下两点

  • 通过Condition能够精细的控制多线程的休眠与唤醒。

  • 对于一个锁,我们可以为多个线程间建立不同的Condition。

Condition是一个接口类,其主要方法如下:

public interface Condition {

 /**
  * 使当前线程进入等待状态直到被通知(signal)或中断
  * 当其他线程调用singal()或singalAll()方法时,该线程将被唤醒
  * 当其他线程调用interrupt()方法中断当前线程
  * await()相当于synchronized等待唤醒机制中的wait()方法
  */
 void await() throws InterruptedException;

 //当前线程进入等待状态,直到被唤醒,该方法不响应中断要求
 void awaitUninterruptibly();

 //调用该方法,当前线程进入等待状态,直到被唤醒或被中断或超时
 //其中nanosTimeout指的等待超时时间,单位纳秒
 long awaitNanos(long nanosTimeout) throws InterruptedException;

  //同awaitNanos,但可以指明时间单位
  boolean await(long time, TimeUnit unit) throws InterruptedException;

 //调用该方法当前线程进入等待状态,直到被唤醒、中断或到达某个时
 //间期限(deadline),如果没到指定时间就被唤醒,返回true,其他情况返回false
  boolean awaitUntil(Date deadline) throws InterruptedException;

 //唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须
 //获取与Condition相关联的锁,功能与notify()相同
  void signal();

 //唤醒所有等待在Condition上的线程,该线程从等待方法返回前必须
 //获取与Condition相关联的锁,功能与notifyAll()相同
  void signalAll();
}

关于Condition的实现类是AQS的内部类ConditionObject,关于这点我们稍后分析,这里先来看一个Condition的使用案例,即经典消费者生产者模式

Condition的使用案例-生产者消费者模式

这里我们通过一个卖烤鸭的案例来演示多生产多消费者的案例,该场景中存在两条生产线程t1和t2,用于生产烤鸭,也存在两条消费线程t3,t4用于消费烤鸭,4条线程同时执行,需要保证只有在生产线程产生烤鸭后,消费线程才能消费,否则只能等待,直到生产线程产生烤鸭后唤醒消费线程,注意烤鸭不能重复消费。ResourceByCondition类中定义product()和consume()两个方法,分别用于生产烤鸭和消费烤鸭,并且定义ReentrantLock锁,用于控制product()和consume()的并发,由于必须在烤鸭生成完成后消费线程才能消费烤鸭,否则只能等待,因此这里定义两组Condition对象,分别是producer_con和consumer_con,前者拥有控制生产线程,后者拥有控制消费线程,这里我们使用一个标志flag来控制是否有烤鸭,当flag为true时,代表烤鸭生成完毕,生产线程必须进入等待状态同时唤醒消费线程进行消费,消费线程消费完毕后将flag设置为false,代表烤鸭消费完成,进入等待状态,同时唤醒生产线程生产烤鸭,具体代码如下

package com.zejian.concurrencys;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Created by zejian on 2017/7/22.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class ResourceByCondition {
    private String name;
    private int count = 1;
    private boolean flag = false;

    //创建一个锁对象。
    Lock lock = new ReentrantLock();

    //通过已有的锁获取两组监视器,一组监视生产者,一组监视消费者。
    Condition producer_con = lock.newCondition();
    Condition consumer_con = lock.newCondition();

    /**
     * 生产
     * @param name
     */
    public  void product(String name)
    {
        lock.lock();
        try
        {
            while(flag){
                try{producer_con.await();}catch(InterruptedException e){}
            }
            this.name = name + count;
            count++;
            System.out.println(Thread.currentThread().getName()+"...生产者5.0..."+this.name);
            flag = true;
            consumer_con.signal();//直接唤醒消费线程
        }
        finally
        {
            lock.unlock();
        }
    }

    /**
     * 消费
     */
    public  void consume()
    {
        lock.lock();
        try
        {
            while(!flag){
                try{consumer_con.await();}catch(InterruptedException e){}
            }
            System.out.println(Thread.currentThread().getName()+"...消费者.5.0......."+this.name);//消费烤鸭1
            flag = false;
            producer_con.signal();//直接唤醒生产线程
        }
        finally
        {
            lock.unlock();
        }
    }
}

执行代码

package com.zejian.concurrencys;
/**
 * Created by zejian on 2017/7/22.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class Mutil_Producer_ConsumerByCondition {

    public static void main(String[] args) {
        ResourceByCondition r = new ResourceByCondition();
        Mutil_Producer pro = new Mutil_Producer(r);
        Mutil_Consumer con = new Mutil_Consumer(r);
        //生产者线程
        Thread t0 = new Thread(pro);
        Thread t1 = new Thread(pro);
        //消费者线程
        Thread t2 = new Thread(con);
        Thread t3 = new Thread(con);
        //启动线程
        t0.start();
        t1.start();
        t2.start();
        t3.start();
    }
}

/**
 * @decrition 生产者线程
 */
class Mutil_Producer implements Runnable {
    private ResourceByCondition r;

    Mutil_Producer(ResourceByCondition r) {
        this.r = r;
    }

    public void run() {
        while (true) {
            r.product("北京烤鸭");
        }
    }
}

/**
 * @decrition 消费者线程
 */
class Mutil_Consumer implements Runnable {
    private ResourceByCondition r;

    Mutil_Consumer(ResourceByCondition r) {
        this.r = r;
    }

    public void run() {
        while (true) {
            r.consume();
        }
    }
}

正如代码所示,我们通过两者Condition对象单独控制消费线程与生产消费,这样可以避免消费线程在唤醒线程时唤醒的还是消费线程,如果是通过synchronized的等待唤醒机制实现的话,就可能无法避免这种情况,毕竟同一个锁,对于synchronized关键字来说只能有一组等待唤醒队列,而不能像Condition一样,同一个锁拥有多个等待队列。synchronized的实现方案如下,

public class KaoYaResource {

    private String name;
    private int count = 1;//烤鸭的初始数量
    private boolean flag = false;//判断是否有需要线程等待的标志
    /**
     * 生产烤鸭
     */
    public synchronized void product(String name){
        while(flag){
            //此时有烤鸭,等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        this.name=name+count;//设置烤鸭的名称
        count++;
        System.out.println(Thread.currentThread().getName()+"...生产者..."+this.name);
        flag=true;//有烤鸭后改变标志
        notifyAll();//通知消费线程可以消费了
    }

    /**
     * 消费烤鸭
     */
    public synchronized void consume(){
        while(!flag){//如果没有烤鸭就等待
            try{this.wait();}catch(InterruptedException e){}
        }
        System.out.println(Thread.currentThread().getName()+"...消费者........"+this.name);//消费烤鸭1
        flag = false;
        notifyAll();//通知生产者生产烤鸭
    }
}

如上代码,在调用notify()或者 notifyAll()方法时,由于等待队列中同时存在生产者线程和消费者线程,所以我们并不能保证被唤醒的到底是消费者线程还是生产者线程,而Codition则可以避免这种情况。嗯,了解完Condition的使用方式后,下面我们将进一步探讨Condition背后的实现机制

Condition的实现原理

Condition的具体实现类是AQS的内部类ConditionObject,前面我们分析过AQS中存在两种队列,一种是同步队列,一种是等待队列,而等待队列就相对于Condition而言的。注意在使用Condition前必须获得锁,同时在Condition的等待队列上的结点与前面同步队列的结点是同一个类即Node,其结点的waitStatus的值为CONDITION。在实现类ConditionObject中有两个结点分别是firstWaiter和lastWaiter,firstWaiter代表等待队列第一个等待结点,lastWaiter代表等待队列最后一个等待结点,如下

 public class ConditionObject implements Condition, java.io.Serializable {
    //等待队列第一个等待结点
    private transient Node firstWaiter;
    //等待队列最后一个等待结点
    private transient Node lastWaiter;
    //省略其他代码.......
}

每个Condition都对应着一个等待队列,也就是说如果一个锁上创建了多个Condition对象,那么也就存在多个等待队列。等待队列是一个FIFO的队列,在队列中每一个节点都包含了一个线程的引用,而该线程就是Condition对象上等待的线程。当一个线程调用了await()相关的方法,那么该线程将会释放锁,并构建一个Node节点封装当前线程的相关信息加入到等待队列中进行等待,直到被唤醒、中断、超时才从队列中移出。Condition中的等待队列模型如下

正如图所示,Node节点的数据结构,在等待队列中使用的变量与同步队列是不同的,Condtion中等待队列的结点只有直接指向的后继结点并没有指明前驱结点,而且使用的变量是nextWaiter而不是next,这点我们在前面分析结点Node的数据结构时讲过。firstWaiter指向等待队列的头结点,lastWaiter指向等待队列的尾结点,等待队列中结点的状态只有两种即CANCELLED和CONDITION,前者表示线程已结束需要从等待队列中移除,后者表示条件结点等待被唤醒。再次强调每个Codition对象对于一个等待队列,也就是说AQS中只能存在一个同步队列,但可拥有多个等待队列。下面从代码层面看看被调用await()方法(其他await()实现原理类似)的线程是如何加入等待队列的,而又是如何从等待队列中被唤醒的

public final void await() throws InterruptedException {
      //判断线程是否被中断
      if (Thread.interrupted())
          throw new InterruptedException();
      //创建新结点加入等待队列并返回
      Node node = addConditionWaiter();
      //释放当前线程锁即释放同步状态
      int savedState = fullyRelease(node);
      int interruptMode = 0;
      //判断结点是否同步队列(SyncQueue)中,即是否被唤醒
      while (!isOnSyncQueue(node)) {
          //挂起线程
          LockSupport.park(this);
          //判断是否被中断唤醒,如果是退出循环。
          if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
              break;
      }
      //被唤醒后执行自旋操作争取获得锁,同时判断线程是否被中断
      if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
          interruptMode = REINTERRUPT;
       // clean up if cancelled
      if (node.nextWaiter != null) 
          //清理等待队列中不为CONDITION状态的结点
          unlinkCancelledWaiters();
      if (interruptMode != 0)
          reportInterruptAfterWait(interruptMode);
  }

执行addConditionWaiter()添加到等待队列。

 private Node addConditionWaiter() {
    Node t = lastWaiter;
      // 判断是否为结束状态的结点并移除
      if (t != null && t.waitStatus != Node.CONDITION) {
          unlinkCancelledWaiters();
          t = lastWaiter;
      }
      //创建新结点状态为CONDITION
      Node node = new Node(Thread.currentThread(), Node.CONDITION);
      //加入等待队列
      if (t == null)
          firstWaiter = node;
      else
          t.nextWaiter = node;
      lastWaiter = node;
      return node;
        }

await()方法主要做了3件事,一是调用addConditionWaiter()方法将当前线程封装成node结点加入等待队列,二是调用fullyRelease(node)方法释放同步状态并唤醒后继结点的线程。三是调用isOnSyncQueue(node)方法判断结点是否在同步队列中,注意是个while循环,如果同步队列中没有该结点就直接挂起该线程,需要明白的是如果线程被唤醒后就调用acquireQueued(node, savedState)执行自旋操作争取锁,即当前线程结点从等待队列转移到同步队列并开始努力获取锁。

接着看看唤醒操作singal()方法

 public final void signal() {
     //判断是否持有独占锁,如果不是抛出异常
   if (!isHeldExclusively())
          throw new IllegalMonitorStateException();
      Node first = firstWaiter;
      //唤醒等待队列第一个结点的线程
      if (first != null)
          doSignal(first);
 }

这里signal()方法做了两件事,一是判断当前线程是否持有独占锁,没有就抛出异常,从这点也可以看出只有独占模式先采用等待队列,而共享模式下是没有等待队列的,也就没法使用Condition。二是唤醒等待队列的第一个结点,即执行doSignal(first)

 private void doSignal(Node first) {
     do {
             //移除条件等待队列中的第一个结点,
             //如果后继结点为null,那么说没有其他结点将尾结点也设置为null
            if ( (firstWaiter = first.nextWaiter) == null)
                 lastWaiter = null;
             first.nextWaiter = null;
          //如果被通知节点没有进入到同步队列并且条件等待队列还有不为空的节点,则继续循环通知后续结点
         } while (!transferForSignal(first) &&
                  (first = firstWaiter) != null);
        }

//transferForSignal方法
final boolean transferForSignal(Node node) {
    //尝试设置唤醒结点的waitStatus为0,即初始化状态
    //如果设置失败,说明当期结点node的waitStatus已不为
    //CONDITION状态,那么只能是结束状态了,因此返回false
    //返回doSignal()方法中继续唤醒其他结点的线程,注意这里并
    //不涉及并发问题,所以CAS操作失败只可能是预期值不为CONDITION,
    //而不是多线程设置导致预期值变化,毕竟操作该方法的线程是持有锁的。
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
         return false;

        //加入同步队列并返回前驱结点p
        Node p = enq(node);
        int ws = p.waitStatus;
        //判断前驱结点是否为结束结点(CANCELLED=1)或者在设置
        //前驱节点状态为Node.SIGNAL状态失败时,唤醒被通知节点代表的线程
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            //唤醒node结点的线程
            LockSupport.unpark(node.thread);
        return true;
    }

注释说得很明白了,这里我们简单整体说明一下,doSignal(first)方法中做了两件事,从条件等待队列移除被唤醒的节点,然后重新维护条件等待队列的firstWaiter和lastWaiter的指向。二是将从等待队列移除的结点加入同步队列(在transferForSignal()方法中完成的),如果进入到同步队列失败并且条件等待队列还有不为空的节点,则继续循环唤醒后续其他结点的线程。到此整个signal()的唤醒过程就很清晰了,即signal()被调用后,先判断当前线程是否持有独占锁,如果有,那么唤醒当前Condition对象中等待队列的第一个结点的线程,并从等待队列中移除该结点,移动到同步队列中,如果加入同步队列失败,那么继续循环唤醒等待队列中的其他结点的线程,如果成功加入同步队列,那么如果其前驱结点是否已结束或者设置前驱节点状态为Node.SIGNAL状态失败,则通过LockSupport.unpark()唤醒被通知节点代表的线程,到此signal()任务完成,注意被唤醒后的线程,将从前面的await()方法中的while循环中退出,因为此时该线程的结点已在同步队列中,那么while (!isOnSyncQueue(node))将不在符合循环条件,进而调用AQS的acquireQueued()方法加入获取同步状态的竞争中,这就是等待唤醒机制的整个流程实现原理,流程如下图所示(注意无论是同步队列还是等待队列使用的Node数据结构都是同一个,不过是使用的内部变量不同罢了)

ok~,本篇先到这,关于AQS中的另一种模式即共享模式,下篇再详聊,欢迎继续关注。

主要参考资料
《Java并发编程的艺术》
《Java 编程思想》

深入理解Java枚举类型(enum)

理解枚举类型

枚举类型是Java 5中新增特性的一部分,它是一种特殊的数据类型,之所以特殊是因为它既是一种类(class)类型却又比类类型多了些特殊的约束,但是这些约束的存在也造就了枚举类型的简洁性、安全性以及便捷性。下面先来看看什么是枚举?如何定义枚举?

枚举的定义

回忆一下下面的程序,这是在没有枚举类型时定义常量常见的方式

/**
 * Created by zejian on 2017/5/7.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 * 使用普通方式定义日期常量
 */
public class DayDemo {

    public static final int MONDAY =1;

    public static final int TUESDAY=2;

    public static final int WEDNESDAY=3;

    public static final int THURSDAY=4;

    public static final int FRIDAY=5;

    public static final int SATURDAY=6;

    public static final int SUNDAY=7;

}

上述的常量定义常量的方式称为int枚举模式,这样的定义方式并没有什么错,但它存在许多不足,如在类型安全和使用方便性上并没有多少好处,如果存在定义int值相同的变量,混淆的几率还是很大的,编译器也不会提出任何警告,因此这种方式在枚举出现后并不提倡,现在我们利用枚举类型来重新定义上述的常量,同时也感受一把枚举定义的方式,如下定义周一到周日的常量

//枚举类型,使用关键字enum
enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

相当简洁,在定义枚举类型时我们使用的关键字是enum,与class关键字类似,只不过前者是定义枚举类型,后者是定义类类型。枚举类型Day中分别定义了从周一到周日的值,这里要注意,值一般是大写的字母,多个值之间以逗号分隔。同时我们应该知道的是枚举类型可以像类(class)类型一样,定义为一个单独的文件,当然也可以定义在其他类内部,更重要的是枚举常量在类型安全性和便捷性都很有保证,如果出现类型问题编译器也会提示我们改进,但务必记住枚举表示的类型其取值是必须有限的,也就是说每个值都是可以枚举出来的,比如上述描述的一周共有七天。那么该如何使用呢?如下:

/**
 * Created by zejian on 2017/5/7.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class EnumDemo {

    public static void main(String[] args){
        //直接引用
        Day day =Day.MONDAY;
    }

}
//定义枚举类型
enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

就像上述代码那样,直接引用枚举的值即可,这便是枚举类型的最简单模型。

枚举实现原理

我们大概了解了枚举类型的定义与简单使用后,现在有必要来了解一下枚举类型的基本实现原理。实际上在使用关键字enum创建枚举类型并编译后,编译器会为我们生成一个相关的类,这个类继承了Java API中的java.lang.Enum类,也就是说通过关键字enum创建枚举类型在编译后事实上也是一个类类型而且该类继承自java.lang.Enum类。下面我们编译前面定义的EnumDemo.java并查看生成的class文件来验证这个结论:

//查看目录下的java文件
zejian@zejiandeMBP enumdemo$ ls
EnumDemo.java
//利用javac命令编译EnumDemo.java
zejian@zejiandeMBP enumdemo$ javac EnumDemo.java 
//查看生成的class文件,注意有Day.class和EnumDemo.class 两个
zejian@zejiandeMBP enumdemo$ ls
Day.class  EnumDemo.class  EnumDemo.java

利用javac编译前面定义的EnumDemo.java文件后分别生成了Day.class和EnumDemo.class文件,而Day.class就是枚举类型,这也就验证前面所说的使用关键字enum定义枚举类型并编译后,编译器会自动帮助我们生成一个与枚举相关的类。我们再来看看反编译Day.class文件:

//反编译Day.class
final class Day extends Enum
{
    //编译器为我们添加的静态的values()方法
    public static Day[] values()
    {
        return (Day[])$VALUES.clone();
    }
    //编译器为我们添加的静态的valueOf()方法,注意间接调用了Enum也类的valueOf方法
    public static Day valueOf(String s)
    {
        return (Day)Enum.valueOf(com/zejian/enumdemo/Day, s);
    }
    //私有构造函数
    private Day(String s, int i)
    {
        super(s, i);
    }
     //前面定义的7种枚举实例
    public static final Day MONDAY;
    public static final Day TUESDAY;
    public static final Day WEDNESDAY;
    public static final Day THURSDAY;
    public static final Day FRIDAY;
    public static final Day SATURDAY;
    public static final Day SUNDAY;
    private static final Day $VALUES[];

    static 
    {    
        //实例化枚举实例
        MONDAY = new Day("MONDAY", 0);
        TUESDAY = new Day("TUESDAY", 1);
        WEDNESDAY = new Day("WEDNESDAY", 2);
        THURSDAY = new Day("THURSDAY", 3);
        FRIDAY = new Day("FRIDAY", 4);
        SATURDAY = new Day("SATURDAY", 5);
        SUNDAY = new Day("SUNDAY", 6);
        $VALUES = (new Day[] {
            MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
        });
    }
}

从反编译的代码可以看出编译器确实帮助我们生成了一个Day类(注意该类是final类型的,将无法被继承)而且该类继承自java.lang.Enum类,该类是一个抽象类(稍后我们会分析该类中的主要方法),除此之外,编译器还帮助我们生成了7个Day类型的实例对象分别对应枚举中定义的7个日期,这也充分说明了我们前面使用关键字enum定义的Day类型中的每种日期枚举常量也是实实在在的Day实例对象,只不过代表的内容不一样而已。注意编译器还为我们生成了两个静态方法,分别是values()和 valueOf(),稍后会分析它们的用法,到此我们也就明白了,使用关键字enum定义的枚举类型,在编译期后,也将转换成为一个实实在在的类,而在该类中,会存在每个在枚举类型中定义好变量的对应实例对象,如上述的MONDAY枚举类型对应public static final Day MONDAY;,同时编译器会为该类创建两个方法,分别是values()和valueOf()。ok~,到此相信我们对枚举的实现原理也比较清晰,下面我们深入了解一下java.lang.Enum类以及values()和valueOf()的用途。

枚举的常见方法

Enum抽象类常见方法

Enum是所有 Java 语言枚举类型的公共基本类(注意Enum是抽象类),以下是它的常见方法:

返回类型方法名称方法说明
int compareTo(E o) 比较此枚举与指定对象的顺序
boolean equals(Object other) 当指定对象等于此枚举常量时,返回 true。
Class<?> getDeclaringClass() 返回与此枚举常量的枚举类型相对应的 Class 对象
String name() 返回此枚举常量的名称,在其枚举声明中对其进行声明
int ordinal() 返回枚举常量的序数(它在枚举声明中的位置,其中初始常量序数为零)
String toString() 返回枚举常量的名称,它包含在声明中
static<T extends Enum<T>> T static valueOf(Class<T> enumType, String name) 返回带指定名称的指定枚举类型的枚举常量。

这里主要说明一下ordinal()方法,该方法获取的是枚举变量在枚举类中声明的顺序,下标从0开始,如日期中的MONDAY在第一个位置,那么MONDAY的ordinal值就是0,如果MONDAY的声明位置发生变化,那么ordinal方法获取到的值也随之变化,注意在大多数情况下我们都不应该首先使用该方法,毕竟它总是变幻莫测的。compareTo(E o)方法则是比较枚举的大小,注意其内部实现是根据每个枚举的ordinal值大小进行比较的。name()方法与toString()几乎是等同的,都是输出变量的字符串形式。至于valueOf(Class<T> enumType, String name)方法则是根据枚举类的Class对象和枚举名称获取枚举常量,注意该方法是静态的,后面在枚举单例时,我们还会详细分析该方法,下面的代码演示了上述方法:

package com.zejian.enumdemo;

/**
 * Created by zejian on 2017/5/7.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class EnumDemo {

    public static void main(String[] args){

        //创建枚举数组
        Day[] days=new Day[]{Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY,
                Day.THURSDAY, Day.FRIDAY, Day.SATURDAY, Day.SUNDAY};

        for (int i = 0; i <days.length ; i++) {
            System.out.println("day["+i+"].ordinal():"+days[i].ordinal());
        }

        System.out.println("-------------------------------------");
        //通过compareTo方法比较,实际上其内部是通过ordinal()值比较的
        System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[1]));
        System.out.println("days[0].compareTo(days[1]):"+days[0].compareTo(days[2]));

        //获取该枚举对象的Class对象引用,当然也可以通过getClass方法
        Class<?> clazz = days[0].getDeclaringClass();
        System.out.println("clazz:"+clazz);

        System.out.println("-------------------------------------");

        //name()
        System.out.println("days[0].name():"+days[0].name());
        System.out.println("days[1].name():"+days[1].name());
        System.out.println("days[2].name():"+days[2].name());
        System.out.println("days[3].name():"+days[3].name());

        System.out.println("-------------------------------------");

        System.out.println("days[0].toString():"+days[0].toString());
        System.out.println("days[1].toString():"+days[1].toString());
        System.out.println("days[2].toString():"+days[2].toString());
        System.out.println("days[3].toString():"+days[3].toString());

        System.out.println("-------------------------------------");

        Day d=Enum.valueOf(Day.class,days[0].name());
        Day d2=Day.valueOf(Day.class,days[0].name());
        System.out.println("d:"+d);
        System.out.println("d2:"+d2);
    }
 /**
 执行结果:
   day[0].ordinal():0
   day[1].ordinal():1
   day[2].ordinal():2
   day[3].ordinal():3
   day[4].ordinal():4
   day[5].ordinal():5
   day[6].ordinal():6
   -------------------------------------
   days[0].compareTo(days[1]):-1
   days[0].compareTo(days[1]):-2
   clazz:class com.zejian.enumdemo.Day
   -------------------------------------
   days[0].name():MONDAY
   days[1].name():TUESDAY
   days[2].name():WEDNESDAY
   days[3].name():THURSDAY
   -------------------------------------
   days[0].toString():MONDAY
   days[1].toString():TUESDAY
   days[2].toString():WEDNESDAY
   days[3].toString():THURSDAY
   -------------------------------------
   d:MONDAY
   d2:MONDAY
   */

}
enum Day {
    MONDAY, TUESDAY, WEDNESDAY,
    THURSDAY, FRIDAY, SATURDAY, SUNDAY
}

到此对于抽象类Enum类的基本内容就介绍完了,这里提醒大家一点,Enum类内部会有一个构造函数,该构造函数只能有编译器调用,我们是无法手动操作的,不妨看看Enum类的主要源码:

//实现了Comparable
public abstract class Enum<E extends Enum<E>>
        implements Comparable<E>, Serializable {

    private final String name; //枚举字符串名称

    public final String name() {
        return name;
    }

    private final int ordinal;//枚举顺序值

    public final int ordinal() {
        return ordinal;
    }

    //枚举的构造方法,只能由编译器调用
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

    public String toString() {
        return name;
    }

    public final boolean equals(Object other) {
        return this==other;
    }

    //比较的是ordinal值
    public final int compareTo(E o) {
        Enum<?> other = (Enum<?>)o;
        Enum<E> self = this;
        if (self.getClass() != other.getClass() && // optimization
            self.getDeclaringClass() != other.getDeclaringClass())
            throw new ClassCastException();
        return self.ordinal - other.ordinal;//根据ordinal值比较大小
    }

    @SuppressWarnings("unchecked")
    public final Class<E> getDeclaringClass() {
        //获取class对象引用,getClass()是Object的方法
        Class<?> clazz = getClass();
        //获取父类Class对象引用
        Class<?> zuper = clazz.getSuperclass();
        return (zuper == Enum.class) ? (Class<E>)clazz : (Class<E>)zuper;
    }


    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                String name) {
        //enumType.enumConstantDirectory()获取到的是一个map集合,key值就是name值,value则是枚举变量值   
        //enumConstantDirectory是class对象内部的方法,根据class对象获取一个map集合的值       
        T result = enumType.enumConstantDirectory().get(name);
        if (result != null)
            return result;
        if (name == null)
            throw new NullPointerException("Name is null");
        throw new IllegalArgumentException(
            "No enum constant " + enumType.getCanonicalName() + "." + name);
    }

    //.....省略其他没用的方法
}

通过Enum源码,可以知道,Enum实现了Comparable接口,这也是可以使用compareTo比较的原因,当然Enum构造函数也是存在的,该函数只能由编译器调用,毕竟我们只能使用enum关键字定义枚举,其他事情就放心交给编译器吧。

//由编译器调用
protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

编译器生成的Values方法与ValueOf方法

values()方法和valueOf(String name)方法是编译器生成的static方法,因此从前面的分析中,在Enum类中并没出现values()方法,但valueOf()方法还是有出现的,只不过编译器生成的valueOf()方法需传递一个name参数,而Enum自带的静态方法valueOf()则需要传递两个方法,从前面反编译后的代码可以看出,编译器生成的valueOf方法最终还是调用了Enum类的valueOf方法,下面通过代码来演示这两个方法的作用:

Day[] days2 = Day.values();
System.out.println("day2:"+Arrays.toString(days2));
Day day = Day.valueOf("MONDAY");
System.out.println("day:"+day);

/**
 输出结果:
 day2:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
 day:MONDAY
 */

从结果可知道,values()方法的作用就是获取枚举类中的所有变量,并作为数组返回,而valueOf(String name)方法与Enum类中的valueOf方法的作用类似根据名称获取枚举变量,只不过编译器生成的valueOf方法更简洁些只需传递一个参数。这里我们还必须注意到,由于values()方法是由编译器插入到枚举类中的static方法,所以如果我们将枚举实例向上转型为Enum,那么values()方法将无法被调用,因为Enum类中并没有values()方法,valueOf()方法也是同样的道理,注意是一个参数的。

 //正常使用
Day[] ds=Day.values();
//向上转型Enum
Enum e = Day.MONDAY;
//无法调用,没有此方法
//e.values();

枚举与Class对象

上述我们提到当枚举实例向上转型为Enum类型后,values()方法将会失效,也就无法一次性获取所有枚举实例变量,但是由于Class对象的存在,即使不使用values()方法,还是有可能一次获取到所有枚举实例变量的,在Class对象中存在如下方法:

返回类型方法名称方法说明
T[] getEnumConstants() 返回该枚举类型的所有元素,如果Class对象不是枚举类型,则返回null。
boolean isEnum() 当且仅当该类声明为源代码中的枚举时返回 true

因此通过getEnumConstants()方法,同样可以轻而易举地获取所有枚举实例变量下面通过代码来演示这个功能:

//正常使用
Day[] ds=Day.values();
//向上转型Enum
Enum e = Day.MONDAY;
//无法调用,没有此方法
//e.values();
//获取class对象引用
Class<?> clasz = e.getDeclaringClass();
if(clasz.isEnum()) {
    Day[] dsz = (Day[]) clasz.getEnumConstants();
    System.out.println("dsz:"+Arrays.toString(dsz));
}

/**
   输出结果:
   dsz:[MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY]
 */

正如上述代码所展示,通过Enum的class对象的getEnumConstants方法,我们仍能一次性获取所有的枚举实例常量。

枚举的进阶用法

在前面的分析中,我们都是基于简单枚举类型的定义,也就是在定义枚举时只定义了枚举实例类型,并没定义方法或者成员变量,实际上使用关键字enum定义的枚举类,除了不能使用继承(因为编译器会自动为我们继承Enum抽象类而Java只支持单继承,因此枚举类是无法手动实现继承的),可以把enum类当成常规类,也就是说我们可以向enum类中添加方法和变量,甚至是mian方法,下面就来感受一把。

向enum类添加方法与自定义构造函数

重新定义一个日期枚举类,带有desc成员变量描述该日期的对于中文描述,同时定义一个getDesc方法,返回中文描述内容,自定义私有构造函数,在声明枚举实例时传入对应的中文描述,代码如下:

package com.zejian.enumdemo;

/**
 * Created by zejian on 2017/5/8.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public enum Day2 {
    MONDAY("星期一"),
    TUESDAY("星期二"),
    WEDNESDAY("星期三"),
    THURSDAY("星期四"),
    FRIDAY("星期五"),
    SATURDAY("星期六"),
    SUNDAY("星期日");//记住要用分号结束

    private String desc;//中文描述

    /**
     * 私有构造,防止被外部调用
     * @param desc
     */
    private Day2(String desc){
        this.desc=desc;
    }

    /**
     * 定义方法,返回描述,跟常规类的定义没区别
     * @return
     */
    public String getDesc(){
        return desc;
    }

    public static void main(String[] args){
        for (Day2 day:Day2.values()) {
            System.out.println("name:"+day.name()+
                    ",desc:"+day.getDesc());
        }
    }

    /**
     输出结果:
     name:MONDAY,desc:星期一
     name:TUESDAY,desc:星期二
     name:WEDNESDAY,desc:星期三
     name:THURSDAY,desc:星期四
     name:FRIDAY,desc:星期五
     name:SATURDAY,desc:星期六
     name:SUNDAY,desc:星期日
     */
}

从上述代码可知,在enum类中确实可以像定义常规类一样声明变量或者成员方法。但是我们必须注意到,如果打算在enum类中定义方法,务必在声明完枚举实例后使用分号分开,倘若在枚举实例前定义任何方法,编译器都将会报错,无法编译通过,同时即使自定义了构造函数且enum的定义结束,我们也永远无法手动调用构造函数创建枚举实例,毕竟这事只能由编译器执行。

关于覆盖enum类方法

既然enum类跟常规类的定义没什么区别(实际上enum还是有些约束的),那么覆盖父类的方法也不会是什么难说,可惜的是父类Enum中的定义的方法只有toString方法没有使用final修饰,因此只能覆盖toString方法,如下通过覆盖toString省去了getDesc方法:

package com.zejian.enumdemo;

/**
 * Created by zejian on 2017/5/8.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public enum Day2 {
    MONDAY("星期一"),
    TUESDAY("星期二"),
    WEDNESDAY("星期三"),
    THURSDAY("星期四"),
    FRIDAY("星期五"),
    SATURDAY("星期六"),
    SUNDAY("星期日");//记住要用分号结束

    private String desc;//中文描述

    /**
     * 私有构造,防止被外部调用
     * @param desc
     */
    private Day2(String desc){
        this.desc=desc;
    }

    /**
     * 覆盖
     * @return
     */
    @Override
    public String toString() {
        return desc;
    }


    public static void main(String[] args){
        for (Day2 day:Day2.values()) {
            System.out.println("name:"+day.name()+
                    ",desc:"+day.toString());
        }
    }

    /**
     输出结果:
     name:MONDAY,desc:星期一
     name:TUESDAY,desc:星期二
     name:WEDNESDAY,desc:星期三
     name:THURSDAY,desc:星期四
     name:FRIDAY,desc:星期五
     name:SATURDAY,desc:星期六
     name:SUNDAY,desc:星期日
     */
}

enum类中定义抽象方法

与常规抽象类一样,enum类允许我们为其定义抽象方法,然后使每个枚举实例都实现该方法,以便产生不同的行为方式,注意abstract关键字对于枚举类来说并不是必须的如下:

package com.zejian.enumdemo;

/**
 * Created by zejian on 2017/5/9.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public enum EnumDemo3 {

    FIRST{
        @Override
        public String getInfo() {
            return "FIRST TIME";
        }
    },
    SECOND{
        @Override
        public String getInfo() {
            return "SECOND TIME";
        }
    }

    ;

    /**
     * 定义抽象方法
     * @return
     */
    public abstract String getInfo();

    //测试
    public static void main(String[] args){
        System.out.println("F:"+EnumDemo3.FIRST.getInfo());
        System.out.println("S:"+EnumDemo3.SECOND.getInfo());
        /**
         输出结果:
         F:FIRST TIME
         S:SECOND TIME
         */
    }
}

通过这种方式就可以轻而易举地定义每个枚举实例的不同行为方式。我们可能注意到,enum类的实例似乎表现出了多态的特性,可惜的是枚举类型的实例终究不能作为类型传递使用,就像下面的使用方式,编译器是不可能答应的:

//无法通过编译,毕竟EnumDemo3.FIRST是个实例对象
 public void text(EnumDemo3.FIRST instance){ }

在枚举实例常量中定义抽象方法

enum类与接口

由于Java单继承的原因,enum类并不能再继承其它类,但并不妨碍它实现接口,因此enum类同样是可以实现多接口的,如下:

package com.zejian.enumdemo;

/**
 * Created by zejian on 2017/5/8.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */

interface food{
    void eat();
}

interface sport{
    void run();
}

public enum EnumDemo2 implements food ,sport{
    FOOD,
    SPORT,
    ; //分号分隔

    @Override
    public void eat() {
        System.out.println("eat.....");
    }

    @Override
    public void run() {
        System.out.println("run.....");
    }
}

有时候,我们可能需要对一组数据进行分类,比如进行食物菜单分类而且希望这些菜单都属于food类型,appetizer(开胃菜)、mainCourse(主菜)、dessert(点心)、Coffee等,每种分类下有多种具体的菜式或食品,此时可以利用接口来组织,如下(代码引用自Thinking in Java):

public interface Food {
  enum Appetizer implements Food {
    SALAD, SOUP, SPRING_ROLLS;
  }
  enum MainCourse implements Food {
    LASAGNE, BURRITO, PAD_THAI,
    LENTILS, HUMMOUS, VINDALOO;
  }
  enum Dessert implements Food {
    TIRAMISU, GELATO, BLACK_FOREST_CAKE,
    FRUIT, CREME_CARAMEL;
  }
  enum Coffee implements Food {
    BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,
    LATTE, CAPPUCCINO, TEA, HERB_TEA;
  }
}

public class TypeOfFood {
  public static void main(String[] args) {
    Food food = Appetizer.SALAD;
    food = MainCourse.LASAGNE;
    food = Dessert.GELATO;
    food = Coffee.CAPPUCCINO;
  }
} 

通过这种方式可以很方便组织上述的情景,同时确保每种具体类型的食物也属于Food,现在我们利用一个枚举嵌套枚举的方式,把前面定义的菜谱存放到一个Meal菜单中,通过这种方式就可以统一管理菜单的数据了。

public enum Meal{
  APPETIZER(Food.Appetizer.class),
  MAINCOURSE(Food.MainCourse.class),
  DESSERT(Food.Dessert.class),
  COFFEE(Food.Coffee.class);
  private Food[] values;
  private Meal(Class<? extends Food> kind) {
    //通过class对象获取枚举实例
    values = kind.getEnumConstants();
  }
  public interface Food {
    enum Appetizer implements Food {
      SALAD, SOUP, SPRING_ROLLS;
    }
    enum MainCourse implements Food {
      LASAGNE, BURRITO, PAD_THAI,
      LENTILS, HUMMOUS, VINDALOO;
    }
    enum Dessert implements Food {
      TIRAMISU, GELATO, BLACK_FOREST_CAKE,
      FRUIT, CREME_CARAMEL;
    }
    enum Coffee implements Food {
      BLACK_COFFEE, DECAF_COFFEE, ESPRESSO,
      LATTE, CAPPUCCINO, TEA, HERB_TEA;
    }
  }
} 

枚举与switch

关于枚举与switch是个比较简单的话题,使用switch进行条件判断时,条件参数一般只能是整型,字符型。而枚举型确实也被switch所支持,在java 1.7后switch也对字符串进行了支持。这里我们简单看一下switch与枚举类型的使用:


/**
 * Created by zejian on 2017/5/9.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */

enum Color {GREEN,RED,BLUE}

public class EnumDemo4 {

    public static void printName(Color color){
        switch (color){
            case BLUE: //无需使用Color进行引用
                System.out.println("蓝色");
                break;
            case RED:
                System.out.println("红色");
                break;
            case GREEN:
                System.out.println("绿色");
                break;
        }
    }

    public static void main(String[] args){
        printName(Color.BLUE);
        printName(Color.RED);
        printName(Color.GREEN);

        //蓝色
        //红色
        //绿色
    }
}

需要注意的是使用在于switch条件进行结合使用时,无需使用Color引用。

枚举与单例模式

单例模式可以说是最常使用的设计模式了,它的作用是确保某个类只有一个实例,自行实例化并向整个系统提供这个实例。在实际应用中,线程池、缓存、日志对象、对话框对象常被设计成单例,总之,选择单例模式就是为了避免不一致状态,下面我们将会简单说明单例模式的几种主要编写方式,从而对比出使用枚举实现单例模式的优点。首先看看饿汉式的单例模式:

/**
 * Created by wuzejian on 2017/5/9.
 * 饿汉式(基于classloder机制避免了多线程的同步问题)
 */
public class SingletonHungry {

    private static SingletonHungry instance = new SingletonHungry();

    private SingletonHungry() {
    }

    public static SingletonHungry getInstance() {
        return instance;
    }
}

显然这种写法比较简单,但问题是无法做到延迟创建对象,事实上如果该单例类涉及资源较多,创建比较耗时间时,我们更希望它可以尽可能地延迟加载,从而减小初始化的负载,于是便有了如下的懒汉式单例:

/**
 * Created by wuzejian on 2017/5/9..
 * 懒汉式单例模式(适合多线程安全)
 */
public class SingletonLazy {

    private static volatile SingletonLazy instance;

    private SingletonLazy() {
    }

    public static synchronized SingletonLazy getInstance() {
        if (instance == null) {
            instance = new SingletonLazy();
        }
        return instance;
    }
}

这种写法能够在多线程中很好的工作避免同步问题,同时也具备lazy loading机制,遗憾的是,由于synchronized的存在,效率很低,在单线程的情景下,完全可以去掉synchronized,为了兼顾效率与性能问题,改进后代码如下:

public class Singleton {
    private static volatile Singleton singleton = null;

    private Singleton(){}

    public static Singleton getSingleton(){
        if(singleton == null){
            synchronized (Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }    
}

这种编写方式被称为“双重检查锁”,主要在getSingleton()方法中,进行两次null检查。这样可以极大提升并发度,进而提升性能。毕竟在单例中new的情况非常少,绝大多数都是可以并行的读操作,因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,也就提高了执行效率。但是必须注意的是volatile关键字,该关键字有两层语义。第一层语义是可见性,可见性是指在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以其它线程会马上读取到已修改的值,关于工作内存和主内存可简单理解为高速缓存(直接与CPU打交道)和主存(日常所说的内存条),注意工作内存是线程独享的,主存是线程共享的。volatile的第二层语义是禁止指令重排序优化,我们写的代码(特别是多线程代码),由于编译器优化,在实际执行的时候可能与我们编写的顺序不同。编译器只保证程序执行结果与源代码相同,却不保证实际指令的顺序与源代码相同,这在单线程并没什么问题,然而一旦引入多线程环境,这种乱序就可能导致严重问题。volatile关键字就可以从语义上解决这个问题,值得关注的是volatile的禁止指令重排序优化功能在Java 1.5后才得以实现,因此1.5前的版本仍然是不安全的,即使使用了volatile关键字。或许我们可以利用静态内部类来实现更安全的机制,静态内部类单例模式如下:

/**
 * Created by wuzejian on 2017/5/9.
 * 静态内部类
 */
public class SingletonInner {
    private static class Holder {
        private static SingletonInner singleton = new SingletonInner();
    }

    private SingletonInner(){}

    public static SingletonInner getSingleton(){
        return Holder.singleton;
    }
}

正如上述代码所展示的,我们把Singleton实例放到一个静态内部类中,这样可以避免了静态实例在Singleton类的加载阶段(类加载过程的其中一个阶段的,此时只创建了Class对象,关于Class对象可以看博主另外一篇博文, 深入理解Java类型信息(Class对象)与反射机制)就创建对象,毕竟静态变量初始化是在SingletonInner类初始化时触发的,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。从上述4种单例模式的写法中,似乎也解决了效率与懒加载的问题,但是它们都有两个共同的缺点:

  • 序列化可能会破坏单例模式,比较每次反序列化一个序列化的对象实例时都会创建一个新的实例,解决方案如下:

    //测试例子(四种写解决方式雷同)
    public class Singleton implements java.io.Serializable {     
       public static Singleton INSTANCE = new Singleton();     
    
       protected Singleton() {     
       }  
    
       //反序列时直接返回当前INSTANCE
       private Object readResolve() {     
                return INSTANCE;     
          }    
    }   
  • 使用反射强行调用私有构造器,解决方式可以修改构造器,让它在创建第二个实例的时候抛异常,如下:

    public static Singleton INSTANCE = new Singleton();     
    private static volatile  boolean  flag = true;
    private Singleton(){
        if(flag){
        flag = false;   
        }else{
            throw new RuntimeException("The instance  already exists !");
        }
    }

如上所述,问题确实也得到了解决,但问题是我们为此付出了不少努力,即添加了不少代码,还应该注意到如果单例类维持了其他对象的状态时还需要使他们成为transient的对象,这种就更复杂了,那有没有更简单更高效的呢?当然是有的,那就是枚举单例了,先来看看如何实现:

/**
 * Created by wuzejian on 2017/5/9.
 * 枚举单利
 */
public enum  SingletonEnum {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

代码相当简洁,我们也可以像常规类一样编写enum类,为其添加变量和方法,访问方式也更简单,使用SingletonEnum.INSTANCE进行访问,这样也就避免调用getInstance方法,更重要的是使用枚举单例的写法,我们完全不用考虑序列化和反射的问题。枚举序列化是由jvm保证的,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,在枚举类型的序列化和反序列化上,Java做了特殊的规定:在序列化时Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的并禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法,从而保证了枚举实例的唯一性,这里我们不妨再次看看Enum类的valueOf方法:

public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                              String name) {
      T result = enumType.enumConstantDirectory().get(name);
      if (result != null)
          return result;
      if (name == null)
          throw new NullPointerException("Name is null");
      throw new IllegalArgumentException(
          "No enum constant " + enumType.getCanonicalName() + "." + name);
  }

实际上通过调用enumType(Class对象的引用)的enumConstantDirectory方法获取到的是一个Map集合,在该集合中存放了以枚举name为key和以枚举实例变量为value的Key&Value数据,因此通过name的值就可以获取到枚举实例,看看enumConstantDirectory方法源码:

Map<String, T> enumConstantDirectory() {
        if (enumConstantDirectory == null) {
            //getEnumConstantsShared最终通过反射调用枚举类的values方法
            T[] universe = getEnumConstantsShared();
            if (universe == null)
                throw new IllegalArgumentException(
                    getName() + " is not an enum type");
            Map<String, T> m = new HashMap<>(2 * universe.length);
            //map存放了当前enum类的所有枚举实例变量,以name为key值
            for (T constant : universe)
                m.put(((Enum<?>)constant).name(), constant);
            enumConstantDirectory = m;
        }
        return enumConstantDirectory;
    }
    private volatile transient Map<String, T> enumConstantDirectory = null;

到这里我们也就可以看出枚举序列化确实不会重新创建新实例,jvm保证了每个枚举实例变量的唯一性。再来看看反射到底能不能创建枚举,下面试图通过反射获取构造器并创建枚举

public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
  //获取枚举类的构造函数(前面的源码已分析过)
   Constructor<SingletonEnum> constructor=SingletonEnum.class.getDeclaredConstructor(String.class,int.class);
   constructor.setAccessible(true);
   //创建枚举
   SingletonEnum singleton=constructor.newInstance("otherInstance",9);
  }

执行报错

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at zejian.SingletonEnum.main(SingletonEnum.java:38)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

显然告诉我们不能使用反射创建枚举类,这是为什么呢?不妨看看newInstance方法源码:

 public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        //这里判断Modifier.ENUM是不是枚举修饰符,如果是就抛异常
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

源码很了然,确实无法使用反射创建枚举实例,也就是说明了创建枚举实例只有编译器能够做到而已。显然枚举单例模式确实是很不错的选择,因此我们推荐使用它。但是这总不是万能的,对于android平台这个可能未必是最好的选择,在android开发中,内存优化是个大块头,而使用枚举时占用的内存常常是静态变量的两倍还多,因此android官方在内存优化方面给出的建议是尽量避免在android中使用enum。但是不管如何,关于单例,我们总是应该记住:线程安全,延迟加载,序列化与反序列化安全,反射安全是很重重要的。

EnumMap

EnumMap基本用法

先思考这样一个问题,现在我们有一堆size大小相同而颜色不同的数据,需要统计出每种颜色的数量是多少以便将数据录入仓库,定义如下枚举用于表示颜色Color:

enum Color {
    GREEN,RED,BLUE,YELLOW
}

我们有如下解决方案,使用Map集合来统计,key值作为颜色名称,value代表衣服数量,如下:

package com.zejian.enumdemo;

import java.util.*;

/**
 * Created by zejian on 2017/5/10.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class EnumMapDemo {
    public static void main(String[] args){
        List<Clothes> list = new ArrayList<>();
        list.add(new Clothes("C001",Color.BLUE));
        list.add(new Clothes("C002",Color.YELLOW));
        list.add(new Clothes("C003",Color.RED));
        list.add(new Clothes("C004",Color.GREEN));
        list.add(new Clothes("C005",Color.BLUE));
        list.add(new Clothes("C006",Color.BLUE));
        list.add(new Clothes("C007",Color.RED));
        list.add(new Clothes("C008",Color.YELLOW));
        list.add(new Clothes("C009",Color.YELLOW));
        list.add(new Clothes("C010",Color.GREEN));
        //方案1:使用HashMap
        Map<String,Integer> map = new HashMap<>();
        for (Clothes clothes:list){
           String colorName=clothes.getColor().name();
           Integer count = map.get(colorName);
            if(count!=null){
                map.put(colorName,count+1);
            }else {
                map.put(colorName,1);
            }
        }

        System.out.println(map.toString());

        System.out.println("---------------");

        //方案2:使用EnumMap
        Map<Color,Integer> enumMap=new EnumMap<>(Color.class);

        for (Clothes clothes:list){
            Color color=clothes.getColor();
            Integer count = enumMap.get(color);
            if(count!=null){
                enumMap.put(color,count+1);
            }else {
                enumMap.put(color,1);
            }
        }

        System.out.println(enumMap.toString());
    }

    /**
     输出结果:
     {RED=2, BLUE=3, YELLOW=3, GREEN=2}
     ---------------
     {GREEN=2, RED=2, BLUE=3, YELLOW=3}
     */
}

代码比较简单,我们使用两种解决方案,一种是HashMap,一种EnumMap,虽然都统计出了正确的结果,但是EnumMap作为枚举的专属的集合,我们没有理由再去使用HashMap,毕竟EnumMap要求其Key必须为Enum类型,因而使用Color枚举实例作为key是最恰当不过了,也避免了获取name的步骤,更重要的是EnumMap效率更高,因为其内部是通过数组实现的(稍后分析),注意EnumMap的key值不能为null,虽说是枚举专属集合,但其操作与一般的Map差不多,概括性来说EnumMap是专门为枚举类型量身定做的Map实现,虽然使用其它的Map(如HashMap)也能完成相同的功能,但是使用EnumMap会更加高效,它只能接收同一枚举类型的实例作为键值且不能为null,由于枚举类型实例的数量相对固定并且有限,所以EnumMap使用数组来存放与枚举类型对应的值,毕竟数组是一段连续的内存空间,根据程序局部性原理,效率会相当高。下面我们来进一步了解EnumMap的用法,先看构造函数:

//创建一个具有指定键类型的空枚举映射。
EnumMap(Class<K> keyType) 
//创建一个其键类型与指定枚举映射相同的枚举映射,最初包含相同的映射关系(如果有的话)。     
EnumMap(EnumMap<K,? extends V> m) 
//创建一个枚举映射,从指定映射对其初始化。
EnumMap(Map<K,? extends V> m)       

与HashMap不同,它需要传递一个类型信息,即Class对象,通过这个参数EnumMap就可以根据类型信息初始化其内部数据结构,另外两只是初始化时传入一个Map集合,代码演示如下:

//使用第一种构造
Map<Color,Integer> enumMap=new EnumMap<>(Color.class);
//使用第二种构造
Map<Color,Integer> enumMap2=new EnumMap<>(enumMap);
//使用第三种构造
Map<Color,Integer> hashMap = new HashMap<>();
hashMap.put(Color.GREEN, 2);
hashMap.put(Color.BLUE, 3);
Map<Color, Integer> enumMap = new EnumMap<>(hashMap);

至于EnumMap的方法,跟普通的map几乎没有区别,注意与HashMap的主要不同在于构造方法需要传递类型参数和EnumMap保证Key顺序与枚举中的顺序一致,但请记住Key不能为null。

EnumMap实现原理剖析

EnumMap的源码有700多行,这里我们主要分析其内部存储结构,添加查找的实现,了解这几点,对应EnumMap内部实现原理也就比较清晰了,先看数据结构和构造函数

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable
{
    //Class对象引用
    private final Class<K> keyType;

    //存储Key值的数组
    private transient K[] keyUniverse;

    //存储Value值的数组
    private transient Object[] vals;

    //map的size
    private transient int size = 0;

    //空map
    private static final Enum<?>[] ZERO_LENGTH_ENUM_ARRAY = new Enum<?>[0];

    //构造函数
    public EnumMap(Class<K> keyType) {
        this.keyType = keyType;
        keyUniverse = getKeyUniverse(keyType);
        vals = new Object[keyUniverse.length];
    }

}

EnumMap继承了AbstractMap类,因此EnumMap具备一般map的使用方法,keyType表示类型信息,keyUniverse表示键数组,存储的是所有可能的枚举值,vals数组表示键对应的值,size表示键值对个数。在构造函数中通过keyUniverse = getKeyUniverse(keyType);初始化了keyUniverse数组的值,内部存储的是所有可能的枚举值,接着初始化了存在Value值得数组vals,其大小与枚举实例的个数相同,getKeyUniverse方法实现如下

//返回枚举数组
private static <K extends Enum<K>> K[] getKeyUniverse(Class<K> keyType) {
        //最终调用到枚举类型的values方法,values方法返回所有可能的枚举值
        return SharedSecrets.getJavaLangAccess()
                                        .getEnumConstantsShared(keyType);
    }

从方法的返回值来看,返回类型是枚举数组,事实也是如此,最终返回值正是枚举类型的values方法的返回值,前面我们分析过values方法返回所有可能的枚举值,因此keyUniverse数组存储就是枚举类型的所有可能的枚举值。接着看put方法的实现

 public V put(K key, V value) {
        typeCheck(key);//检测key的类型
        //获取存放value值得数组下标
        int index = key.ordinal();
        //获取旧值
        Object oldValue = vals[index];
        //设置value值
        vals[index] = maskNull(value);
        if (oldValue == null)
            size++;
        return unmaskNull(oldValue);//返回旧值
    }

这里通过typeCheck方法进行了key类型检测,判断是否为枚举类型,如果类型不对,会抛出异常

private void typeCheck(K key) {
   Class<?> keyClass = key.getClass();//获取类型信息
   if (keyClass != keyType && keyClass.getSuperclass() != keyType)
       throw new ClassCastException(keyClass + " != " + keyType);
}

接着通过int index = key.ordinal()的方式获取到该枚举实例的顺序值,利用此值作为下标,把值存储在vals数组对应下标的元素中即vals[index],这也是为什么EnumMap能维持与枚举实例相同存储顺序的原因,我们发现在对vals[]中元素进行赋值和返回旧值时分别调用了maskNull方法和unmaskNull方法

 //代表NULL值得空对象实例
  private static final Object NULL = new Object() {
        public int hashCode() {
            return 0;
        }

        public String toString() {
            return "java.util.EnumMap.NULL";
        }
    };

    private Object maskNull(Object value) {
        //如果值为空,返回NULL对象,否则返回value
        return (value == null ? NULL : value);
    }

    @SuppressWarnings("unchecked")
    private V unmaskNull(Object value) {
        //将NULL对象转换为null值
        return (V)(value == NULL ? null : value);
    }

由此看来EnumMap还是允许存放null值的,但key绝对不能为null,对于null值,EnumMap进行了特殊处理,将其包装为NULL对象,毕竟vals[]存的是Object,maskNull方法和unmaskNull方法正是用于null的包装和解包装的。这就是EnumMap集合的添加过程。下面接着看获取方法

 public V get(Object key) {
        return (isValidKey(key) ?
                unmaskNull(vals[((Enum<?>)key).ordinal()]) : null);
    }

 //对Key值的有效性和类型信息进行判断
 private boolean isValidKey(Object key) {
      if (key == null)
          return false;

      // Cheaper than instanceof Enum followed by getDeclaringClass
      Class<?> keyClass = key.getClass();
      return keyClass == keyType || keyClass.getSuperclass() == keyType;
  }

相对应put方法,get方法显示相当简洁,key有效的话,直接通过ordinal方法取索引,然后在值数组vals里通过索引获取值返回。remove方法如下:

 public V remove(Object key) {
        //判断key值是否有效
        if (!isValidKey(key))
            return null;
        //直接获取索引
        int index = ((Enum<?>)key).ordinal();

        Object oldValue = vals[index];
        //对应下标元素值设置为null
        vals[index] = null;
        if (oldValue != null)
            size--;//减size
        return unmaskNull(oldValue);
    }

非常简单,key值有效,通过key获取下标索引值,把vals[]对应下标值设置为null,size减一。查看是否包含某个值,

判断是否包含某value
public boolean containsValue(Object value) {
    value = maskNull(value);
    //遍历数组实现
    for (Object val : vals)
        if (value.equals(val))
            return true;

    return false;
}
//判断是否包含key
public boolean containsKey(Object key) {
    return isValidKey(key) && vals[((Enum<?>)key).ordinal()] != null;
}

判断value直接通过遍历数组实现,而判断key就更简单了,判断key是否有效和对应vals[]中是否存在该值。ok~,这就是EnumMap的主要实现原理,即内部有两个数组,长度相同,一个表示所有可能的键(枚举值),一个表示对应的值,不允许keynull,但允许value为null,键都有一个对应的索引,根据索引直接访问和操作其键数组和值数组,由于操作都是数组,因此效率很高。

EnumSet

EnumSet是与枚举类型一起使用的专用 Set 集合,EnumSet 中所有元素都必须是枚举类型。与其他Set接口的实现类HashSet/TreeSet(内部都是用对应的HashMap/TreeMap实现的)不同的是,EnumSet在内部实现是位向量(稍后分析),它是一种极为高效的位运算操作,由于直接存储和操作都是bit,因此EnumSet空间和时间性能都十分可观,足以媲美传统上基于 int 的“位标志”的运算,重要的是我们可像操作set集合一般来操作位运算,这样使用代码更简单易懂同时又具备类型安全的优势。注意EnumSet不允许使用 null 元素。试图插入 null 元素将抛出 NullPointerException,但试图测试判断是否存在null 元素或移除 null 元素则不会抛出异常,与大多数collection 实现一样,EnumSet不是线程安全的,因此在多线程环境下应该注意数据同步问题,ok~,下面先来简单看看EnumSet的使用方式。

EnumSet用法

创建EnumSet并不能使用new关键字,因为它是个抽象类,而应该使用其提供的静态工厂方法,EnumSet的静态工厂方法比较多,如下:

创建一个具有指定元素类型的空EnumSet。
EnumSet<E>  noneOf(Class<E> elementType)       
//创建一个指定元素类型并包含所有枚举值的EnumSet
<E extends Enum<E>> EnumSet<E> allOf(Class<E> elementType)
// 创建一个包括枚举值中指定范围元素的EnumSet
<E extends Enum<E>> EnumSet<E> range(E from, E to)
// 初始集合包括指定集合的补集
<E extends Enum<E>> EnumSet<E> complementOf(EnumSet<E> s)
// 创建一个包括参数中所有元素的EnumSet
<E extends Enum<E>> EnumSet<E> of(E e)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4)
<E extends Enum<E>> EnumSet<E> of(E e1, E e2, E e3, E e4, E e5)
<E extends Enum<E>> EnumSet<E> of(E first, E... rest)
//创建一个包含参数容器中的所有元素的EnumSet
<E extends Enum<E>> EnumSet<E> copyOf(EnumSet<E> s)
<E extends Enum<E>> EnumSet<E> copyOf(Collection<E> c)

代码演示如下:

package zejian;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;

/**
 * Created by wuzejian on 2017/5/12.
 *
 */
enum Color {
    GREEN , RED , BLUE , BLACK , YELLOW
}


public class EnumSetDemo {

    public static void main(String[] args){

        //空集合
        EnumSet<Color> enumSet= EnumSet.noneOf(Color.class);
        System.out.println("添加前:"+enumSet.toString());
        enumSet.add(Color.GREEN);
        enumSet.add(Color.RED);
        enumSet.add(Color.BLACK);
        enumSet.add(Color.BLUE);
        enumSet.add(Color.YELLOW);
        System.out.println("添加后:"+enumSet.toString());

        System.out.println("-----------------------------------");

        //使用allOf创建包含所有枚举类型的enumSet,其内部根据Class对象初始化了所有枚举实例
        EnumSet<Color> enumSet1= EnumSet.allOf(Color.class);
        System.out.println("allOf直接填充:"+enumSet1.toString());

        System.out.println("-----------------------------------");

        //初始集合包括枚举值中指定范围的元素
        EnumSet<Color> enumSet2= EnumSet.range(Color.BLACK,Color.YELLOW);
        System.out.println("指定初始化范围:"+enumSet2.toString());

        System.out.println("-----------------------------------");

        //指定补集,也就是从全部枚举类型中去除参数集合中的元素,如下去掉上述enumSet2的元素
        EnumSet<Color> enumSet3= EnumSet.complementOf(enumSet2);
        System.out.println("指定补集:"+enumSet3.toString());

        System.out.println("-----------------------------------");

        //初始化时直接指定元素
        EnumSet<Color> enumSet4= EnumSet.of(Color.BLACK);
        System.out.println("指定Color.BLACK元素:"+enumSet4.toString());
        EnumSet<Color> enumSet5= EnumSet.of(Color.BLACK,Color.GREEN);
        System.out.println("指定Color.BLACK和Color.GREEN元素:"+enumSet5.toString());

        System.out.println("-----------------------------------");

        //复制enumSet5容器的数据作为初始化数据
        EnumSet<Color> enumSet6= EnumSet.copyOf(enumSet5);
        System.out.println("enumSet6:"+enumSet6.toString());

        System.out.println("-----------------------------------");

        List<Color> list = new ArrayList<Color>();
        list.add(Color.BLACK);
        list.add(Color.BLACK);//重复元素
        list.add(Color.RED);
        list.add(Color.BLUE);
        System.out.println("list:"+list.toString());

        //使用copyOf(Collection<E> c)
        EnumSet enumSet7=EnumSet.copyOf(list);
        System.out.println("enumSet7:"+enumSet7.toString());

        /**
         输出结果:
         添加前:[]
         添加后:[GREEN, RED, BLUE, BLACK, YELLOW]
         -----------------------------------
         allOf直接填充:[GREEN, RED, BLUE, BLACK, YELLOW]
         -----------------------------------
         指定初始化范围:[BLACK, YELLOW]
         -----------------------------------
         指定补集:[GREEN, RED, BLUE]
         -----------------------------------
         指定Color.BLACK元素:[BLACK]
         指定Color.BLACK和Color.GREEN元素:[GREEN, BLACK]
         -----------------------------------
         enumSet6:[GREEN, BLACK]
         -----------------------------------
         list:[BLACK, BLACK, RED, BLUE]
         enumSet7:[RED, BLUE, BLACK]
         */
    }

}

noneOf(Class<E> elementType)静态方法,主要用于创建一个空的EnumSet集合,传递参数elementType代表的是枚举类型的类型信息,即Class对象。EnumSet<E> allOf(Class<E> elementType)静态方法则是创建一个填充了elementType类型所代表的所有枚举实例,奇怪的是EnumSet提供了多个重载形式的of方法,最后一个接受的的是可变参数,其他重载方法则是固定参数个数,EnumSet之所以这样设计是因为可变参数的运行效率低一些,所有在参数数据不多的情况下,强烈不建议使用传递参数为可变参数的of方法,即EnumSet<E> of(E first, E... rest),其他方法就不分析了,看代码演示即可。至于EnumSet的操作方法,则与set集合是一样的,可以看API即可这也不过多说明。什么时候使用EnumSet比较恰当的,事实上当需要进行位域运算,就可以使用EnumSet提到位域,如下:

public class EnumSetDemo {
    //定义位域变量
    public static final int TYPE_ONE = 1 << 0 ; //1
    public static final int TYPE_TWO = 1 << 1 ; //2
    public static final int TYPE_THREE = 1 << 2 ; //4
    public static final int TYPE_FOUR = 1 << 3 ; //8
    public static void main(String[] args){
        //位域运算
        int type= TYPE_ONE | TYPE_TWO | TYPE_THREE |TYPE_FOUR;
    }
}

诸如上述情况,我们都可以将上述的类型定义成枚举然后采用EnumSet来装载,进行各种操作,这样不仅不用手动编写太多冗余代码,而且使用EnumSet集合进行操作也将使代码更加简洁明了。

enum Type{
    TYPE_ONE,TYPE_TWO,TYPE_THREE,TYPE_FOUR 
}

public class EnumSetDemo {
    public static void main(String[] args){
    EnumSet set =EnumSet.of(Type.TYPE_ONE,Type.TYPE_FOUR);
    }
}

其实博主认为EnumSet最有价值的是其内部实现原理,采用的是位向量,它体现出来的是一种高效的数据处理方式,这点很值得我们去学习它。

EnumSet实现原理剖析

关于EnumSet实现原理可能会有点烧脑,内部执行几乎都是位运算,博主将尽力使用图片来分析,协助大家理解。

理解位向量

在分析EnumSet前有必要先了解以下位向量,顾名思义位向量就是用一个bit位(0或1)标记一个元素的状态,用一组bit位表示一个集合的状态,而每个位对应一个元素,每个bit位的状态只可能有两种,即0或1。位向量能表示的元素个数与向量的bit位长度有关,如一个int类型能表示32个元素,而一个long类型则可以表示64个元素,对于EnumSet而言采用的就long类型或者long类型数组。比如现在有一个文件中的数据,该文件存储了N=1000000个无序的整数,需要把这些整数读取到内存并排序再重新写回文件中,该如何解决?最简单的方式是用int类型来存储每个数,并把其存入到数组(int a[m])中,再进行排序,但是这种方式将会导致存储空间异常大,对数据操作起来效率也能成问题,那有没更高效的方式呢?的确是有的,那就是运用位向量,我们知道一个int型的数有4个字节,也就是32位,那么我们可以用N/32个int型数组来表示这N个数:

a[0]表示第1~32个数(0~31)
a[1]表示第33~64个数(32~63)
a[2]表示第65~96个数(64~95)
...... 以此类推

这样,每当输入一个数字m,我们应该先找到该数字在数组的第?个元素,也就是a[?],然后再确定在这个元素的第几个bit位,找到后设置为1,代表存在该数字。举个例子来说,比如输入40,那么40/32为1余8,则应该将a[1]元素值的第9个bit位置为1(1的二进制左移8位后就是第9个位置),表示该数字存在,40数字的表示原理图过程如下:

大概明白了位向量表示方式后,上述过程的计算方式,通过以下方式可以计算该数存储在数组的第?个元素和元素中第?个bit位置,为了演示方便,我们这里假设整第?个元素中的?为P,余值设置S


//m 除以 2^n 则商(P)表示为 m >> n 
//等同于 m / 2^5 取整数 即:40 / 32 = 1 ,那么P=1就是数组中第2个元素,即a[1]

//位操作过程如下,40的二进制
00000000 00000000 00000000 00101000

//右移5位即 n=5 , m >> 5 ,即结果转为10进制就是P=1
00000000 00000000 00000000 00000001

在这里我们使用的int类型,即32位,所有2^5=32,因此n=5,由此计算出 P的值代表的是数组的第 P 个元素,接着利用下述方式计算出余数(S),以此设置该元素值的第(S+1)个bit位为1

//m 除以2^n 的余数(S)表示为 m & (2^n-1) 
//等同于: m % 2^5 取余数 即:40 % 32 = 8

//m=40的二进制
00000000 00000000 00000000 00101000

//2^n-1(31)的二进制
00000000 00000000 00000000 00011111

// m & (2^n-1) 即40与31进行与操作得出余数 即 S=8
00000000 00000000 00000000 00001000 

//下面是将a[1]元素值的第(8+1)个bit设置为1,为什么是(8+1)不是8?因为1左移8位就在第9个bit位了,过程如下:

//1的二进制如下:
00000000 00000000 00000000 00000001

//1 << 8 利用余数8对1进行左移动
00000000 00000000 00000001 0000000 

//然后再与a[1]执行或操作后就可以将对应的bit位设置为1
//a[P] |= 1 << S 见下述java实现的代码

通过上述二进制位运算过程(关于位运算可以看博主的另一篇博文~java位运算)就可以计算出整数部分P和余数部分S,并成功设置bit位为1,现在利用java来实现这个运算过程如下:

//定义变量
private int[] a; //数组存储元素的数组
private int BIT_LENGTH = 32;//默认使用int类型
private int P; //整数部分
private int S; //余数
private int MASK =  0x1F;// 2^5 - 1
private int SHIFT = 5; // 2^n SHIFT=n=5 表示2^5=32 即bit位长度32

计算代码

/**
 * 置位操作,添加操作
 * @param i
 */
public void set(int i){
     P = i >> SHIFT; //结果等同  P = i / BIT_LENGTH; 取整数 ①
     S = i & MASK;   //结果等同  S = i % BIT_LENGTH; 取余数 ②

     a[P] |= 1 << S;  //赋值设置该元素bit位为1               ③
     //将int型变量j的第k个比特位设置为1, 即j=j|(1<<k),上述3句合并为一句
     //a[i >> SHIFT ] |= (1 << (i & MASK));               ④
 }

计算出P和S后,就可以进行赋值了,其中 a[P]代表数组中第P个元素,a[P] |= 1 << S 整句意思是把a[P]元素的第S+1位设置为1,注意从低位到高位设置,即从右到左,①②③合并为④,代码将更佳简洁。当然有添加操作,那么就会有删除操作,删除操作过程与添加类似,只不过删除是把相对应的bit位设置0,代表不存在该数值。

/**
* 置0操作,相当于清除元素
* @param i
*/
public void clear(int i){
   P =  i >> SHIFT; //计算位于数组中第?个元素 P = i / BIT_LENGTH;
   S =  i & MASK;   //计算余数  S = i % BIT_LENGTH;
   //把a[P]元素的第S+1个(从低位到高位)bit位设置为0
   a[P] &= ~(1 << S);

   //更优写法
   //将int型变量j的第k个比特位设置为0,即j= j&~(1<<k)
   //a[i>>SHIFT] &= ~(1<<(i &MASK));
}

与添加唯一不同的是,计算出余数S,利用1左移S位,再取反(~)操作,最后进行与(&)操作,即将a[P]元素的第S+1个(从低位到高位)bit位设置为0,表示删除该数字,这个计算过程大家可以自行推算一下。这就是位向量表示法的添加和清除方法,然后我们可以利用下述的get方法判断某个bit是否存在某个数字:

/**
 * 读取操作,返回1代表该bit位有值,返回0代表该bit位没值
 * @param i
 * @return
 */
public int get(int i){
    //a[i>>SHIFT] & (1<<(i&MASK));
    P = i >> SHIFT;
    S = i &  MASK;
    return Integer.bitCount(a[P] & (1 << S));
}

其中Integer.bitCount()是返回指定 int 值的二进制补码(计算机数字的二进制表示法都是使用补码表示的)表示形式的 1 位的数量。位向量运算整体代码实现如下:

package com.zejian;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * Created by zejian on 2017/5/13.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 * 位向量存储数据
 */
public class BitVetory {
    private int count;
    private int[] a; //数组
    private int BIT_LENGTH = 32;//默认使用int类型
    private int P; //整数部分
    private int S; //余数
    private int MASK =  0x1F;// 2^5 - 1
    private int SHIFT = 5; // 2^n SHIFT=n=5 表示2^5=32 即bit位长度32

    /**
     * 初始化位向量
     * @param count
     */
    public BitVetory(int count) {
        this.count = count;
        a = new int[(count-1)/BIT_LENGTH + 1];
        init();
    }

    /**
     * 将数组中元素bit位设置为0
     */
    public void init(){
        for (int i = 0; i < count; i++) {
            clear(i);
        }
    }

    /**
     * 获取排序后的数组
     * @return
     */
    public List<Integer> getSortedArray(){
        List<Integer> sortedArray = new ArrayList<Integer>();

        for (int i = 0; i < count; i++) {
            if (get(i) == 1) {//判断i是否存在
                sortedArray.add(i);
            }
        }
        return sortedArray;
    }
    /**
     * 置位操作,设置元素
     * @param i
     */
    public void set(int i){
        P = i >> SHIFT; //P = i / BIT_LENGTH; 取整数
        S = i & MASK; //S = i % BIT_LENGTH; 取余数
        a[P] |= 1 << S;

        //将int型变量j的第k个比特位设置为1, 即j=j|(1<<k),上述3句合并为一句
        //a[i >> SHIFT ] |= (1 << (i & MASK));
    }

    /**
     * 置0操作,相当于清除元素
     * @param i
     */
    public void clear(int i){
        P =  i >> SHIFT; //计算位于数组中第?个元素 P = i / BIT_LENGTH;
        S =  i & MASK;   //计算余数  S = i % BIT_LENGTH;
        a[P] &= ~(1 << S);

        //更优写法
        //将int型变量j的第k个比特位设置为0,即j= j&~(1<<k)
        //a[i>>SHIFT] &= ~(1<<(i &MASK));
    }

    /**
     * 读取操作,返回1代表该bit位有值,返回0代表该bit位没值
     * @param i
     * @return
     */
    public int get(int i){
        //a[i>>SHIFT] & (1<<(i&MASK));
        P = i >> SHIFT;
        S = i &  MASK;
        return Integer.bitCount(a[P] & (1 << S));
    }

    //测试
    public static void main(String[] args) {
        int count = 25;
        List<Integer> randoms = getRandomsList(count);
        System.out.println("排序前:");

        BitVetory bitVetory = new BitVetory(count);
        for (Integer e : randoms) {
            System.out.print(e+",");
            bitVetory.set(e);
        }

        List<Integer> sortedArray = bitVetory.getSortedArray();
        System.out.println();
        System.out.println("排序后:");
        for (Integer e : sortedArray) {
            System.out.print(e+",");
        }

        /**
         输出结果:
         排序前:
         6,3,20,10,18,15,19,16,13,4,21,22,24,2,14,5,12,7,23,8,1,17,9,11,
         排序后:
         1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,
         */
    }

    private static List<Integer> getRandomsList(int count) {
        Random random = new Random();

        List<Integer> randomsList = new ArrayList<Integer>();
        while(randomsList.size() < (count - 1)){
            int element = random.nextInt(count - 1) + 1;//element ∈  [1,count)
            if (!randomsList.contains(element)) {
                randomsList.add(element);
            }
        }
        return randomsList;
    }
}

EnumSet原理

有前面位向量的分析,对于了解EnumSet的实现原理就相对简单些了,EnumSet内部使用的位向量实现的,前面我们说过EnumSet是一个抽象类,事实上它存在两个子类,RegularEnumSet和JumboEnumSet。RegularEnumSet使用一个long类型的变量作为位向量,long类型的位长度是64,因此可以存储64个枚举实例的标志位,一般情况下是够用的了,而JumboEnumSet使用一个long类型的数组,当枚举个数超过64时,就会采用long数组的方式存储。先看看EnumSet内部的数据结构:

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable
{
    //表示枚举类型
    final Class<E> elementType;
    //存储该类型信息所表示的所有可能的枚举实例
    final Enum<?>[] universe;
    //..........
}

EnumSet中有两个变量,一个elementType用于表示枚举的类型信息,universe是数组类型,存储该类型信息所表示的所有可能的枚举实例,EnumSet是抽象类,因此具体的实现是由子类完成的,下面看看noneOf(Class<E> elementType)静态构建方法

  public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        //根据EnumMap中的一样,获取所有可能的枚举实例
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            //枚举个数小于64,创建RegularEnumSet
            return new RegularEnumSet<>(elementType, universe);
        else
            //否则创建JumboEnumSet
            return new JumboEnumSet<>(elementType, universe);
    }

从源码可以看出如果枚举值个数小于等于64,则静态工厂方法中创建的就是RegularEnumSet,否则大于64的话就创建JumboEnumSet。无论是RegularEnumSet还是JumboEnumSet,其构造函数内部都间接调用了EnumSet的构造函数,因此最终的elementType和universe都传递给了父类EnumSet的内部变量。如下:

//RegularEnumSet构造
RegularEnumSet(Class<E>elementType, Enum<?>[] universe) {
      super(elementType, universe);
  }

//JumboEnumSet构造
JumboEnumSet(Class<E>elementType, Enum<?>[] universe) {
      super(elementType, universe);
      elements = new long[(universe.length + 63) >>> 6];
  }

在RegularEnumSet类和JumboEnumSet类中都存在一个elements变量,用于记录位向量的操作,

//RegularEnumSet
class RegularEnumSet<E extends Enum<E>> extends EnumSet<E> {
    private static final long serialVersionUID = 3411599620347842686L;
    //通过long类型的elements记录位向量的操作
    private long elements = 0L;
    //.......
}

//对于JumboEnumSet则是:
class JumboEnumSet<E extends Enum<E>> extends EnumSet<E> {
    private static final long serialVersionUID = 334349849919042784L;
    //通过long数组类型的elements记录位向量
    private long elements[];
     //表示集合大小
    private int size = 0;

    //.............
    }

在RegularEnumSet中elements是一个long类型的变量,共有64个bit位,因此可以记录64个枚举常量,当枚举常量的数量超过64个时,将使用JumboEnumSet,elements在该类中是一个long型的数组,每个数组元素都可以存储64个枚举常量,这个过程其实与前面位向量的分析是同样的道理,只不过前面使用的是32位的int类型,这里使用的是64位的long类型罢了。接着我们看看EnumSet是如何添加数据的,RegularEnumSet中的add实现如下

public boolean add(E e) {
    //检测是否为枚举类型
    typeCheck(e);
    //记录旧elements
    long oldElements = elements;
    //执行位向量操作,是不是很熟悉?
    //数组版:a[i >> SHIFT ] |= (1 << (i & MASK))
    elements |= (1L << ((Enum)e).ordinal());
    return elements != oldElements;
}

关于elements |= (1L << ((Enum)e).ordinal());这句跟我们前面分析位向量操作是相同的原理,只不过前面分析的是数组类型实现,这里用的long类型单一变量实现,((Enum)e).ordinal()通过该语句获取要添加的枚举实例的序号,然后通过1左移再与 long类型的elements进行或操作,就可以把对应位置上的bit设置为1了,也就代表该枚举实例存在。图示演示过程如下,注意universe数组在EnumSet创建时就初始化并填充了所有可能的枚举实例,而elements值的第n个bit位1时代表枚举存在,而获取的则是从universe数组中的第n个元素值。

这就是枚举实例的添加过程和获取原理。而对于JumboEnumSet的add实现则是如下:

public boolean add(E e) {
    typeCheck(e);
    //计算ordinal值
    int eOrdinal = e.ordinal();
    int eWordNum = eOrdinal >>> 6;

    long oldElements = elements[eWordNum];
    //与前面分析的位向量相同:a[i >> SHIFT ] |= (1 << (i & MASK))
    elements[eWordNum] |= (1L << eOrdinal);
    boolean result = (elements[eWordNum] != oldElements);
    if (result)
        size++;
    return result;
}

关于JumboEnumSet的add实现与RegularEnumSet区别是一个是long数组类型,一个long变量,运算原理相同,数组的位向量运算与前面分析的是相同的,这里不再分析。接着看看如何删除元素

//RegularEnumSet类实现
public boolean remove(Object e) {
    if (e == null)
        return false;
    Class eClass = e.getClass();
    if (eClass != elementType && eClass.getSuperclass() != elementType)
        return false;

    long oldElements = elements;
    //将int型变量j的第k个比特位设置为0,即j= j&~(1<<k)
    //数组类型:a[i>>SHIFT] &= ~(1<<(i &MASK));

    elements &= ~(1L << ((Enum)e).ordinal());//long遍历类型操作
    return elements != oldElements;
}


//JumboEnumSet类的remove实现
public boolean remove(Object e) {
        if (e == null)
            return false;
        Class<?> eClass = e.getClass();
        if (eClass != elementType && eClass.getSuperclass() != elementType)
            return false;
        int eOrdinal = ((Enum<?>)e).ordinal();
        int eWordNum = eOrdinal >>> 6;

        long oldElements = elements[eWordNum];
        //与a[i>>SHIFT] &= ~(1<<(i &MASK));相同
        elements[eWordNum] &= ~(1L << eOrdinal);
        boolean result = (elements[eWordNum] != oldElements);
        if (result)
            size--;
        return result;
    }

删除remove的实现,跟位向量的清空操作是同样的实现原理,如下:

至于JumboEnumSet的实现原理也是类似的,这里不再重复。下面为了简洁起见,我们以RegularEnumSet类的实现作为源码分析,毕竟JumboEnumSet的内部实现原理可以说跟前面分析过的位向量几乎一样。o~,看看如何判断是否包含某个元素

public boolean contains(Object e) {
    if (e == null)
        return false;
    Class eClass = e.getClass();
    if (eClass != elementType && eClass.getSuperclass() != elementType)
        return false;
    //先左移再按&操作
    return (elements & (1L << ((Enum)e).ordinal())) != 0;
}

public boolean containsAll(Collection<?> c) {
    if (!(c instanceof RegularEnumSet))
        return super.containsAll(c);

    RegularEnumSet<?> es = (RegularEnumSet<?>)c;
    if (es.elementType != elementType)
        return es.isEmpty();
    //~elements取反相当于elements补集,再与es.elements进行&操作,如果为0,
    //就说明elements补集与es.elements没有交集,也就是es.elements是elements的子集
    return (es.elements & ~elements) == 0;
}

对于contains(Object e) 方法,先左移再按位与操作,不为0,则表示包含该元素,跟位向量的get操作实现原理类似,这个比较简单。对于containsAll(Collection<?> c)则可能比较难懂,这里分析一下,elements变量(long类型)标记EnumSet集合中已存在元素的bit位,如果bit位为1则说明存在枚举实例,为0则不存在,现在执行~elements 操作后 则说明~elements是elements的补集,那么只要传递进来的es.elements与补集~elements 执行&操作为0,那么就可以证明es.elements与补集~elements 没有交集的可能,也就是说es.elements只能是elements的子集,这样也就可以判断出当前EnumSet集合中包含传递进来的集合c了,借着下图协助理解:

图中,elements代表A,es.elements代表S,~elements就是求A的补集,(es.elements & ~elements) == 0就是在验证A’∩B是不是空集,即S是否为A的子集。接着看retainAll方法,求两个集合交集

public boolean retainAll(Collection<?> c) {
        if (!(c instanceof RegularEnumSet))
            return super.retainAll(c);

        RegularEnumSet<?> es = (RegularEnumSet<?>)c;
        if (es.elementType != elementType) {
            boolean changed = (elements != 0);
            elements = 0;
            return changed;
        }

        long oldElements = elements;
        //执行与操作,求交集,比较简单
        elements &= es.elements;
        return elements != oldElements;
    }

最后来看看迭代器是如何取值的

 public Iterator<E> iterator() {
        return new EnumSetIterator<>();
    }

    private class EnumSetIterator<E extends Enum<E>> implements Iterator<E> {
        //记录elements
        long unseen;

        //记录最后一个返回值
        long lastReturned = 0;

        EnumSetIterator() {
            unseen = elements;
        }

        public boolean hasNext() {
            return unseen != 0;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (unseen == 0)
                throw new NoSuchElementException();
            //取值过程,先与本身负执行&操作得出的就是二进制低位开始的第一个1的数值大小
            lastReturned = unseen & -unseen;
            //取值后减去已取得lastReturned
            unseen -= lastReturned;
            //返回在指定 long 值的二进制补码表示形式中最低位(最右边)的 1 位之后的零位的数量
            return (E) universe[Long.numberOfTrailingZeros(lastReturned)];
        }

        public void remove() {
            if (lastReturned == 0)
                throw new IllegalStateException();
            elements &= ~lastReturned;
            lastReturned = 0;
        }
    }

比较晦涩的应该是

//取值过程,先与本身负执行&操作得出的就是二进制低位开始的第一个1的数值大小
lastReturned = unseen & -unseen; 
//取值后减去已取得lastReturned
unseen -= lastReturned;
return (E) universe[Long.numberOfTrailingZeros(lastReturned)];

我们通过原理图来协助理解,现在假设集合中已保存所有可能的枚举实例变量,我们需要把它们遍历展示出来,下面的第一个枚举元素的获取过程,显然通过unseen & -unseen;操作,我们可以获取到二进制低位开始的第一个1的数值,该计算的结果是要么全部都是0,要么就只有一个1,然后赋值给lastReturned,通过Long.numberOfTrailingZeros(lastReturned)获取到该bit为1在64位的long类型中的位置,即从低位算起的第几个bit,如图,该bit的位置恰好是低位的第1个bit位置,也就指明了universe数组的第一个元素就是要获取的枚举变量。执行unseen -= lastReturned;后继续进行第2个元素的遍历,依次类推遍历出所有值,这就是EnumSet的取值过程,真正存储枚举变量的是universe数组,而通过long类型变量的bit位的0或1表示存储该枚举变量在universe数组的那个位置,这样做的好处是任何操作都是执行long类型变量的bit位操作,这样执行效率将特别高,毕竟是二进制直接执行,只有最终获取值时才会操作到数组universe。

ok~,到这关于EnumSet的实现原理主要部分我们就分析完了,其内部使用位向量,存储结构很简洁,节省空间,大部分操作都是按位运算,直接操作二进制数据,因此效率极高。当然通过前面的分析,我们也掌握位向量的运算原理。好~,关于java枚举,我们暂时聊到这。

 

参考资料 《Thinking in Java》 And 《Effective Java》

剖析基于并发AQS的共享锁的实现(基于信号量Semaphore)

信号量-Semaphore

Semaphore共享锁的使用

信号量(Semaphore),又被称为信号灯,在多线程环境下用于协调各个线程, 以保证它们能够正确、合理的使用公共资源。信号量维护了一个许可集,我们在初始化Semaphore时需要为这个许可集传入一个数量值,该数量值代表同一时间能访问共享资源的线程数量。线程可以通过acquire()方法获取到一个许可,然后对共享资源进行操作,注意如果许可集已分配完了,那么线程将进入等待状态,直到其他线程释放许可才有机会再获取许可,线程释放一个许可通过release()方法完成。下面通过一个简单案例来演示

public class SemaphoreTest {

    public static void main(String[] args) {  
       // 线程池 
       ExecutorService exec = Executors.newCachedThreadPool();  
       //设置信号量同时执行的线程数是5 
       final Semaphore semp = new Semaphore(5);  
       // 模拟20个客户端访问 
       for (int index = 0; index < 20; index++) {
           final int NO = index;  
           Runnable run = new Runnable() {  
               public void run() {  
                   try {  
                       //使用acquire()获取锁 
                       semp.acquire();  
                       System.out.println("Accessing: " + NO);  
                       //睡眠1秒
                       Thread.sleep(1000);  

                   } catch (InterruptedException e) {  
                   }  finally {
                        //使用完成释放锁 
                        semp.release();
                    }
               }  
           };  
           exec.execute(run);  
       }  
       // 退出线程池 
       exec.shutdown();  
   }  
}

上述代码中,在创建Semaphore时初始化5个许可,这也就意味着同一个时间点允许5个线程进行共享资源访问,使用acquire()方法为每个线程获取许可,并进行休眠1秒,如果5个许可已被分配完,新到来的线程将进入等待状态。如果线程顺利完成操作将通过release()方法释放许可,我们执行代码,可以发现每隔1秒几乎同一时间出现5条线程访,如下图

Semaphore实现互斥锁

在初始化信号量时传入1,使得它在使用时最多只有一个可用的许可,从而可用作一个相互排斥的锁。这通常也称为二进制信号量,因为它只能有两种状态:一个可用的许可或零个可用的许可。按此方式使用时,二进制信号量具有某种属性(与很多 Lock 实现不同),即可以由线程释放“锁”,而不是由所有者(因为信号量没有所有权的概念)。下面简单看一个Semaphore实现互斥功能的例子

/**
 * Created by zejian on 2017/7/30.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class SemaphoreMutex {
    //初始化为1,互斥信号量
    private final static Semaphore mutex = new Semaphore(1);

    public static void main(String[] args){
        ExecutorService pools = Executors.newCachedThreadPool();

        for (int i=0 ; i < 10;i++){
            final int index = i;
           Runnable run = new Runnable() {
               @Override
               public void run() {
                   try {
                       mutex.acquire();
                       System.out.println(String.format("[Thread-%s]任务id --- %s",Thread.currentThread().getId(),index));
                       TimeUnit.SECONDS.sleep(1);

                   } catch (InterruptedException e) {
                       e.printStackTrace();
                   } finally {
                        //使用完成释放锁 
                       mutex.release();
                       System.out.println("-----------release");
                    }
               }
           };
            pools.execute(run);
        }
        pools.shutdown();
    }
}

创建一个数量为1的互斥信号量Semaphore,然后并发执行10个线程,在线程中利用Semaphore控制线程的并发执行,因为信号量数值只有1,因此每次只能一条线程执行,其他线程进入等待状态,如下

除了获取许可的aquire()方法和release()方法外,还提供了其他方法如下


//构造方法摘要
//创建具有给定的许可数和非公平的公平设置的Semaphore。
Semaphore(int permits) 

//创建具有给定的许可数和给定的公平设置的Semaphore,true即为公平锁     
Semaphore(int permits, boolean fair) 

//从此信号量中获取许可,不可中断
void acquireUninterruptibly() 

//返回此信号量中当前可用的许可数。      
int availablePermits() 

//获取并返回立即可用的所有许可。    
int drainPermits() 

//返回一个 collection,包含可能等待获取的线程。       
protected  Collection<Thread> getQueuedThreads();

//返回正在等待获取的线程的估计数目。        
int getQueueLength() 

//查询是否有线程正在等待获取。       
boolean hasQueuedThreads() 

//如果此信号量的公平设置为 true,则返回 true。          
boolean isFair() 

//仅在调用时此信号量存在一个可用许可,才从信号量获取许可。          
boolean tryAcquire() 

//如果在给定的等待时间内,此信号量有可用的许可并且当前线程未被中断,则从此信号量获取一个许可。        
boolean tryAcquire(long timeout, TimeUnit unit) 

Semaphore中共享锁的实现

Semaphore的实现内部原理概要

在深入分析Semaphore的内部原理前先看看一张类图结构

根据类图可知,信号量Semaphore的类结构与上一篇中分析的ReetrantLock的类结构几乎如出一辙。Semaphore内部同样存在继承自AQS的内部类Sync以及继承自Sync的公平锁(FairSync)和非公平锁(NofairSync),从这点也足以说明Semaphore的内部实现原理也是基于AQS并发组件的,在上一篇文章中,我们提到过,AQS是基础组件,只负责核心并发操作,如加入或维护同步队列,控制同步状态,等,而具体的加锁和解锁操作交由子类完成,因此子类Semaphore共享锁的获取与释放需要自己实现,这两个方法分别是获取锁的tryAcquireShared(int arg)方法和释放锁的tryReleaseShared(int arg)方法,这点从Semaphore的内部结构完全可以看出来

从图可知,Semaphore的内部类公平锁(FairSync)和非公平锁(NoFairSync)各自实现不同的获取锁方法即tryAcquireShared(int arg),毕竟公平锁和非公平锁的获取稍后不同,而释放锁tryReleaseShared(int arg)的操作交由Sync实现,因为释放操作都是相同的,因此放在父类Sync中实现当然是最好的。需要明白的是,我们在调用Semaphore的方法时,其内部则是通过间接调用其内部类或AQS执行的。下面我们就从Semaphore的源码入手分析共享锁实现原理,这里先从非公平锁入手。

非公平锁中的共享锁

Semaphore的构造函数如下

//默认创建公平锁,permits指定同一时间访问共享资源的线程数
public Semaphore(int permits) {
        sync = new NonfairSync(permits);
    }

public Semaphore(int permits, boolean fair) {
     sync = fair ? new FairSync(permits) : new NonfairSync(permits);
 }

显然我们通过默认构造函数创建时,诞生的就是非公平锁,

static final class NonfairSync extends Sync {
    NonfairSync(int permits) {
          super(permits);
    }
   //调用父类Sync的nonfairTryAcquireShared
   protected int tryAcquireShared(int acquires) {
       return nonfairTryAcquireShared(acquires);
   }
}

显然传入的许可数permits传递给了父类,最终会传给AQS中的state变量,也就是同步状态的变量,如下

//AQS中控制同步状态的state变量
public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer {

    private volatile int state;

    protected final int getState() {
        return state;
    }

    protected final void setState(int newState) {
        state = newState;
    }

    //对state变量进行CAS 操作
    protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

}

从这点可知,Semaphore的初始化值也就是state的初始化值。当我们调用Semaphore的acquire()方法后,执行过程是这样的,当一个线程请求到来时,如果state值代表的许可数足够使用,那么请求线程将会获得同步状态即对共享资源的访问权,并更新state的值(一般是对state值减1),但如果state值代表的许可数已为0,则请求线程将无法获取同步状态,线程将被加入到同步队列并阻塞,直到其他线程释放同步状态(一般是对state值加1)才可能获取对共享资源的访问权。调用Semaphore的acquire()方法后将会调用到AQS的acquireSharedInterruptibly()如下

//Semaphore的acquire()
public void acquire() throws InterruptedException {
      sync.acquireSharedInterruptibly(1);
  }

/**
*  注意Sync类继承自AQS
*  AQS的acquireSharedInterruptibly()方法
*/ 
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    //判断是否中断请求
    if (Thread.interrupted())
        throw new InterruptedException();
    //如果tryAcquireShared(arg)不小于0,则线程获取同步状态成功
    if (tryAcquireShared(arg) < 0)
        //未获取成功加入同步队列等待
        doAcquireSharedInterruptibly(arg);
}

从方法名就可以看出该方法是可以中断的,也就是说Semaphore的acquire()方法也是可中断的。在acquireSharedInterruptibly()方法内部先进行了线程中断的判断,如果没有中断,那么先尝试调用tryAcquireShared(arg)方法获取同步状态,如果获取成功,那么方法执行结束,如果获取失败调用doAcquireSharedInterruptibly(arg);方法加入同步队列等待。这里的tryAcquireShared(arg)是个模板方法,AQS内部没有提供具体实现,由子类实现,也就是有Semaphore内部自己实现,该方法在Semaphore内部非公平锁的实现如下

//Semaphore中非公平锁NonfairSync的tryAcquireShared()
protected int tryAcquireShared(int acquires) {
    //调用了父类Sync中的实现方法
    return nonfairTryAcquireShared(acquires);
}

//Syn类中
abstract static class Sync extends AbstractQueuedSynchronizer {

    final int nonfairTryAcquireShared(int acquires) {
         //使用死循环
         for (;;) {
             int available = getState();
             int remaining = available - acquires;
             //判断信号量是否已小于0或者CAS执行是否成功
             if (remaining < 0 ||
                 compareAndSetState(available, remaining))
                 return remaining;
         }
     }
}

nonfairTryAcquireShared(int acquires)方法内部,先获取state的值,并执行减法操作,得到remaining值,如果remaining不小于0,那么线程获取同步状态成功,可访问共享资源,并更新state的值,如果remaining大于0,那么线程获取同步状态失败,将被加入同步队列(通过doAcquireSharedInterruptibly(arg)),注意Semaphore的acquire()可能存在并发操作,因此nonfairTryAcquireShared()方法体内部采用无锁(CAS)并发的操作保证对state值修改的安全性。如何尝试获取同步状态失败,那么将会执行doAcquireSharedInterruptibly(int arg)方法

private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
     //创建共享模式的结点Node.SHARED,并加入同步队列
   final Node node = addWaiter(Node.SHARED);
     boolean failed = true;
     try {
         //进入自旋操作
         for (;;) {
             final Node p = node.predecessor();
             //判断前驱结点是否为head
             if (p == head) {
                 //尝试获取同步状态
                 int r = tryAcquireShared(arg);
                 //如果r>0 说明获取同步状态成功
                 if (r >= 0) {
                     //将当前线程结点设置为头结点并传播               
                     setHeadAndPropagate(node, r);
                     p.next = null; // help GC
                     failed = false;
                     return;
                 }
             }
           //调整同步队列中node结点的状态并判断是否应该被挂起
           //并判断是否需要被中断,如果中断直接抛出异常,当前结点请求也就结束
             if (shouldParkAfterFailedAcquire(p, node) &&
                 parkAndCheckInterrupt())
                 throw new InterruptedException();
         }
     } finally {
         if (failed)
             //结束该结点线程的请求
             cancelAcquire(node);
     }
    }

在方法中,由于当前线程没有获取同步状态,因此创建一个共享模式(Node.SHARED)的结点并通过addWaiter(Node.SHARED)加入同步队列,加入完成后,当前线程进入自旋状态,首先判断前驱结点是否为head,如果是,那么尝试获取同步状态并返回r值,如果r大于0,则说明获取同步状态成功,将当前线程设置为head并传播,传播指的是,同步状态剩余的许可数值不为0,通知后续结点继续获取同步状态,到此方法将会return结束,获取到同步状态的线程将会执行原定的任务。但如果前驱结点不为head或前驱结点为head并尝试获取同步状态失败,那么调用shouldParkAfterFailedAcquire(p, node)方法判断前驱结点的waitStatus值是否为SIGNAL并调整同步队列中的node结点状态,如果返回true,那么执行parkAndCheckInterrupt()方法,将当前线程挂起并返回是否中断线程的flag。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        //获取当前结点的等待状态
        int ws = pred.waitStatus;
        //如果为等待唤醒(SIGNAL)状态则返回true
        if (ws == Node.SIGNAL)
            return true;
        //如果ws>0 则说明是结束状态,
        //遍历前驱结点直到找到没有结束状态的结点
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            //如果ws小于0又不是SIGNAL状态,
            //则将其设置为SIGNAL状态,代表该结点的线程正在等待唤醒。
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

private final boolean parkAndCheckInterrupt() {
        //将当前线程挂起
        LockSupport.park(this);
        //获取线程中断状态,interrupted()是判断当前中断状态,
        //并非中断线程,因此可能true也可能false,并返回
        return Thread.interrupted();
}

到此,加入同步队列的整个过程完成。这里小结一下,在AQS中存在一个变量state,当我们创建Semaphore对象传入许可数值时,最终会赋值给state,state的数值代表同一个时刻可同时操作共享数据的线程数量,每当一个线程请求(如调用Semaphored的acquire()方法)获取同步状态成功,state的值将会减少1,直到state为0时,表示已没有可用的许可数,也就是对共享数据进行操作的线程数已达到最大值,其他后来线程将被阻塞,此时AQS内部会将线程封装成共享模式的Node结点,加入同步队列中等待并开启自旋操作。只有当持有对共享数据访问权限的线程执行完成任务并释放同步状态后,同步队列中的对于的结点线程才有可能获取同步状态并被唤醒执行同步操作,注意在同步队列中获取到同步状态的结点将被设置成head并清空相关线程数据(毕竟线程已在执行也就没有必要保存信息了),AQS通过这种方式便实现共享锁,简单模型如下

前面我们分析的是可中断的请求,与只对应的不可中的的请求(这些方法都存在于AQS,由子类Semaphore间接调用)如下

//不可中的acquireShared()
public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
}

private void doAcquireShared(int arg) {
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    //没有抛出异常中的。。。。
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

 private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);//设置为头结点
        /* 
         * 尝试去唤醒队列中的下一个节点,如果满足如下条件: 
         * 调用者明确表示"传递"(propagate > 0), 
         * 或者h.waitStatus为PROPAGATE(被上一个操作设置) 
         * 并且 
         *   下一个节点处于共享模式或者为null。 
         * 
         * 这两项检查中的保守主义可能会导致不必要的唤醒,但只有在有
         * 有在多个线程争取获得/释放同步状态时才会发生,所以大多
         * 数情况下会立马获得需要的信号
         */  
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
            //唤醒后继节点,因为是共享模式,所以允许多个线程同时获取同步状态
                doReleaseShared();
        }
    }

显然与前面带中断请求doAcquireSharedInterruptibly(int arg)方法不同的是少线程中断的判断以及异常抛出,其他操作都一样,关于doReleaseShared(),放后面分析。ok~,了解完请求同步状态的过程,我们看看释放请求状态的过程,当每个线程执行完成任务将会释放同步状态,此时state值一般都会增加1。先从Semaphore的release()方法入手

//Semaphore的release()
public void release() {
       sync.releaseShared(1);
}

//调用到AQS中的releaseShared(int arg) 
public final boolean releaseShared(int arg) {
       //调用子类Semaphore实现的tryReleaseShared方法尝试释放同步状态
      if (tryReleaseShared(arg)) {
          doReleaseShared();
          return true;
      }
      return false;
  }

显然Semaphore间接调用了AQS中的releaseShared(int arg)方法,通过tryReleaseShared(arg)方法尝试释放同步状态,如果释放成功,那么将调用doReleaseShared()唤醒同步队列中后继结点的线程,tryReleaseShared(int releases)方法如下

//在Semaphore的内部类Sync中实现的
protected final boolean tryReleaseShared(int releases) {
       for (;;) {
              //获取当前state
             int current = getState();
             //释放状态state增加releases
             int next = current + releases;
             if (next < current) // overflow
                 throw new Error("Maximum permit count exceeded");
              //通过CAS更新state的值
             if (compareAndSetState(current, next))
                 return true;
         }
        }

逻辑很简单,释放同步状态,更新state的值,值得注意的是这里必须操作无锁操作,即for死循环和CAS操作来保证线程安全问题,因为可能存在多个线程同时释放同步状态的场景。释放成功后通过doReleaseShared()方法唤醒后继结点。

private void doReleaseShared() {
    /* 
     * 保证释放动作(向同步等待队列尾部)传递,即使没有其他正在进行的  
     * 请求或释放动作。如果头节点的后继节点需要唤醒,那么执行唤醒  
     * 动作;如果不需要,将头结点的等待状态设置为PROPAGATE保证   
     * 唤醒传递。另外,为了防止过程中有新节点进入(队列),这里必  
     * 需做循环,所以,和其他unparkSuccessor方法使用方式不一样  
     * 的是,如果(头结点)等待状态设置失败,重新检测。 
     */  
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            // 获取头节点对应的线程的状态
            int ws = h.waitStatus;
            // 如果头节点对应的线程是SIGNAL状态,则意味着头
            //结点的后继结点所对应的线程需要被unpark唤醒。
            if (ws == Node.SIGNAL) {
                // 修改头结点对应的线程状态设置为0。失败的话,则继续循环。
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;
                // 唤醒头结点h的后继结点所对应的线程
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 如果头结点发生变化,则继续循环。否则,退出循环。
        if (h == head)                   // loop if head changed
            break;
    }
}


//唤醒传入结点的后继结点对应的线程
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
      if (ws < 0)
          compareAndSetWaitStatus(node, ws, 0);
       //拿到后继结点
      Node s = node.next;
      if (s == null || s.waitStatus > 0) {
          s = 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);
    }

显然doReleaseShared()方法中通过调用unparkSuccessor(h)方法唤醒head的后继结点对应的线程。注意这里把head的状态设置为Node.PROPAGATE是为了保证唤醒传递,博主认为是可能同时存在多个线程并发争取资源,如果线程A已执行到doReleaseShared()方法中,正被唤醒后正准备替换head(实际上还没替换),而线程B又跑来请求资源,此时调用setHeadAndPropagate(Node node, int propagate)时,传入的propagate=0

 if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
            //唤醒后继节点,因为是共享模式,所以允许多个线程同时获取同步状态
                doReleaseShared();
        }

但为了保证持续唤醒后继结点的线程即doReleaseShared()方法被调用,可以把head的waitStatus设置为Node.PROPAGATE,这样就保证线程B也可以执行doReleaseShared()保证后续结点被唤醒或传播,注意doReleaseShared()可以同时被释放操作和获取操作调用,但目的都是为唤醒后继节点,因为是共享模式,所以允许多个线程同时获取同步状态。ok~,释放过程的分析到此完结,对于释放操作的过程还是相对简单些的,即尝试更新state值,更新成功调用doReleaseShared()方法唤醒后继结点对应的线程。

公平锁中的共享锁

事实上公平锁的中的共享模式实现除了在获取同步状态时与非公平锁不同外,其他基本一样,看看公平锁的实现

static final class FairSync extends Sync {
        FairSync(int permits) {
            super(permits);
        }

        protected int tryAcquireShared(int acquires) {
            for (;;) {
                //这里是重点,先判断队列中是否有结点再执行
                //同步状态获取。
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                    compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }

从代码中可以看出,与非公平锁tryAcquireShared(int acquires)方法实现的唯一不同是,在尝试获取同步状态前,先调用了hasQueuedPredecessors()方法判断同步队列中是否存在结点,如果存在则返回-1,即将线程加入同步队列等待。从而保证先到来的线程请求一定会先执行,也就是所谓的公平锁。至于其他操作,与前面分析的非公平锁一样。

小结

ok~,到此我们通过对Semaphore的内部实现原理分析后,对共享锁的实现有了基本的认识,即AQS中通过state值来控制对共享资源访问的线程数,每当线程请求同步状态成功,state值将会减1,如果超过限制数量的线程将被封装共享模式的Node结点加入同步队列等待,直到其他执行线程释放同步状态,才有机会获得执行权,而每个线程执行完成任务释放同步状态后,state值将会增加1,这就是共享锁的基本实现模型。至于公平锁与非公平锁的不同之处在于公平锁会在线程请求同步状态前,判断同步队列是否存在Node,如果存在就将请求线程封装成Node结点加入同步队列,从而保证每个线程获取同步状态都是先到先得的顺序执行的。非公平锁则是通过竞争的方式获取,不管同步队列是否存在Node结点,只有通过竞争获取就可以获取线程执行权。

深入剖析java并发之阻塞队列LinkedBlockingQueue与ArrayBlockingQueue

阻塞队列概要

阻塞队列与我们平常接触的普通队列(LinkedList或ArrayList等)的最大不同点,在于阻塞队列支出阻塞添加和阻塞删除方法。

  • 阻塞添加
    所谓的阻塞添加是指当阻塞队列元素已满时,队列会阻塞加入元素的线程,直队列元素不满时才重新唤醒线程执行元素加入操作。

  • 阻塞删除
    阻塞删除是指在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般都会返回被删除的元素)

由于Java中的阻塞队列接口BlockingQueue继承自Queue接口,因此先来看看阻塞队列接口为我们提供的主要方法

public interface BlockingQueue<E> extends Queue<E> {

	//将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量)
	//在成功时返回 true,如果此队列已满,则抛IllegalStateException。 
	boolean add(E e); 
	
	//将指定的元素插入到此队列的尾部(如果立即可行且不会超过该队列的容量) 
	// 将指定的元素插入此队列的尾部,如果该队列已满, 
	//则在到达指定的等待时间之前等待可用的空间,该方法可中断 
	boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; 
	
	//将指定的元素插入此队列的尾部,如果该队列已满,则一直等到(阻塞)。 
	void put(E e) throws InterruptedException; 
	
	//获取并移除此队列的头部,如果没有元素则等待(阻塞), 
	//直到有元素将唤醒等待线程执行该操作 
	E take() throws InterruptedException; 
	
	//获取并移除此队列的头部,在指定的等待时间前一直等到获取元素, //超过时间方法将结束
	E poll(long timeout, TimeUnit unit) throws InterruptedException; 
	
	//从此队列中移除指定元素的单个实例(如果存在)。 
	boolean remove(Object o); 
}

	//除了上述方法还有继承自Queue接口的方法 
	//获取但不移除此队列的头元素,没有则跑异常NoSuchElementException 
	E element(); 
	
	//获取但不移除此队列的头;如果此队列为空,则返回 null。 
	E peek(); 
	
	//获取并移除此队列的头,如果此队列为空,则返回 null。 
	E poll();

这里我们把上述操作进行分类

  • 插入方法

    • add(E e) : 添加成功返回true,失败抛IllegalStateException异常
    • offer(E e) : 成功返回 true,如果此队列已满,则返回 false。
    • put(E e) :将元素插入此队列的尾部,如果该队列已满,则一直阻塞
  • 删除方法:

    • remove(Object o) :移除指定元素,成功返回true,失败返回false
    • poll() : 获取并移除此队列的头元素,若队列为空,则返回 null
    • take():获取并移除此队列头元素,若没有元素则一直阻塞。
  • 检查方法

    • element() :获取但不移除此队列的头元素,没有元素则抛异常
    • peek() :获取但不移除此队列的头;若队列为空,则返回 null。

阻塞队列的对元素的增删查操作主要就是上述的三类方法,通常情况下我们都是通过这3类方法操作阻塞队列,了解完阻塞队列的基本方法后,下面我们将分析阻塞队列中的两个实现类ArrayBlockingQueue和LinkedBlockingQueue的简单使用和实现原理,其中实现原理是这篇文章重点分析的内容。
#ArrayBlockingQueue的基本使用
ArrayBlockingQueue 是一个用数组实现的有界阻塞队列,其内部按先进先出的原则对元素进行排序,其中put方法和take方法为添加和删除的阻塞方法,下面我们通过ArrayBlockingQueue队列实现一个生产者消费者的案例,通过该案例简单了解其使用方式

这里写代码片

代码比较简单, Consumer 消费者和 Producer 生产者,通过ArrayBlockingQueue 队列获取和添加元素,其中消费者调用了take()方法获取元素当队列没有元素就阻塞,生产者调用put()方法添加元素,当队列满时就阻塞,通过这种方式便实现生产者消费者模式。比直接使用等待唤醒机制或者Condition条件队列来得更加简单。执行代码,打印部分Log如下

package com.zejian.concurrencys.Queue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.TimeUnit;

/**
 * Created by wuzejian on 2017/8/13
 */
public class ArrayBlockingQueueDemo {
    private final static ArrayBlockingQueue<Apple> queue= new ArrayBlockingQueue<>(1);
    public static void main(String[] args){
        new Thread(new Producer(queue)).start();
        new Thread(new Producer(queue)).start();
        new Thread(new Consumer(queue)).start();
        new Thread(new Consumer(queue)).start();
    }
}

 class Apple {
    public Apple(){
    }
 }

/**
 * 生产者线程
 */
class Producer implements Runnable{
    private final ArrayBlockingQueue<Apple> mAbq;
    Producer(ArrayBlockingQueue<Apple> arrayBlockingQueue){
        this.mAbq = arrayBlockingQueue;
    }

    @Override
    public void run() {
        while (true) {
            Produce();
        }
    }

    private void Produce(){
        try {
            Apple apple = new Apple();
            mAbq.put(apple);
            System.out.println("生产:"+apple);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

/**
 * 消费者线程
 */
class Consumer implements Runnable{

    private ArrayBlockingQueue<Apple> mAbq;
    Consumer(ArrayBlockingQueue<Apple> arrayBlockingQueue){
        this.mAbq = arrayBlockingQueue;
    }

    @Override
    public void run() {
        while (true){
            try {
                TimeUnit.MILLISECONDS.sleep(1000);
                comsume();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void comsume() throws InterruptedException {
        Apple apple = mAbq.take();
        System.out.println("消费Apple="+apple);
    }
}
生产:com.zejian.concurrencys.Queue.Apple@109967f
消费Apple=com.zejian.concurrencys.Queue.Apple@109967f
生产:com.zejian.concurrencys.Queue.Apple@269a77
生产:com.zejian.concurrencys.Queue.Apple@1ce746e
消费Apple=com.zejian.concurrencys.Queue.Apple@269a77
消费Apple=com.zejian.concurrencys.Queue.Apple@1ce746e
........

有点需要注意的是ArrayBlockingQueue内部的阻塞队列是通过重入锁ReenterLock和Condition条件队列实现的,所以ArrayBlockingQueue中的元素存在公平访问与非公平访问的区别,对于公平访问队列,被阻塞的线程可以按照阻塞的先后顺序访问队列,即先阻塞的线程先访问队列。而非公平队列,当队列可用时,阻塞的线程将进入争夺访问资源的竞争中,也就是说谁先抢到谁就执行,没有固定的先后顺序。创建公平与非公平阻塞队列代码如下:

//默认非公平阻塞队列
ArrayBlockingQueue queue = new ArrayBlockingQueue(2);
//公平阻塞队列
ArrayBlockingQueue queue1 = new ArrayBlockingQueue(2,true);

//构造方法源码
public ArrayBlockingQueue(int capacity) {
     this(capacity, false);
 }

public ArrayBlockingQueue(int capacity, boolean fair) {
     if (capacity <= 0)
         throw new IllegalArgumentException();
     this.items = new Object[capacity];
     lock = new ReentrantLock(fair);
     notEmpty = lock.newCondition();
     notFull =  lock.newCondition();
 }

其他方法如下:

//自动移除此队列中的所有元素。
void clear() 

//如果此队列包含指定的元素,则返回 true。          
boolean contains(Object o) 

//移除此队列中所有可用的元素,并将它们添加到给定collection中。           
int drainTo(Collection<? super E> c) 

//最多从此队列中移除给定数量的可用元素,并将这些元素添加到给定collection 中。       
int drainTo(Collection<? super E> c, int maxElements) 

//返回在此队列中的元素上按适当顺序进行迭代的迭代器。         
Iterator<E> iterator() 

//返回队列还能添加元素的数量
int remainingCapacity() 

//返回此队列中元素的数量。      
int size() 

//返回一个按适当顺序包含此队列中所有元素的数组。
Object[] toArray() 

//返回一个按适当顺序包含此队列中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。      
<T> T[] toArray(T[] a)

#ArrayBlockingQueue的实现原理剖析
##ArrayBlockingQueue原理概要
ArrayBlockingQueue的内部是通过一个可重入锁ReentrantLock和两个Condition条件对象来实现阻塞,这里先看看其内部成员变量

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    /** 存储数据的数组 */
    final Object[] items;

    /**获取数据的索引,主要用于take,poll,peek,remove方法 */
    int takeIndex;

    /**添加数据的索引,主要用于 put, offer, or add 方法*/
    int putIndex;

    /** 队列元素的个数 */
    int count;


    /** 控制并非访问的锁 */
    final ReentrantLock lock;

    /**notEmpty条件对象,用于通知take方法队列已有元素,可执行获取操作 */
    private final Condition notEmpty;

    /**notFull条件对象,用于通知put方法队列未满,可执行添加操作 */
    private final Condition notFull;

    /**
       迭代器
     */
    transient Itrs itrs = null;

}

从成员变量可看出,ArrayBlockingQueue内部确实是通过数组对象items来存储所有的数据,值得注意的是ArrayBlockingQueue通过一个ReentrantLock来同时控制添加线程与移除线程的并非访问,这点与LinkedBlockingQueue区别很大(稍后会分析)。而对于notEmpty条件对象则是用于存放等待或唤醒调用take方法的线程,告诉他们队列已有元素,可以执行获取操作。同理notFull条件对象是用于等待或唤醒调用put方法的线程,告诉它们,队列未满,可以执行添加元素的操作。takeIndex代表的是下一个方法(take,poll,peek,remove)被调用时获取数组元素的索引,putIndex则代表下一个方法(put, offer, or add)被调用时元素添加到数组中的索引。图示如下

##ArrayBlockingQueue的(阻塞)添加的实现原理

//add方法实现,间接调用了offer(e)
public boolean add(E e) {
        if (offer(e))
            return true;
        else
            throw new IllegalStateException("Queue full");
    }

//offer方法
public boolean offer(E e) {
     checkNotNull(e);//检查元素是否为null
     final ReentrantLock lock = this.lock;
     lock.lock();//加锁
     try {
         if (count == items.length)//判断队列是否满
             return false;
         else {
             enqueue(e);//添加元素到队列
             return true;
         }
     } finally {
         lock.unlock();
     }
 }

//入队操作
private void enqueue(E x) {
    //获取当前数组
    final Object[] items = this.items;
    //通过putIndex索引对数组进行赋值
    items[putIndex] = x;
    //索引自增,如果已是最后一个位置,重新设置 putIndex = 0;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;//队列中元素数量加1
    //唤醒调用take()方法的线程,执行元素获取操作。
    notEmpty.signal();
}

这里的add方法和offer方法实现比较简单,其中需要注意的是enqueue(E x)方法,其方法内部通过putIndex索引直接将元素添加到数组items中,这里可能会疑惑的是当putIndex索引大小等于数组长度时,需要将putIndex重新设置为0,这是因为当前队列执行元素获取时总是从队列头部获取,而添加元素从中从队列尾部获取所以当队列索引(从0开始)与数组长度相等时,下次我们就需要从数组头部开始添加了,如下图演示

ok~,接着看put方法,它是一个阻塞添加的方法,

//put方法,阻塞时可中断
 public void put(E e) throws InterruptedException {
     checkNotNull(e);
      final ReentrantLock lock = this.lock;
      lock.lockInterruptibly();//该方法可中断
      try {
          //当队列元素个数与数组长度相等时,无法添加元素
          while (count == items.length)
              //将当前调用线程挂起,添加到notFull条件队列中等待唤醒
              notFull.await();
          enqueue(e);//如果队列没有满直接添加。。
      } finally {
          lock.unlock();
      }
  }

put方法是一个阻塞的方法,如果队列元素已满,那么当前线程将会被notFull条件对象挂起加到等待队列中,直到队列有空档才会唤醒执行添加操作。但如果队列没有满,那么就直接调用enqueue(e)方法将元素加入到数组队列中。到此我们对三个添加方法即put,offer,add都分析完毕,其中offer,add在正常情况下都是无阻塞的添加,而put方法是阻塞添加。这就是阻塞队列的添加过程。说白了就是当队列满时通过条件对象Condtion来阻塞当前调用put方法的线程,直到线程又再次被唤醒执行。总得来说添加线程的执行存在以下两种情况,一是,队列已满,那么新到来的put线程将添加到notFull的条件队列中等待,二是,有移除线程执行移除操作,移除成功同时唤醒put线程,如下图所示

##ArrayBlockingQueue的(阻塞)移除实现原理
关于删除先看poll方法,该方法获取并移除此队列的头元素,若队列为空,则返回 null

public E poll() {
      final ReentrantLock lock = this.lock;
       lock.lock();
       try {
           //判断队列是否为null,不为null执行dequeue()方法,否则返回null
           return (count == 0) ? null : dequeue();
       } finally {
           lock.unlock();
       }
    }
 //删除队列头元素并返回
 private E dequeue() {
     //拿到当前数组的数据
     final Object[] items = this.items;
      @SuppressWarnings("unchecked")
      //获取要删除的对象
      E x = (E) items[takeIndex];
      将数组中takeIndex索引位置设置为null
      items[takeIndex] = null;
      //takeIndex索引加1并判断是否与数组长度相等,
      //如果相等说明已到尽头,恢复为0
      if (++takeIndex == items.length)
          takeIndex = 0;
      count--;//队列个数减1
      if (itrs != null)
          itrs.elementDequeued();//同时更新迭代器中的元素数据
      //删除了元素说明队列有空位,唤醒notFull条件对象添加线程,执行添加操作
      notFull.signal();
      return x;
    }

poll(),获取并删除队列头元素,队列没有数据就返回null,内部通过dequeue()方法删除头元素,注释很清晰,这里不重复了。接着看remove(Object o)方法

public boolean remove(Object o) {
    if (o == null) return false;
    //获取数组数据
    final Object[] items = this.items;
    final ReentrantLock lock = this.lock;
    lock.lock();//加锁
    try {
        //如果此时队列不为null,这里是为了防止并发情况
        if (count > 0) {
            //获取下一个要添加元素时的索引
            final int putIndex = this.putIndex;
            //获取当前要被删除元素的索引
            int i = takeIndex;
            //执行循环查找要删除的元素
            do {
                //找到要删除的元素
                if (o.equals(items[i])) {
                    removeAt(i);//执行删除
                    return true;//删除成功返回true
                }
                //当前删除索引执行加1后判断是否与数组长度相等
                //若为true,说明索引已到数组尽头,将i设置为0
                if (++i == items.length)
                    i = 0; 
            } while (i != putIndex);//继承查找
        }
        return false;
    } finally {
        lock.unlock();
    }
}

//根据索引删除元素,实际上是把删除索引之后的元素往前移动一个位置
void removeAt(final int removeIndex) {

     final Object[] items = this.items;
      //先判断要删除的元素是否为当前队列头元素
      if (removeIndex == takeIndex) {
          //如果是直接删除
          items[takeIndex] = null;
          //当前队列头元素加1并判断是否与数组长度相等,若为true设置为0
          if (++takeIndex == items.length)
              takeIndex = 0;
          count--;//队列元素减1
          if (itrs != null)
              itrs.elementDequeued();//更新迭代器中的数据
      } else {
      //如果要删除的元素不在队列头部,
      //那么只需循环迭代把删除元素后面的所有元素往前移动一个位置
          //获取下一个要被添加的元素的索引,作为循环判断结束条件
          final int putIndex = this.putIndex;
          //执行循环
          for (int i = removeIndex;;) {
              //获取要删除节点索引的下一个索引
              int next = i + 1;
              //判断是否已为数组长度,如果是从数组头部(索引为0)开始找
              if (next == items.length)
                  next = 0;
               //如果查找的索引不等于要添加元素的索引,说明元素可以再移动
              if (next != putIndex) {
                  items[i] = items[next];//把后一个元素前移覆盖要删除的元
                  i = next;
              } else {
              //在removeIndex索引之后的元素都往前移动完毕后清空最后一个元素
                  items[i] = null;
                  this.putIndex = i;
                  break;//结束循环
              }
          }
          count--;//队列元素减1
          if (itrs != null)
              itrs.removedAt(removeIndex);//更新迭代器数据
      }
      notFull.signal();//唤醒添加线程
    }

remove(Object o)方法的删除过程相对复杂些,因为该方法并不是直接从队列头部删除元素。首先线程先获取锁,再一步判断队列count>0,这点是保证并发情况下删除操作安全执行。接着获取下一个要添加源的索引putIndex以及takeIndex索引 ,作为后续循环的结束判断,因为只要putIndex与takeIndex不相等就说明队列没有结束。然后通过while循环找到要删除的元素索引,执行removeAt(i)方法删除,在removeAt(i)方法中实际上做了两件事,一是首先判断队列头部元素是否为删除元素,如果是直接删除,并唤醒添加线程,二是如果要删除的元素并不是队列头元素,那么执行循环操作,从要删除元素的索引removeIndex之后的元素都往前移动一个位置,那么要删除的元素就被removeIndex之后的元素替换,从而也就完成了删除操作。接着看take()方法,是一个阻塞方法,直接获取队列头元素并删除。

//从队列头部删除,队列没有元素就阻塞,可中断
 public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
      lock.lockInterruptibly();//中断
      try {
          //如果队列没有元素
          while (count == 0)
              //执行阻塞操作
              notEmpty.await();
          return dequeue();//如果队列有元素执行删除操作
      } finally {
          lock.unlock();
      }
    }

take方法其实很简单,有就删除没有就阻塞,注意这个阻塞是可以中断的,如果队列没有数据那么就加入notEmpty条件队列等待(有数据就直接取走,方法结束),如果有新的put线程添加了数据,那么put操作将会唤醒take线程,执行take操作。图示如下

public E peek() {
      final ReentrantLock lock = this.lock;
      lock.lock();
      try {
       //直接返回当前队列的头元素,但不删除
          return itemAt(takeIndex); // null when queue is empty
      } finally {
          lock.unlock();
      }
  }

final E itemAt(int i) {
      return (E) items[i];
  }

peek方法非常简单,直接返回当前队列的头元素但不删除任何元素。ok~,到此对于ArrayBlockingQueue的主要方法就分析完了。
#LinkedBlockingQueue的基本概要
LinkedBlockingQueue是一个由链表实现的有界队列阻塞队列,但大小默认值为Integer.MAX_VALUE,所以我们在使用LinkedBlockingQueue时建议手动传值,为其提供我们所需的大小,避免队列过大造成机器负载或者内存爆满等情况。其构造函数如下

//默认大小为Integer.MAX_VALUE
public LinkedBlockingQueue() {
       this(Integer.MAX_VALUE);
}

//创建指定大小为capacity的阻塞队列
public LinkedBlockingQueue(int capacity) {
     if (capacity <= 0) throw new IllegalArgumentException();
     this.capacity = capacity;
     last = head = new Node<E>(null);
 }

//创建大小默认值为Integer.MAX_VALUE的阻塞队列并添加c中的元素到阻塞队列
public LinkedBlockingQueue(Collection<? extends E> c) {
     this(Integer.MAX_VALUE);
     final ReentrantLock putLock = this.putLock;
     putLock.lock(); // Never contended, but necessary for visibility
     try {
         int n = 0;
         for (E e : c) {
             if (e == null)
                 throw new NullPointerException();
             if (n == capacity)
                 throw new IllegalStateException("Queue full");
             enqueue(new Node<E>(e));
             ++n;
         }
         count.set(n);
     } finally {
         putLock.unlock();
     }
 }

从源码看,有三种方式可以构造LinkedBlockingQueue,通常情况下,我们建议创建指定大小的LinkedBlockingQueue阻塞队列。LinkedBlockingQueue队列也是按 FIFO(先进先出)排序元素。队列的头部是在队列中时间最长的元素,队列的尾部 是在队列中时间最短的元素,新元素插入到队列的尾部,而队列执行获取操作会获得位于队列头部的元素。在正常情况下,链接队列的吞吐量要高于基于数组的队列(ArrayBlockingQueue),因为其内部实现添加和删除操作使用的两个ReenterLock来控制并发执行,而ArrayBlockingQueue内部只是使用一个ReenterLock控制并发,因此LinkedBlockingQueue的吞吐量要高于ArrayBlockingQueue。注意LinkedBlockingQueue和ArrayBlockingQueue的API几乎是一样的,但它们的内部实现原理不太相同,这点稍后会分析。使用LinkedBlockingQueue,我们同样也能实现生产者消费者模式。只需把前面ArrayBlockingQueue案例中的阻塞队列对象换成LinkedBlockingQueue即可。这里限于篇幅就不贴重复代码了。接下来我们重点分析LinkedBlockingQueue的内部实现原理,最后我们将对ArrayBlockingQueue和LinkedBlockingQueue 做总结,阐明它们间的不同之处。

#LinkedBlockingQueue的实现原理剖析
##原理概论

LinkedBlockingQueue是一个基于链表的阻塞队列,其内部维持一个基于链表的数据队列,实际上我们对LinkedBlockingQueue的API操作都是间接操作该数据队列,这里我们先看看LinkedBlockingQueue的内部成员变量

public class LinkedBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {

    /**
     * 节点类,用于存储数据
     */
    static class Node<E> {
        E item;

        /**
         * One of:
         * - the real successor Node
         * - this Node, meaning the successor is head.next
         * - null, meaning there is no successor (this is the last node)
         */
        Node<E> next;

        Node(E x) { item = x; }
    }

    /** 阻塞队列的大小,默认为Integer.MAX_VALUE */
    private final int capacity;

    /** 当前阻塞队列中的元素个数 */
    private final AtomicInteger count = new AtomicInteger();

    /**
     * 阻塞队列的头结点
     */
    transient Node<E> head;

    /**
     * 阻塞队列的尾节点
     */
    private transient Node<E> last;

    /** 获取并移除元素时使用的锁,如take, poll, etc */
    private final ReentrantLock takeLock = new ReentrantLock();

    /** notEmpty条件对象,当队列没有数据时用于挂起执行删除的线程 */
    private final Condition notEmpty = takeLock.newCondition();

    /** 添加元素时使用的锁如 put, offer, etc */
    private final ReentrantLock putLock = new ReentrantLock();

    /** notFull条件对象,当队列数据已满时用于挂起执行添加的线程 */
    private final Condition notFull = putLock.newCondition();

}

从上述可看成,每个添加到LinkedBlockingQueue队列中的数据都将被封装成Node节点,添加的链表队列中,其中head和last分别指向队列的头结点和尾结点。与ArrayBlockingQueue不同的是,LinkedBlockingQueue内部分别使用了takeLock 和 putLock 对并发进行控制,也就是说,添加和删除操作并不是互斥操作,可以同时进行,这样也就可以大大提高吞吐量。这里再次强调如果没有给LinkedBlockingQueue指定容量大小,其默认值将是Integer.MAX_VALUE,如果存在添加速度大于删除速度时候,有可能会内存溢出,这点在使用前希望慎重考虑。至于LinkedBlockingQueue的实现原理图与ArrayBlockingQueue是类似的,除了对添加和移除方法使用单独的锁控制外,两者都使用了不同的Condition条件对象作为等待队列,用于挂起take线程和put线程。

ok~,下面我们看看其其内部添加过程和删除过程是如何实现的。

##添加方法的实现原理

对于添加方法,主要指的是add,offer以及put,这里先看看add方法和offer方法的实现

public boolean add(E e) {
     if (offer(e))
         return true;
     else
         throw new IllegalStateException("Queue full");
}

从源码可以看出,add方法间接调用的是offer方法,如果add方法添加失败将抛出IllegalStateException异常,添加成功则返回true,那么下面我们直接看看offer的相关方法实现

public boolean offer(E e) {
     //添加元素为null直接抛出异常
     if (e == null) throw new NullPointerException();
      //获取队列的个数
      final AtomicInteger count = this.count;
      //判断队列是否已满
      if (count.get() == capacity)
          return false;
      int c = -1;
      //构建节点
      Node<E> node = new Node<E>(e);
      final ReentrantLock putLock = this.putLock;
      putLock.lock();
      try {
          //再次判断队列是否已满,考虑并发情况
          if (count.get() < capacity) {
              enqueue(node);//添加元素
              c = count.getAndIncrement();//拿到当前未添加新元素时的队列长度
              //如果容量还没满
              if (c + 1 < capacity)
                  notFull.signal();//唤醒下一个添加线程,执行添加操作
          }
      } finally {
          putLock.unlock();
      }
      // 由于存在添加锁和消费锁,而消费锁和添加锁都会持续唤醒等到线程,因此count肯定会变化。
      //这里的if条件表示如果队列中还有1条数据
      if (c == 0) 
        signalNotEmpty();//如果还存在数据那么就唤醒消费锁
    return c >= 0; // 添加成功返回true,否则返回false
  }

//入队操作
private void enqueue(Node<E> node) {
     //队列尾节点指向新的node节点
     last = last.next = node;
}

//signalNotEmpty方法
private void signalNotEmpty() {
      final ReentrantLock takeLock = this.takeLock;
      takeLock.lock();
          //唤醒获取并删除元素的线程
          notEmpty.signal();
      } finally {
          takeLock.unlock();
      }
  }

这里的Offer()方法做了两件事,第一件事是判断队列是否满,满了就直接释放锁,没满就将节点封装成Node入队,然后再次判断队列添加完成后是否已满,不满就继续唤醒等到在条件对象notFull上的添加线程。第二件事是,判断是否需要唤醒等到在notEmpty条件对象上的消费线程。这里我们可能会有点疑惑,为什么添加完成后是继续唤醒在条件对象notFull上的添加线程而不是像ArrayBlockingQueue那样直接唤醒notEmpty条件对象上的消费线程?而又为什么要当if (c == 0)时才去唤醒消费线程呢?

  • 唤醒添加线程的原因,在添加新元素完成后,会判断队列是否已满,不满就继续唤醒在条件对象notFull上的添加线程,这点与前面分析的ArrayBlockingQueue很不相同,在ArrayBlockingQueue内部完成添加操作后,会直接唤醒消费线程对元素进行获取,这是因为ArrayBlockingQueue只用了一个ReenterLock同时对添加线程和消费线程进行控制,这样如果在添加完成后再次唤醒添加线程的话,消费线程可能永远无法执行,而对于LinkedBlockingQueue来说就不一样了,其内部对添加线程和消费线程分别使用了各自的ReenterLock锁对并发进行控制,也就是说添加线程和消费线程是不会互斥的,所以添加锁只要管好自己的添加线程即可,添加线程自己直接唤醒自己的其他添加线程,如果没有等待的添加线程,直接结束了。如果有就直到队列元素已满才结束挂起,当然offer方法并不会挂起,而是直接结束,只有put方法才会当队列满时才执行挂起操作。注意消费线程的执行过程也是如此。这也是为什么LinkedBlockingQueue的吞吐量要相对大些的原因。

  • 为什么要判断if (c == 0)时才去唤醒消费线程呢,这是因为消费线程一旦被唤醒是一直在消费的(前提是有数据),所以c值是一直在变化的,c值是添加完元素前队列的大小,此时c只可能是0或c>0,如果是c=0,那么说明之前消费线程已停止,条件对象上可能存在等待的消费线程,添加完数据后应该是c+1,那么有数据就直接唤醒等待消费线程,如果没有就结束啦,等待下一次的消费操作。如果c>0那么消费线程就不会被唤醒,只能等待下一个消费操作(poll、take、remove)的调用,那为什么不是条件c>0才去唤醒呢?我们要明白的是消费线程一旦被唤醒会和添加线程一样,一直不断唤醒其他消费线程,如果添加前c>0,那么很可能上一次调用的消费线程后,数据并没有被消费完,条件队列上也就不存在等待的消费线程了,所以c>0唤醒消费线程得意义不是很大,当然如果添加线程一直添加元素,那么一直c>0,消费线程执行的换就要等待下一次调用消费操作了(poll、take、remove)。

##移除方法的实现原理

关于移除的方法主要是指remove和poll以及take方法,下面一一分析

public boolean remove(Object o) {
   if (o == null) return false;
     fullyLock();//同时对putLock和takeLock加锁
     try {
         //循环查找要删除的元素
         for (Node<E> trail = head, p = trail.next;
              p != null;
              trail = p, p = p.next) {
             if (o.equals(p.item)) {//找到要删除的节点
                 unlink(p, trail);//直接删除
                 return true;
             }
         }
         return false;
     } finally {
         fullyUnlock();//解锁
     }
    }

//两个同时加锁
void fullyLock() {
       putLock.lock();
       takeLock.lock();
   }

void fullyUnlock() {
      takeLock.unlock();
      putLock.unlock();
  }

remove方法删除指定的对象,这里我们可能会诧异,为什么同时对putLock和takeLock加锁?这是因为remove方法删除的数据的位置不确定,为了避免造成并非安全问题,所以需要对2个锁同时加锁。

public E poll() {
         //获取当前队列的大小
        final AtomicInteger count = this.count;
        if (count.get() == 0)//如果没有元素直接返回null
            return null;
        E x = null;
        int c = -1;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            //判断队列是否有数据
            if (count.get() > 0) {
                //如果有,直接删除并获取该元素值
                x = dequeue();
                //当前队列大小减一
                c = count.getAndDecrement();
                //如果队列未空,继续唤醒等待在条件对象notEmpty上的消费线程
                if (c > 1)
                    notEmpty.signal();
            }
        } finally {
            takeLock.unlock();
        }
        //判断c是否等于capacity,这是因为如果满说明NotFull条件对象上
        //可能存在等待的添加线程
        if (c == capacity)
            signalNotFull();
        return x;
    }

  private E dequeue() {
        Node<E> h = head;//获取头结点
        Node<E> first = h.next; 获取头结的下一个节点(要删除的节点)
        h.next = h; // help GC//自己next指向自己,即被删除
        head = first;//更新头结点
        E x = first.item;//获取删除节点的值
        first.item = null;//清空数据,因为first变成头结点是不能带数据的,这样也就删除队列的带数据的第一个节点
        return x;
    }

poll方法也比较简单,如果队列没有数据就返回null,如果队列有数据,那么就取出来,如果队列还有数据那么唤醒等待在条件对象notEmpty上的消费线程。然后判断if (c == capacity)为true就唤醒添加线程,这点与前面分析if(c==0)是一样的道理。因为只有可能队列满了,notFull条件对象上才可能存在等待的添加线程。

public E take() throws InterruptedException {
        E x;
        int c = -1;
        //获取当前队列大小
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lockInterruptibly();//可中断
        try {
            //如果队列没有数据,挂机当前线程到条件对象的等待队列中
            while (count.get() == 0) {
                notEmpty.await();
            }
            //如果存在数据直接删除并返回该数据
            x = dequeue();
            c = count.getAndDecrement();//队列大小减1
            if (c > 1)
                notEmpty.signal();//还有数据就唤醒后续的消费线程
        } finally {
            takeLock.unlock();
        }
        //满足条件,唤醒条件对象上等待队列中的添加线程
        if (c == capacity)
            signalNotFull();
        return x;
    }

take方法是一个可阻塞可中断的移除方法,主要做了两件事,一是,如果队列没有数据就挂起当前线程到 notEmpty条件对象的等待队列中一直等待,如果有数据就删除节点并返回数据项,同时唤醒后续消费线程,二是尝试唤醒条件对象notFull上等待队列中的添加线程。 到此关于remove、poll、take的实现也分析完了,其中只有take方法具备阻塞功能。remove方法则是成功返回true失败返回false,poll方法成功返回被移除的值,失败或没数据返回null。下面再看看两个检查方法,即peek和element

//构造方法,head 节点不存放数据
 public LinkedBlockingQueue(int capacity) {
       if (capacity <= 0) throw new IllegalArgumentException();
       this.capacity = capacity;
       last = head = new Node<E>(null);
   }

 public E element() {
        E x = peek();//直接调用peek
        if (x != null)
            return x;
        else
            throw new NoSuchElementException();//没数据抛异常
    }

 public E peek() {
        if (count.get() == 0)
            return null;
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            //获取头结节点的下一个节点
            Node<E> first = head.next;
            if (first == null)
                return null;//为null就返回null
            else
                return first.item;//返回值
        } finally {
            takeLock.unlock();
        }
    }

从代码来看,head头结节点在初始化时是本身不带数据的,仅仅作为头部head方便我们执行链表的相关操作。peek返回直接获取头结点的下一个节点返回其值,如果没有值就返回null,有值就返回节点对应的值。element方法内部调用的是peek,有数据就返回,没数据就抛异常。下面我们最后来看两个根据时间阻塞的方法,比较有意思,利用的Conditin来实现的。

//在指定时间内阻塞添加的方法,超时就结束
 public boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException {

        if (e == null) throw new NullPointerException();
        //将时间转换成纳秒
        long nanos = unit.toNanos(timeout);
        int c = -1;
        //获取锁
        final ReentrantLock putLock = this.putLock;
        //获取当前队列大小
        final AtomicInteger count = this.count;
        //锁中断(如果需要)
        putLock.lockInterruptibly();
        try {
            //判断队列是否满
            while (count.get() == capacity) {
                if (nanos <= 0)
                    return false;
                //如果队列满根据阻塞的等待
                nanos = notFull.awaitNanos(nanos);
            }
            //队列没满直接入队
            enqueue(new Node<E>(e));
            c = count.getAndIncrement();
            //唤醒条件对象上等待的线程
            if (c + 1 < capacity)
                notFull.signal();
        } finally { 
            putLock.unlock();
        }
        //唤醒消费线程
        if (c == 0)
            signalNotEmpty();
        return true;
    }

对于这个offer方法,我们重点来看看阻塞的这段代码

//判断队列是否满
 while (count.get() == capacity) {
        if (nanos <= 0)
            return false;
        //如果队列满根据阻塞的等待
        nanos = notFull.awaitNanos(nanos);
    }

//CoditionObject(Codition的实现类)中的awaitNanos方法
 public final long awaitNanos(long nanosTimeout)
                throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            //这里是将当前添加线程封装成NODE节点加入Condition的等待队列中
            //注意这里的NODE是AQS的内部类Node
            Node node = addConditionWaiter();
            //加入等待,那么就释放当前线程持有的锁
            int savedState = fullyRelease(node);
            //计算过期时间
            final long deadline = System.nanoTime() + nanosTimeout;
            int interruptMode = 0;

            while (!isOnSyncQueue(node)) {
                if (nanosTimeout <= 0L) {
                    transferAfterCancelledWait(node);
                    break;
                }
                //主要看这里!!由于是while 循环,这里会不断判断等待时间
                //nanosTimeout 是否超时
                //static final long spinForTimeoutThreshold = 1000L;
                if (nanosTimeout >= spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);//挂起线程
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
                //重新计算剩余等待时间,while循环中继续判断下列公式
                //nanosTimeout >= spinForTimeoutThreshold
                nanosTimeout = deadline - System.nanoTime();
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
            return deadline - System.nanoTime();
        }

awaitNanos方法中,根据传递进来的时间计算超时阻塞nanosTimeout,然后通过while循环中判断nanosTimeout >= spinForTimeoutThreshold 该公式是否成立,当其为true时则说明超时时间nanosTimeout 还未到期,再次计算nanosTimeout = deadline - System.nanoTime();即nanosTimeout ,持续判断,直到nanosTimeout 小于spinForTimeoutThreshold结束超时阻塞操作,方法也就结束。这里的spinForTimeoutThreshold其实更像一个经验值,因为非常短的超时等待无法做到十分精确,因此采用了spinForTimeoutThreshold这样一个临界值。offer(E e, long timeout, TimeUnit unit)方法内部正是利用这样的Codition的超时等待awaitNanos方法实现添加方法的超时阻塞操作。同样对于poll(long timeout, TimeUnit unit)方法也是一样的道理。

#LinkedBlockingQueue和ArrayBlockingQueue迥异

通过上述的分析,对于LinkedBlockingQueue和ArrayBlockingQueue的基本使用以及内部实现原理我们已较为熟悉了,这里我们就对它们两间的区别来个小结

1.队列大小有所不同,ArrayBlockingQueue是有界的初始化必须指定大小,而LinkedBlockingQueue可以是有界的也可以是无界的(Integer.MAX_VALUE),对于后者而言,当添加速度大于移除速度时,在无界的情况下,可能会造成内存溢出等问题。

2.数据存储容器不同,ArrayBlockingQueue采用的是数组作为数据存储容器,而LinkedBlockingQueue采用的则是以Node节点作为连接对象的链表。

3.由于ArrayBlockingQueue采用的是数组的存储容器,因此在插入或删除元素时不会产生或销毁任何额外的对象实例,而LinkedBlockingQueue则会生成一个额外的Node对象。这可能在长时间内需要高效并发地处理大批量数据的时,对于GC可能存在较大影响。

4.两者的实现队列添加或移除的锁不一样,ArrayBlockingQueue实现的队列中的锁是没有分离的,即添加操作和移除操作采用的同一个ReenterLock锁,而LinkedBlockingQueue实现的队列中的锁是分离的,其添加采用的是putLock,移除采用的则是takeLock,这样能大大提高队列的吞吐量,也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。

 

 

posted @ 2021-07-29 14:18  CharyGao  阅读(3)  评论(0编辑  收藏  举报