前言
对于Java的学习也有一段时间了,却始终会有一些地方容易混淆,归结原因,还是偏底层的东西不太了解。前段时间便学习了关于Java虚拟机相关的内容,主要从阅读 《深入理解Java虚拟机》 进行总结。
首先Java技术体系主要由:Java第三方框架类库、Java API类库、Java程序设计语言、Class类文件格式、Java 虚拟机构成,把Java API类库、Java程序设计语言、Java虚拟机统称为JDK,用于支持Java程序开发的最小运行环境。
然后从 Java内存 相关的 内存模型
、 内存分配
、 垃圾回收
、 内存溢出
; 虚拟机执行子系统 相关的 Class类文件结构
、 类加载机制
、 字节码执行引擎
; 高效并发 相关的 Java内存模型与线程
、 线程安全与锁优化
几个部分进行了Java虚拟机初步的学习。
Java内存
内存模型
程序计数器
程序计数器(Program Counter Register)
可以看做当前线程所执行的字节码的行号指示器,每条线程都有一个独立的程序计数器,称为 线程私有
内存。
如果线程正在执行一个Java方法,计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是Native方法,则计数器值为空(Undefined)。
虚拟机栈
虚拟机栈(Virtual Machine Stacks)
是 线程私有
,每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表
、操作数栈
、动态链接
、方法出口
等信息(具体内容在后面会讲到)。每个方法从调用到执行完成对应一个栈帧从入栈到出栈。
经常把Java内存分为堆内存(Heap)和栈内存(Stack),这里指的栈就是虚拟机栈。
本地方法栈
本地方法栈(Native Method Stack)
与虚拟机栈作用类似,只不过不是为Java方法(也就是字节码)服务,而是为虚拟机使用的Native方法服务。
堆
堆(Heap)
是被 所有线程共享
的一块内存区域,在虚拟机启动时创建。用于存放对象实例,几乎所有的对象实例
以及数组
都在堆上分配内存。
Java堆可以处于物理上不连续的内存空间中,只要逻辑上连续即可,也是垃圾收集器管理的主要区域。
方法区
方法区(Method Area)
是 各个线程共享
的内存区域。用于存储已经被虚拟机加载的类信息
、常量
、静态变量
、即时编译器编译后的代码
等数据。
有别名叫Non-Heap(非堆),也被称为“永久代”。
额外:HotSpot虚拟机对象
对象的创建
- 类加载检查:虚拟机遇到new指令,检查这个指令的参数能否定位到一个类的符号引用;并检查这个符号引用代表的类是否已被加载、解析和初始化过。若没有,则先执行相应的类加载过程。
- 为对象分配内存:把一块确定大小的内存从Java堆中划分出来。由Java堆是否规整有两种划分方式,
“指针碰撞”
:中间放置一个指针作为分界点的指示器,分配内存就是指针向空闲空间挪动;“空闲列表”
:维护内存块可用的列表,分配内存就是从列表找出一块足够大的空间。 - 初始化零值:将分配到的内存空间都初始化为零值,不包含对象头。
- 必要设置:将对象的对象头信息取出进行必要的设置。
:执行new指令后会执行 方法,把对象按照程序员的意愿进行初始化。
对象的内存布局
- 对象头:对象头包含两部分信息,
“Mark Word”
用于存储对象自身的运行时数据;“类型指针”
用于存储对象指向它的类元数据的指针。 - 实例数据:在程序代码中所定义的各种类型的字段内容。
- 对齐填充:不是必然存在的,只是起着占位符的作用。
内存的访问定位
- 使用句柄:Java堆划分一块内存作为句柄池,reference存储句柄地址。句柄中包含
对象实例数据
和类型数据的具体地址
。 - 直接地址访问:Java堆的对象考虑如何放置访问类型数据的相关信息。reference存储的是对象地址。
内存溢出
Java堆溢出
Java堆用于存储对象实例,不断创建对象,当避免垃圾回收机制,在对象数量达到最大堆的容量限制后就会产生内存溢出异常。
虚拟机栈和本地方法栈溢出
- 线程请求的栈深度大于虚拟机所允许的最大深度,将抛出
StackOverflowError
异常。 - 虚拟机在扩展栈时无法申请到足够的内存空间,将抛出
OutOfMemoryError
异常。
方法区溢出
方法区用于存放Class相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等,当运行时产生大量的类填满方法区时会产生内存溢出。
垃圾回收
对象是否可回收
引用计数算法
引用计数算法
是给对象添加一个引用计数器,当一个地方引用时,计数器值加1;当引用失效时,计数器值减1;当计数器值为0时说明对象不可用。
不能解决对象之间相互循环引用的问题。
可达性分析算法
可达性分析
是选取 “GC Roots” 对象作为起始点,向下搜索走过的路径称为 引用链
;当一个对象到GC Roots没有任何引用链的时候则说明对象不可用。
Java语言中可作为引用链的对象包括:虚拟机栈、方法区中类静态属性、方法区常量、本地方法栈Native方法引用的对象。
两次标记
在可达性分析算法中不可达的对象还需要经历两次标记才真正回收。
- 是否有必要执行finalize()方法:当对象
没有覆盖finalize()方法
或者finalize()方法已经被虚拟机调用过
则视为没有必要执行。 - 重新引用:如果对象有必要执行finalize()方法,则会将对象放置在F-Queue队列中,由低优先级Finalizer线程执行,在此过程中只要
重新与引用链上一个对象建立关联
则会移除回收队列。
垃圾回收算法
标记-清除算法
- 标记所有需要回收对象。
- 回收所有被标记对象。
复制算法
将内存按容量划分为两块,每次只使用其中一块。
- 标记回收对象。
- 将存活对象复制到另一块。
- 已使用内存空间全部回收。
标记-整理算法
- 标记回收对象。
- 将存活对象向一端移动。
- 回收边界以外内存。
分代收集算法
根据对象存活周期将Java堆划分为新生代和老年代,不同年代使用不同的垃圾回收算法。
垃圾收集器
Serial收集器
Serial收集器
是负责新生代的收集的单线程收集器。垃圾回收时会暂停其他所有的工作线程
,直到收集结束。
ParNew收集器
ParNew收集器
是Serial收集器的多线程版本。除了Serial收集器,只有它能与CMS收集器配合工作。
Parallel Scavenge收集器
Parallel Scavenge收集器
也是使用复制算法的多线程收集器。目的是达到一个可控制的吞吐量, 吞吐量=运行用户代码时间 / (运行用户代码时间+垃圾回收时间)
,也被称为 “吞吐量优先”收集器
。
Serial Old收集器
Serial Old收集器
是Serial收集器的老年代版本,使用标记-整理算法的单线程收集器。
Parallel Old收集器
Parallel Old
是Parallel Scavenge收集器的老年代版本,使用标记-整理算法的多线程收集器。
CMS收集器
CMS收集器
是使用标记-清除算法的多线程收集器,目的是获取最短回收停顿时间。
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
G1收集器
G1收集器
在后台维护一个优先列表,根据允许的收集时间,优先回收价值(回收所获得的空间大小以及所需时间)最大的Region。
- 初始标记(Initial Marking)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)
- 筛选回收(Live Data Counting and Evacuation)
理解GC日志
GC日志是一些人为确定的规则,每个日志格式有收集器决定。通常来看由以下几个部分组成: GC发生时间
、 GC停顿类型
、 GC发生区域
、 GC前后内存使用情况
、 GC所占用时间
。
内存分配
对象优先在Eden分配
大多情况下,对象在新生代Eden区域中分配。Eden空间不足时,虚拟机发起一次Minor GC。Minor GC过程:将Eden区域对象放入Survivor空间,若无法放入则通过分配担保机制提前进入到老年代。
新生代GC(Minor GC):Minor GC频繁,回收速度快。
老年代GC(Major GC/Full GC):Full GC速度一般比Minor GC慢10倍。
空间分配担保
在Minor GC前,虚拟机会先检查老年代连续空间是否大于新生代对象总大小。若成立,则Minor GC安全;若不成立,虚拟机会查看是否允许担保失败。
担保:取每一次进入老年代对象的平均值与老年代剩余空间比较,若不足则进行Full GC。
大对象直接进入老年代
大对象指需要 大量连续内存空间
的Java对象。当所需空间大于设置值时直接进入老年代分配,目的在于避免在Eden区及两个Survivor区之间发生大量的内存复制。
长期存活的对象进入老年代
年龄计数器:对象在Survivor区每过一次Minor GC则年龄加1。当年龄大于设置值(默认为15)则进入老年代。
对象年龄动态判断:如果在Survivor空间中, 相同年龄
所有对象大小超过Survivor空间的一半,年龄大于或等于该年龄的对象进入老年代。
虚拟机执行子系统
Class类文件结构
概述
- Class文件是一组以
8个字节为单位
的二进制流,对应着类或接口的定义信息,是实现平台无关性
和语言无关性
的基础。 - Class文件格式采用
伪结构
存储数据,这种伪结构只有两种数据类型:无符号数(u1、u2、u4、u8代表x个字节的无符号数),表(由多个无符号数或其他表构成,习惯以_info结尾)。
Class文件格式
- 魔数:前
4个字节
,值为:0xCAFEBABE。 - Class版本号:紧接着魔数的
4个字节
,分别为:次版本号、主版本号。 - 常量池:紧接着Class版本号,常量数量
不固定
,入口放置一项u2类型的常量池容量计数器
。主要存放字面量
(Java中常量)和符号引用
(类、接口、字段、方法的名称和描述符)。 - 访问标志:紧接着常量池的
2个字节
,用于标识一些类或接口层次的访问信息
。 - 类索引、父类索引、接口索引:排列着访问标志之后,类索引和父类索引用两个u2类型表示,接口索引是一组u2类型的集合;索引用于确定
全限定名
来确定这个类的继承关系
。 - 字段表集合:用于描述接口或者类中声明的
变量信息
。字段信息需要引用常量池
中的常量来描述,无法固定大小。 - 方法表集合:与字段表相似,用于
描述方法
定义的标志、名称索引、描述符索引。 - 属性表集合:Class文件、字段表、方法表都可以携带自己的属性表集合,用于
描述特定信息
。预定义包含Code、Exception、LineNumberTable、LocalVariableTable等属性。
全限定名:把类全名中的”.”替换成了”/“,如com/baidu/www/class/TestClass。
简单名称:没有类型和参数修饰的方法或者字段名称,如inc()方法和m字段简称为inc和m。
描述符:描述字段的数据类型、方法的参数类型、返回值。
- 基本类型和void用一个大写字符表示,I。
- 对象类型用大写字符L加对象全限定名表示,Ljava/lang/String。
- 数组类型的每一个维度使用一个前置的[字符描述,如[[Ljava/lang/String、[I。
- 方法:先参数列表,后返回值,如()V、()Ljava/lang/String、([CII[CIII)I。
字节码指令
Java虚拟机的指令由操作码(一个字节长度的数字)和操作数(零至多个代表此操作所需的参数)构成。
字节码与数据类型
由于Java虚拟机的操作码长度只有1个字节,指令集将会故意被设计为非完全独立,即并非每种数据类型和每一种操作都有对应的指令。
大部分的指令都没有支持boolean、byte、char、short类型的操作,实际上都是使用相应的int类型作为运算类型。
加载和存储指令
加载和存储指令
用于将数据在栈帧中的局部变量表和操作数栈之间来回传输。
- 将局部变量加载到操作数栈:iload等。
- 将一个数值从操作数栈存储到局部变量表:istore等。
- 将一个常量加载到操作数栈:bipush、sipush、iconst_m1等。
- 扩充局部变量表的访问索引指令:wide。
运算指令
运算或算数指令
用于对两个操作数栈的值进行某种特定运算,并把结果重新存入到操作数栈顶。都使用Java虚拟机的数据类型,boolean、byte、char、short的运算都会转为int类型。
算数指令有:加法(iadd、ladd、fadd、dadd)、减法(sub)、乘法(mul)、除法(div)、求余(rem)、取反(neg)、位移、按位或、按位与、按位异或、局部变量自增、比较。
类型转换指令
类型转换指令用于将两种不同的数值类型进行相互转换。 宽化
类型转换无需显式的转换指令。 窄化
必须显示转换:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l、d2f。
其他指令
其他指令可以查看虚拟机字节码指令,这里不全部列出,主要有:对象创建与访问指令、操作数栈管理指令、控制转移指令、方法调用和返回指令、异常处理指令、同步指令。
虚拟机类加载机制
类加载机制指虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析、初始化,最终形成可以被虚拟机直接使用的Java类型。
类加载的时机
类的生命周期
- 加载、连接(验证、准备、解析)、初始化、使用、卸载。
对类主动引用
有且只有5中情况需要立即对类进行初始化:
- 遇到new、getstatic、putstatic、invokestatic这4个字节码时,如果类没有过初始化,则需要先触发其初始化。Java代码场景:
new实例化对象
、读取或设置一个类的静态字段
、调用一个类的静态方法
。 - 使用
java.lang.reflect包
的方法对类进行反射调用时,该类没有过初始化需要触发初始化。 - 当初始化一个类,其父类没有过初始化需要先初始化父类。
- 虚拟机启动时,需要先初始化含main()方法的主类。
- java.lang.invoke.MethodHandle实例解析句柄对应的类需要初始化。
被动引用
所有被动引用类都不会触发初始化。
- 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
- 通过数组定义来引用类,不会触发此类的初始化。
- 调用类的常量,由于常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用类,不会触发该类的初始化。
接口不要求其父接口都完成了初始化,只有在真正使用父接口的时候(引用接口的常量)才会初始化。
类加载的过程
Java虚拟机中类加载全过程:加载、验证、准备、解析、初始化。
加载
- 通过一个类的全限定名来获取定义此类的二进制流。
- 将这个字节流按照虚拟机所需的格式存储在方法区中。
- 在内存中生成一个代表这个类的java.lang.Class对象(HotSpot虚拟机存放在方法区中),作为方法区这个类的各种数据的访问入口。
验证
确保Class文件的字节流中包含的信息符合虚拟机的要求。
- 文件格式验证:对Class文件格式中魔数、版本号、常量池等进行验证,保证字节流能正常解析并存储到方法区。
- 元数据验证:对字节码描述的信息该类是否有父类、是否继承final类等进行语义分析,保证符合Java语言规范。
- 字节码验证:通过数据流和控制流分析,确定程序语义是符合逻辑的。
- 符号引用验证:对类自身以外(常量池中的各种符号引用)的信息进行匹配性验证。
准备
为类变量(static修饰)分配内存并设置初值(数据类型的零值)。
解析
虚拟机将常量池内的符号引用替换为直接引用。
- 符号引用:以一组符号来描述所引用的目标,引用目标并不一定已经加载到内存中。
- 直接引用:直接引用是直接指向目标的指针、偏移量或者是一个能够间接定位到目标的句柄。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄、调用点限定符这几类符号引用进行。
类或接口的解析
假设当前代码所处的 类为D
,要把一个从未解析过的 符号引用N
解析为一个 类或接口C
的直接引用:
- 如果C不是一个数组类型,虚拟机会把代表N的全限定名传递给D的类加载器去加载这个类C;可能会触发其他相关类的加载,如父类或实现的接口。
- 如果C是一个数组类型,数组的元素类型为对象按照上一点规则加载;数组的元素类型为基本类型则由虚拟机生成一个代表此数组维度和元素的数组对象。
- 如果以上步骤没有异常,则C在虚拟机中已经成为一个有效的类或接口。
字段解析
- 解析字段所属的类或接口的符号引用。
- 与类中匹配目标的简单名称和字段描述符。
- 按照继承关系从下往上递归搜索接口和父接口。
- 如果不是java.lang.Object,搜索其父类。
- 否则,查找失败。
类方法解析
- 解析方法所属的类或接口的符号引用。
- 在类中查找简单名称和描述符。
- 在类的父类中查找简单名称和描述符。
- 在类实现的接口列表和父接口中匹配。
- 否则,查找失败。
接口方法解析
- 解析方法所属的类或接口的符号引用。
- 在接口中查找简单名称和描述符。
- 在父接口中查找简单名称和描述符。
- 否则,查找失败。
初始化
开始执行类中定义的Java程序代码(字节码)。初始化阶段时执行类构造器
()方法是由编译器自动收集类中的类变量赋值和静态语句块。静态语句块中只能访问到定义在静态语句块之前变量, 之后的变量只能赋值不能访问
。()方法实例构造器 ()方法不同,不需要显式调用父类构造器,保证父类 ()方法在子类 。()方法前执行 - 由于父类先执行
()方法,父类定义的静态语句块先于子类,第一个被执行 ()方法的类是java.lang.Object。 ()方法对呀类或接口不是必需的。 - 虚拟机会保证一个类的
()方法在多线程环境中正确被加锁、同步。
类加载器
类加载器是实现让应用程序自己决定如何去获取所需要的类的代码模块。
类与类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一起确立其在Java虚拟机中的唯一性。
双亲委派模型
- 启动类加载器(Bootstrap ClassLoader):负责将存放在<JAVA_HOME\lib>目录下的类库加载到虚拟机内存中。
- 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME\lib\ext>目录中的类库。
- 应用程序类加载器(Application ClassLoader):负责加载用户类路径(ClassPath)指定的类库。
双亲委派模型的工作流程:如果一个类加载器收到类加载的请求,首先将这个请求委派给父类加载器去完成,最终传送到顶层的启动类加载器;当父加载器反馈无法完成这个加载请求,子加载器才会尝试加载。
破坏双亲委派模型
- 第一次:JDK 1.2发布前,重写loadClass()方法。
- 第二次:模型自身缺陷,线程上下文类加载器可以实现父类加载器请求子类加载器去完成类加载动作。
- 第三次:对程序动态性的追求。
字节码执行引擎
运行时栈帧结构
栈帧(Stack Frame)
是用于支持虚拟机进行方法调用和方法执行的数据结构,存储了局部变量表、操作数栈、动态连接、方法返回地址等信息。每一个方法从调用开始到执行完成对应栈帧在虚拟机从入栈到出栈的过程。
局部变量表
局部变量表(Local Variable Table)
是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
局部变量表的容量以变量槽(Variable Slot)为最小单位,每个Slot都能存放一个 boolean
、 byte
、 char
、 short
、 int
、 float
、 reference
、 returnAddress
类型的数据。
reference类型表示对一个对象的引用,通过引用要做到两点:从此引用中直接或间接地查找到到对象在 Java堆
中的数据存放的起始 地址索引
;此引用中直接或间接查找到对象所属数据类型在 方法区
中的存储的 类型
信息。
虚拟机通过索引定位的方式使用局部变量表。如果执行的实例方法(非static),局部变量表的第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字this访问该隐含参数。其他参数按照参数表顺序排序,局部变量表的Slot可以重用。
如果一个局部变量 定义
了但没有 赋初始值
会导致类加载失败。
操作数栈
操作数栈(Operand Stack)
是一个 后入先出(Last In First Out)
栈,每一个元素可以是任意的Java数据类型。
在方法执行过程,各种字节码指令往操作数栈中写入和提取内容,也就是入栈/出栈操作。
动态连接
每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。在每一次运行时期将符号引用转化为直接引用称为动态连接。
返回地址
方法在退出后,都需要返回到方法被调用的位置,栈帧保存返回地址信息。
方法调用
方法调用的目的是确定被调用方法的版本,即调用哪一个方法。
解析
所有的方法调用中的目标方法在Class文件中都是一个常量池中的符号引用。在类加载的解析阶段,将会把一部分符号引用转化为直接引用,解析前提是 “编译期可知,运行期不可变” 。
- 对应的调用字节码指令:invokestatic、invokespecial、invokevirtual、invokeinterface、invokedynamic。
- 符合条件:静态方法、私有方法、实例构造器、父类方法、final修饰方法。
分派
静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。(方法重载)
- 静态类型在编译期可知。
- 实际类型变化的结果在运行期确定。
重载方法匹配优先级
以’a’为例:
- char->int->long->float->double
- java.lang.Character
- java.lang.Serializable、java.lang.Comparable
- 装箱转型为父类
- 变长参数
动态分派
运行期根据实际类型确定方法执行版本的分派过程称为动态分派。(方法重写)
单分配与多分配
方法宗量:方法接受者+方法参数。根据分派基于多少种宗量划分为单分配和多分配。
- 静态分派:选择目标方法。(静态类型+方法参数)
- 动态分派:方法接受者的实际类型。
Java是静态多分配、动态单分配的语言。
高效并发
Java内存模型与线程
Java内存模型
主内存与工作内存
Java内存模型的目的是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量的底层细节。
这里的变量与Java编程中所说的变量不同,它包括实例字段、静态字段、构成数组对象的元素,不包括局部变量和方法参数。
Java内存模型规定
- 所有变量都存储在主内存。
- 每天线程有自己的工作内存。
- 工作内存保存主内存的副本拷贝。
- 线程对变量的所有操作(读取、赋值等)都在工作内存中进行。
主内存对应于Java堆中的对象实例数据部分。
工作内存对应虚拟机栈中的部分区域。
内存间交互操作
- lock:把一个变量标识为一条线程独占状态。
- unlock:释放锁定变量。
- read:把变量从主内存读取到工作线程。
- load:把read操作读取的变量值放入工作内存变量副本中。
- use:把工作内存的变量值传递给执行引擎。
- assign:从执行引擎接收变量值到工作线程。
- store:把工作线程的变量值传到主内存中。
- write:把store操作得到的变量值放入主内存的变量中。
原子性、可见性、有序性
- 原子性:线程从运行开始会一直到运行结束,不会被方法调度打断或进行线程切换。
- 可见性:当一个线程修改了共享变量的值,其他线程能够立即得到这个修改。
- 有序性:禁止指令重排序,在本线程中表现为串行,整体表现为指令重排序。
volatile关键字
- 保证了可见性和有序性。
- Java运算并非原子操作,导致volatile变量的运算在并发下不安全。
- 需要确保运算结果并不依赖变量的当前值来保证原子性。
synchronized同步块同时保证了原子性、可见性、有序性。
先行发生原则
先行发生是Java内存模型中定义的两项操作之间的偏序关系。如果操作A先行于操作B,则操作A产生的影响能够被操作B观察到。
Java内存模型预定义的先行发生关系
- 程序次序规则:一个线程中,按照程序代码顺序。
- 管程锁定规则:unlock操作先行于同一个锁的lock操作。
- volatile变量规则:volatile变量的写操作先行于后面的读操作。
- 线程启动规则:Thread对象的start()方法优先。
- 线程终止规则:Thread.join()方法结束最后。
- 线程中断规则:interrupt()方法先行于中断事件。
- 对象终结规则:一个对象的初始化完成先行于finalize()方法。
- 传递性:A先行于B,B先行于C,则A先行于C。
时间先后顺序与先行发生规则基本没有关系。
Java与线程
线程的实现
各个线程既可以共享进程资源,又可以独立调度。
实现线程的方式
- 使用内核线程实现:直接由操作系统内核支持的线程,用内核线程支持 轻量级进程(LWP) 实现。
- 使用用户线程实现:用户线程(UT) 完全建立在用户空间的线程库,不需要切换到内核态。
- 混合实现:既存在用户线程,也存在轻量级进程。
Java线程调度
线程调度指系统为线程分配处理器的使用权。
- 协同式调度:线程的执行时间由自己控制,线程自身执行完后主动通知系统切换到另一个线程。
- 抢占式调度:每个线程由系统分配执行时间,可以设置线程优先级。
状态转换
- 新建(New):创建后尚未启动的线程。
- 运行(Runable):包括Running和Ready。
- 无限期等待(Wating):等待其他线程显式唤醒。
- 限期等待(Timed Wating):一定时间后由系统自动唤醒。
- 阻塞(Blocked):等待获取一个排它锁,在另一个线程放弃这个锁时发生。
- 结束(Terminated):已终止线程。
线程安全与锁优化
线程安全
线程安全指当多个线程访问一个对象时,调用这个对象的行为都可以像单线程一样得到正确的结果。
共享数据类型
- 不可变:不可变的对象一定是线程安全的,如String。
- 绝对线程安全:在多线程环境中需要在方法调用端做额外的同步措施。
- 相对线程安全:需要保证这个对象单独的操作是线程安全的,如:Vector、HashTable等。
- 线程兼容:对象本身并不是线程安全的,可以通过在调用端正确使用同步手段保证线程安全,如ArrayList、HashMap等。
- 线程对立:无论调用端是否采用同步措施,都无法在多线程环境中并发使用。
线程安全的实现
- 互斥同步:synchronized或java.util.concurrent包中ReentrantLock实现。
- 非阻塞同步:先进行操作,产生了冲突再采取补偿措施,也称为“乐观锁”。
- 无同步方案:可重入代码、线程本地存储。
锁优化
- 自旋锁:不放弃处理器的执行时间,让线程执行一个忙循环(自旋)。
- 锁消除:对一些代码要求同步却被检测到不可能存在共享数据竞争的锁进行消除。
- 锁粗化:如果一系列的连续操作都对同一个对象反复加锁和解锁,则将加锁同步范围扩展到整个操作序列的外部。
- 轻量级锁
- 偏向锁