第6章类文件结构
6.1 概述
6.2 无关性基石
6.3 Class类文件的结构
java虚拟机不和包括java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。
Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的。
Class文件是一组以字节(8位二进制/8bit)为基础单位的二进制流。
整个Class文件中存储的内容几乎全部是程序运行的必要数据。
Class文件格式采用伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。
无符号数 u1、u2、u4、u8分别代表1个字节、2个字节。。。无符号数可以用来描述数字、索引引用、数量值或者按照utf-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的符合数据类型。
整个Class文件本质上就是一张表。
高版本的jdk能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
6.3.2 常量池
常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一。
常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
1.类和接口的全限定名
2.字段的名称和描述符(访问修饰符)
3.方法的名称和描述符
Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
常量池中每一项常量都是一个表。
Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称。
6.3.3 访问标志
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
6.3.4 类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合。Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。
6.3.5 字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
Java中描述一个字段可以包含什么信息?字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来标识。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
字段表集合中不会列出从超类或者父接口中继承而来的字段。
6.3.6 方法表集合
方法表的结构如同字段表一样,依次包含了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。
如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但是,有可能会出现由编译器自动添加的方法,最典型的便是类构造器方法和实例构造器方法。
6.3.7 属性表集合
1.Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中。
Code属性是Class文件中最重要的一个属性。一个Java程序中的信息可以分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义以及其他信息)两部分。
了解Code属性是学习后面关于字节码执行引擎内容的必要基础,能直接阅读字节码也是工作中分析Java代码语义问题的必要工具和基本技能。
在任何方法里面,都可以通过“this”关键字访问到此方法所属的对象。这个机制的实现,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。
6.ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。对于非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属
性。目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化。
7.InnerClasses属性
InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它锁包含的内部类生成InnerClasses属性。
6.4 字节码指令简介
java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构(这两种架构的区别和影响将在第8章中探讨),所以大多数的指令都不包含操作数,只有一个操作码。
Java虚拟机操作码的长度为一个字节,所以指令集的操作码总数不可能超过256条。
6.4.1 字节码与数据类型
6.4.2 加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。
1.将一个局部变量加载到操作栈:iload等load相关的指令;
2.将一个数值从操作数栈存储到局部变量表:istore等相关的指令;
3.将一个常量加载到操作数栈:ipush等push等等
存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作。
6.4.3 运算指令
运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
加法指令:iadd、ladd、fadd、dadd。
减法指令:isub、lsub、fsub、dsub。
乘法指令:imul、lmul、fmul、dmul。
除法指令:idiv、ldiv、fdiv、ddiv。
求余指令:irem、lrem、frem、drem。
取反指令:ineg、lneg、fneg、dneg。
位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
按位或指令:ior、lor。
按位与指令:iand、land。
按位异或指令:ixor、lxor。
局部变量自增指令:iinc。
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。
6.4.4 类型转换指令
类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作。
窄化类型转换指令包括::i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。
6.4.5 对象创建与访问指令
虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。对象创建后,就可
以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素。
创建类实例的指令:new。
创建数组的指令:newarray、anewarray、multianewarray。
访问类字段和实例字段的指令:getfield、putfield、getstatic、putstatic
把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
取数组长度的指令:arraylength。
检查类实例类型的指令:instanceof、checkcast。
6.4.6 操作数栈管理指令
将操作数栈的栈顶一个或两个元素出栈:pop、pop2
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、
将栈最顶端的两个数值互换;swap
6.4.7 控制转移指令
从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。
6.4.8 方法调用和返回指令
invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
invokestatic指令用于调用类方法(static方法)。
方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。
6.4.9 异常处理指令
6.4.10 同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
第8章虚拟机字节码执行引擎
8.1 概述
本章将主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行。
8.2 运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。
每个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
每一个栈帧都包含局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。
8.2.1 局部变量表
局部变量表用于存放方法参数和方法内部定义的局部变量。
局部变量表的容量以变量槽为最小单位。
Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference[3]和returnAddress
8种类型。
reference类型表示对一个对象实例的引用。虚拟机实现至少都应当能通过这个引用做到两点,一是从此引用中直接或间接地查找到对象在Java堆中的数据存放的其实地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息。
在方法执行时,虚拟机是使用局部变量表完成参数值到变量列表的传递过程的,如果执行的是实例方法,那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。
如果一个局部变量定义了但没有初始值是不能使用的。
8.2.2 操作数栈
操作数栈(Operand Stack)也常称为操作栈。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。
在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。
8.2.3 动态连接
每个栈帧都包含了一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化成为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分成为动态连接。
8.2.4 方法返回地址
方法返回有两种情况:1.遇到return。2.发生异常。
无论何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。
方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数占中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
8.2.5 附加信息
略
8.3 方法调用
Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于之前说的直接引用)。
8.3.1 解析
在Java语言中符合“编译器可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类的加载阶段进行解析。
为什么需要动态连接,是为了满足多态的需求。
8.3.2 分派
分派调用过程揭示多态。如“重载”和“重写”在Java虚拟机中是如何实现的,虚拟机如何确定正确的目标方法。
1.静态分派
虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类行作为判定依据的。
2.动态分派
动态分派和多态性的另一个重要体现——重写有着很密切的关联。
第12章 Java内存模型与线程
12.1 概述
12.2 硬件的效率与一致性
高速缓存解决了处理器与内存的速度矛盾,但引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(Main Memory)。
为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。
在本章中将会多次提到的“内存模型”一词,可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。
12.3 Java内存模型
12.3.1 主内存与工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。这里的变量指的是共享变量,如:实例字段、静态字段和构成数组对象的元素。
Java内存模型定义了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
12.3.2 内存间交互操作
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了一下8中操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
1.lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
2.unlock(解锁):把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
3.read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
4.load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入到工作内存的变量副本中。
5.use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的字节码指令时将会执行这个操作。
6.assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
7.store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
8.write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中。
Java内存模型规定了在执行上述8中基本操作时必须满足如下规则:
1.不允许read和load、store和wirte操作之一单独出现。
2.不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化通不会主内存。
3.不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步会主内存中。
4.一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
5.一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
6.如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
7.如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个呗其他线程锁定住的变量。
8.对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
12.3.3 对于volatile型变量的特殊规则
当一个变量定义为volatile之后,它将具备两种特性:
1.保证此变量对所有线程的可见性。这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。
a.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
b.变量不需要与其他的状态变量共同参与不变约束。
2.使用volatile变量会禁止指令重排序。
指令重排序无法越过内存屏障。
volatile变量读操作的性能消耗和普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
volatile的使用场景?
Java内存模型中对volatile变量定义的特殊规则。假定T表示一个线程,V和W分别表示两个volatile型变量,那么在进行read、load、use、assign、store和write操作时需要满足如下规则:
1.只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。线程T对变量V的use动作可以认为是和线程T变量V的load、read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)。
2.只有当线程T对变量V行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程T对变量V的assign动作可以认为是和线程T对变量V的store、write动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量所做的修改)。
3、假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)。
12.3.5 原子性、可见性与有序性
Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性3个特征来建立的。
可见性:
除了volatile,synchronized和final可以实现可见性。
同步快的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这条规则获得的。final的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去,那在其他线程中就能看见final字段的值。
有序性:
Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。
12.3.6 先行发生原则
如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
Java内存模型下一些“天然的”先行发生关系:
1.程序次序规则(Program Order
Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
2.管程锁定规则(Monitor Lock
Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
3.volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
4.线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
5.线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段加测到线程已经终止执行。
6.线程中断规则:对线程interrupt()方法的调用先发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
7.对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
8.传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
12.4 Java与线程
12.4.1 线程的实现
12.4.2 Java线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度和抢占式调度。
Java使用的是抢占式调度。每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。
12.4.3 状态转换
第13章线程安全与锁优化
13.1 概述
13.2 线程安全
线程安全的代码都必须具备一个特征:代码本身封装了所有必要的正确性保障手段,令调用者无须关心多想的问题,更无须自己采取任何措施来保证多线程的正确调用。
13.2.1 Java语言中的线程安全
线程安全的“安全程度”由强到弱,不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
1.不可变
不可变的对象一定是线程安全的。
2.绝对线程安全
3.相对线程安全
4.
13.2.2 线程安全的实现方法
1.互斥同步
同步是指在多线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。
最基本的互斥同步手段就是synchronized关键字。
synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。
除了synchronized,还有重入锁ReentrantLock。相比synchronized,ReentrantLock增加了一些高级功能,有3项:等待可中断、可实现公平锁、锁可以绑定多个条件。
等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步快很有帮助。
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
所绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象。
2.非阻塞同步
互斥同步也称为阻塞同步。主要的问题就是进行线程阻塞和唤醒所带来的性能问题。
互斥同步属于一种悲观的并发策略。另外一种基于冲突检测的乐观并发策略,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这个称谓非阻塞同步。
CAS精髓
3.无同步方案
如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施。
两种天生就是线程安全的代码:
a.可重入代码
b.线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。
如果一个变量要被某个线程独享,可以通过ThreadLocal类来实现线程本地存储。
13.3 锁优化
13.3.1 自旋锁与自适应自旋
有些情况下,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起会恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求所的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
如果锁贝占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次。JDK1.6中引入了自适应的自旋锁。
13.3.2 锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持(第11章已经讲解过逃逸分析技术),如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
13.3.3 锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
13.3.4 轻量级锁
对象头的Mark Word是实现轻量级锁和偏向锁的关键。
第6章类文件结构
6.1 概述
6.2 无关性基石
6.3 Class类文件的结构
java虚拟机不和包括java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联。Class文件中包含了Java虚拟机指令集和符号表以及若干其他辅助信息。
Java语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的。
Class文件是一组以字节(8位二进制/8bit)为基础单位的二进制流。
整个Class文件中存储的内容几乎全部是程序运行的必要数据。
Class文件格式采用伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。
无符号数 u1、u2、u4、u8分别代表1个字节、2个字节。。。无符号数可以用来描述数字、索引引用、数量值或者按照utf-8编码构成字符串值。
表是由多个无符号数或者其他表作为数据项构成的符合数据类型。
整个Class文件本质上就是一张表。
高版本的jdk能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
6.3.2 常量池
常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一。
常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
1.类和接口的全限定名
2.字段的名称和描述符(访问修饰符)
3.方法的名称和描述符
Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
常量池中每一项常量都是一个表。
Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称。
6.3.3 访问标志
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等。
6.3.4 类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合。Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。
6.3.5 字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
Java中描述一个字段可以包含什么信息?字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来标识。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
字段表集合中不会列出从超类或者父接口中继承而来的字段。
6.3.6 方法表集合
方法表的结构如同字段表一样,依次包含了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。
如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但是,有可能会出现由编译器自动添加的方法,最典型的便是类构造器方法和实例构造器方法。
6.3.7 属性表集合
1.Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中。
Code属性是Class文件中最重要的一个属性。一个Java程序中的信息可以分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义以及其他信息)两部分。
了解Code属性是学习后面关于字节码执行引擎内容的必要基础,能直接阅读字节码也是工作中分析Java代码语义问题的必要工具和基本技能。
在任何方法里面,都可以通过“this”关键字访问到此方法所属的对象。这个机制的实现,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,然后在虚拟机调用实例方法时自动传入此参数而已。
6.ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。对于非static类型的变量(也就是实例变量)的赋值是在实例构造器<init>方法中进行的;而对于类变量,则有两种方式可以选择:在类构造器<clinit>方法中或者使用ConstantValue属
性。目前Sun Javac编译器的选择是:如果同时使用final和static来修饰一个变量(按照习惯,这里称“常量”更贴切),并且这个变量的数据类型是基本类型或者java.lang.String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有被final修饰,或者并非基本类型及字符串,则将会选择在<clinit>方法中进行初始化。
7.InnerClasses属性
InnerClasses属性用于记录内部类与宿主类之间的关联。如果一个类中定义了内部类,那编译器将会为它以及它锁包含的内部类生成InnerClasses属性。
6.4 字节码指令简介
java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是寄存器的架构(这两种架构的区别和影响将在第8章中探讨),所以大多数的指令都不包含操作数,只有一个操作码。
Java虚拟机操作码的长度为一个字节,所以指令集的操作码总数不可能超过256条。
6.4.1 字节码与数据类型
6.4.2 加载和存储指令
加载和存储指令用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。
1.将一个局部变量加载到操作栈:iload等load相关的指令;
2.将一个数值从操作数栈存储到局部变量表:istore等相关的指令;
3.将一个常量加载到操作数栈:ipush等push等等
存储数据的操作数栈和局部变量表主要就是由加载和存储指令进行操作。
6.4.3 运算指令
运算或算术指令用于对两个操作数栈上的值进行某种特定运算,并把结果重新存入到操作栈顶。
加法指令:iadd、ladd、fadd、dadd。
减法指令:isub、lsub、fsub、dsub。
乘法指令:imul、lmul、fmul、dmul。
除法指令:idiv、ldiv、fdiv、ddiv。
求余指令:irem、lrem、frem、drem。
取反指令:ineg、lneg、fneg、dneg。
位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
按位或指令:ior、lor。
按位与指令:iand、land。
按位异或指令:ixor、lxor。
局部变量自增指令:iinc。
比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。
6.4.4 类型转换指令
类型转换指令可以将两种不同的数值类型进行相互转换,这些转换操作一般用于实现用户代码中的显示类型转换操作。
窄化类型转换指令包括::i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。
6.4.5 对象创建与访问指令
虽然类实例和数组都是对象,但Java虚拟机对类实例和数组的创建与操作使用了不同的字节码指令。对象创建后,就可
以通过对象访问指令获取对象实例或者数组实例中的字段或者数组元素。
创建类实例的指令:new。
创建数组的指令:newarray、anewarray、multianewarray。
访问类字段和实例字段的指令:getfield、putfield、getstatic、putstatic
把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
将一个操作数栈的值存储到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
取数组长度的指令:arraylength。
检查类实例类型的指令:instanceof、checkcast。
6.4.6 操作数栈管理指令
将操作数栈的栈顶一个或两个元素出栈:pop、pop2
复制栈顶一个或两个数值并将复制值或双份的复制值重新压入栈顶:dup、dup2、
将栈最顶端的两个数值互换;swap
6.4.7 控制转移指令
从概念模型上理解,可以认为控制转移指令就是在有条件或无条件地修改PC寄存器的值。
6.4.8 方法调用和返回指令
invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
invokestatic指令用于调用类方法(static方法)。
方法调用指令与数据类型无关,而方法返回指令是根据返回值的类型区分的,包括ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn和areturn,另外还有一条return指令供声明为void的方法、实例初始化方法以及类和接口的类初始化方法使用。
6.4.9 异常处理指令
6.4.10 同步指令
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
第8章虚拟机字节码执行引擎
8.1 概述
本章将主要从概念模型的角度来讲解虚拟机的方法调用和字节码执行。
8.2 运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。
每个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
每一个栈帧都包含局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。
8.2.1 局部变量表
局部变量表用于存放方法参数和方法内部定义的局部变量。
局部变量表的容量以变量槽为最小单位。
Java中占用32位以内的数据类型有boolean、byte、char、short、int、float、reference[3]和returnAddress
8种类型。
reference类型表示对一个对象实例的引用。虚拟机实现至少都应当能通过这个引用做到两点,一是从此引用中直接或间接地查找到对象在Java堆中的数据存放的其实地址索引,二是此引用中直接或间接地查找到对象所属数据类型在方法区中的存储的类型信息。
在方法执行时,虚拟机是使用局部变量表完成参数值到变量列表的传递过程的,如果执行的是实例方法,那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。
如果一个局部变量定义了但没有初始值是不能使用的。
8.2.2 操作数栈
操作数栈(Operand Stack)也常称为操作栈。操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2。
在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。
8.2.3 动态连接
每个栈帧都包含了一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这种转化成为静态解析。另外一部分将在每一次运行期间转化为直接引用,这部分成为动态连接。
8.2.4 方法返回地址
方法返回有两种情况:1.遇到return。2.发生异常。
无论何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。
方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数占中,调整PC计数器的值以指向方法调用指令后面的一条指令等。
8.2.5 附加信息
略
8.3 方法调用
Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于之前说的直接引用)。
8.3.1 解析
在Java语言中符合“编译器可知,运行期不可变”这个要求的方法,主要包括静态方法和私有方法两大类,前者与类型直接关联,后者在外部不可被访问,这两种方法各自的特点决定了它们都不可能通过继承或别的方式重写其他版本,因此它们都适合在类的加载阶段进行解析。
为什么需要动态连接,是为了满足多态的需求。
8.3.2 分派
分派调用过程揭示多态。如“重载”和“重写”在Java虚拟机中是如何实现的,虚拟机如何确定正确的目标方法。
1.静态分派
虚拟机(准确地说是编译器)在重载时是通过参数的静态类型而不是实际类行作为判定依据的。
2.动态分派
动态分派和多态性的另一个重要体现——重写有着很密切的关联。
第12章 Java内存模型与线程
12.1 概述
12.2 硬件的效率与一致性
高速缓存解决了处理器与内存的速度矛盾,但引入了一个新的问题:缓存一致性(Cache Coherence)。在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(Main Memory)。
为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作。
在本章中将会多次提到的“内存模型”一词,可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。
12.3 Java内存模型
12.3.1 主内存与工作内存
Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。这里的变量指的是共享变量,如:实例字段、静态字段和构成数组对象的元素。
Java内存模型定义了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
12.3.2 内存间交互操作
关于主内存与工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了一下8中操作来完成,虚拟机实现时必须保证下面提及的每一种操作都是原子的、不可再分的。
1.lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
2.unlock(解锁):把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
3.read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
4.load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入到工作内存的变量副本中。
5.use(使用):作用于工作内存的变量,它把工作内存中的一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的字节码指令时将会执行这个操作。
6.assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
7.store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
8.write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量值放入主内存的变量中。
Java内存模型规定了在执行上述8中基本操作时必须满足如下规则:
1.不允许read和load、store和wirte操作之一单独出现。
2.不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化通不会主内存。
3.不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步会主内存中。
4.一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign和load操作。
5.一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。
6.如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。
7.如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个呗其他线程锁定住的变量。
8.对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)。
12.3.3 对于volatile型变量的特殊规则
当一个变量定义为volatile之后,它将具备两种特性:
1.保证此变量对所有线程的可见性。这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。
由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized或java.util.concurrent中的原子类)来保证原子性。
a.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
b.变量不需要与其他的状态变量共同参与不变约束。
2.使用volatile变量会禁止指令重排序。
指令重排序无法越过内存屏障。
volatile变量读操作的性能消耗和普通变量几乎没有什么差别,但是写操作则可能会慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
volatile的使用场景?
Java内存模型中对volatile变量定义的特殊规则。假定T表示一个线程,V和W分别表示两个volatile型变量,那么在进行read、load、use、assign、store和write操作时需要满足如下规则:
1.只有当线程T对变量V执行的前一个动作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。线程T对变量V的use动作可以认为是和线程T变量V的load、read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用V前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)。
2.只有当线程T对变量V行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程T对变量V的assign动作可以认为是和线程T对变量V的store、write动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量所做的修改)。
3、假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或store动作,假定动作P是和动作F相应的对变量V的read或write动作;类似的,假定动作B是线程T对变量W实施的use或assign动作,假定动作G是和动作B相关联的load或store动作,假定动作Q是和动作G相应的对变量W的read或write动作。如果A先于B,那么P先于Q(这条规则要求volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)。
12.3.5 原子性、可见性与有序性
Java内存模型是围绕着在并发过程中如何处理原子性、可见性和有序性3个特征来建立的。
可见性:
除了volatile,synchronized和final可以实现可见性。
同步快的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store和write操作)”这条规则获得的。final的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去,那在其他线程中就能看见final字段的值。
有序性:
Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。
12.3.6 先行发生原则
如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。
Java内存模型下一些“天然的”先行发生关系:
1.程序次序规则(Program Order
Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
2.管程锁定规则(Monitor Lock
Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
3.volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
4.线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
5.线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段加测到线程已经终止执行。
6.线程中断规则:对线程interrupt()方法的调用先发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
7.对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
8.传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。
12.4 Java与线程
12.4.1 线程的实现
12.4.2 Java线程调度
线程调度是指系统为线程分配处理器使用权的过程,主要调度方式有两种,分别是协同式线程调度和抢占式调度。
Java使用的是抢占式调度。每个线程将由系统来分配执行时间,线程的切换不由线程本身来决定。
12.4.3 状态转换
第13章线程安全与锁优化
13.1 概述
13.2 线程安全
线程安全的代码都必须具备一个特征:代码本身封装了所有必要的正确性保障手段,令调用者无须关心多想的问题,更无须自己采取任何措施来保证多线程的正确调用。
13.2.1 Java语言中的线程安全
线程安全的“安全程度”由强到弱,不可变、绝对线程安全、相对线程安全、线程兼容和线程对立。
1.不可变
不可变的对象一定是线程安全的。
2.绝对线程安全
3.相对线程安全
4.
13.2.2 线程安全的实现方法
1。互斥同步
同步是指在多线程并发访问共享数据时,保证共享数据在同一个时刻只被一个(或者是一些,使用信号量的时候)线程使用。而互斥是实现同步的一种手段,临界区、互斥量和信号量都是主要的互斥实现方式。
最基本的互斥同步手段就是synchronized关键字。
synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令,这两个字节码都需要一个reference类型的参数来指明要锁定和解锁的对象。
除了synchronized,还有重入锁ReentrantLock。相比synchronized,ReentrantLock增加了一些高级功能,有3项:等待可中断、可实现公平锁、锁可以绑定多个条件。
等待可中断是指当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步快很有帮助。
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
所绑定多个条件是指一个ReentrantLock对象可以同时绑定多个Condition对象。
2.非阻塞同步
互斥同步也称为阻塞同步。主要的问题就是进行线程阻塞和唤醒所带来的性能问题。
互斥同步属于一种悲观的并发策略。另外一种基于冲突检测的乐观并发策略,就是先进行操作,如果没有其他线程争用共享数据,那操作就成功了;如果共享数据有争用,产生了冲突,那就再采取其他的补偿措施(最常见的补偿措施就是不断地重试,直到成功为止),这个称谓非阻塞同步。
CAS精髓
3.无同步方案
如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施。
两种天生就是线程安全的代码:
a.可重入代码
b.线程本地存储:如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行?如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内。
如果一个变量要被某个线程独享,可以通过ThreadLocal类来实现线程本地存储。
13.3 锁优化
13.3.1 自旋锁与自适应自旋
有些情况下,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起会恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求所的那个线程“稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。
如果锁贝占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长,那么自旋的线程只会白白消耗处理器资源。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次。JDK1.6中引入了自适应的自旋锁。
13.3.2 锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持(第11章已经讲解过逃逸分析技术),如果判断在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把它们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。
13.3.3 锁粗化
如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
如果虚拟机探测到有这样一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。
13.3.4 轻量级锁
对象头的Mark Word是实现轻量级锁和偏向锁的关键。