为什么正数不断无符号右移(>>>)的最小值是0;负数不断无符号右移(>>>)的最小值是1 ?
在实际编程中,无符号右移运算一般只用于短整型(32位)和长整型(64位)数上。由于移位运算实际上移动的位数是一个mod32或者mod64的结果(包括有符号位移“>>“ "<<"也是取余运算)。
对于短整型数(32位)来说,移动的位数是一个mod32的结果,即35>>>1与35>>>33的结果是一样的(1%32=33%32=1);如果是长整型数(64位),移动的位数则是一个mod64的结果,即35>>>1与35>>>65的结果过是一样的(1%64=65%64=1)。
而移动32位(短整型)和64位(长整型)的结果都是其本身(32%32=0,64%64=0)。所以移位运算能移动的最大位数是31位(短整型)和63位(长整型)。因为无符号右移时,正负数高位均补0,所以正数不断无符号右移的最小值是0,而负数不断无符号右移的最小值是1。下面举例说明:
在计算机中存储和操作的数都是二进制补码形式。
正数的原码、反码、补码均相同。
负数的反码是原码符号位不动、剩余各位按位取反。
负数的补码是原码符号位不动、剩余各位按位取反、再加1(即反码+1)。【即:负数的补码转为原码也是补码符号位不动、剩余各位按位取反、再加1】
正数:
32的原码、反码、补码的二进制形式是
0000_0000_0000_0000_0000_0000_0010_0000
不断无符号右移,直至右移31位
0000_0000_0000_0000_0000_0000_0000_0000
所以正数不断无符号右移的最小值为0;
负数:
-32的原码二进制形式
1000_0000_0000_0000_0000_0000_0010_0000
原码
-32的反码二进制形式【符号位不动,原码按位取反】
1111_1111_1111_1111_1111_1111_1101_1111
反码
-32的补码二进制形式【符号位不动,原码按位取反,再加1】
1111_1111_1111_1111_1111_1111_1110_0000
补码
不断无符号右移,直至右移31位
0000_0000_0000_0000_0000_0000_0000_0001
= 1
所以负数不断无符号右移的最小值为1。
浮点数的标准是IEEE754,单精度和双精度的取值范围如下表所示(来自维基百科 ):
因为浮点数无法表示零值,所以取值范围分为两个区间:正数区间和负数区间。
以单精度类型为例,它占据4个字节,总共32位,具体格式如图1-3所示:
在规格化表示上存在差异,称谓有所改变,指数称为”阶码“,有效数字称为”尾数“,所以用于存储符号、阶码、尾数的二进制位分别称为符号位、阶码位、尾数位。
符号位S
在最高二进制位上分配1位表示浮点数的符号,0表示正数,1表示负数。
阶码位E
在符号位右侧分配8位用来存储指数,IEEE754标准规定阶码存储的是指数对应的移码,而不是指数的原码或者补码。根据计算机组成原理中对移码的定义可知,移码是将一个真值在数轴上正向平移一个偏移量之后得到的,即[x] 移 = x + 2n-1(n为x的二进制位数,含符号位)。移码的几何意义是把真值映射到一个正数域,其特点是可以直观反映两个真值的大小,即移码大真值也大。
由于阶码实际存储的是指数的移码。假设指数的真值用e表示,阶码用E表示,则有 E=e + (2n-1 - 1),(即 E = e +127)。其中 2n-1 - 1是IEEE754标准规定的偏移量,n = 8是阶码的二进制位数。
为什么偏移量是 2n-1 - 1 而不是 2n-1 呢?因为8个二进制位能表示的取值范围是[-128,127],现在将指数用移码表示,即将区间 [-128,127] 正向平移到正数域,区间里的每一个数都需要加上128,从而得到阶码E范围为 [0,255] 。由于计算机组成原理规定阶码全为0或者全为1的两种情况被当做特殊值处理(全0认为是机器零,全1认为是无穷大),去除这两个特殊值,阶码E的取值范围变成了 [1,254] 。如果偏移量不变仍为128的话,根据换算关系E = e +128 得到指数e的范围是 [-127,126] ,指数最大只能取到126,显然会缩小浮点数能表示的取值范围。所以IEEE754标准规定单精度的阶码偏移量为 2n-1-1(即127),这样能表示的指数范围为[-126,127],指数最大值能取到127。
尾数位M
最右侧分配连续的23位用来存储有效数字,IEEE754标准规定尾数以原码表示。为了节约存储空间,将符合规格化的首个1省略,所以位数表面上是23位,却表示了24位二进制数,如图1-4所示。
单精度浮点数的真值 = (-1)s × 1.M × 2E-127
正指数和有效数字的最大值决定了32位存储空间能表示浮点数的十进制最大值。
指数最大值为2127≈1.7×1038,有效数字部分最大的二进制值是1.111…111(小数点后23个1),是一个无限接近2的数字,所以得到最大的十进制数为2×1.7×1038,再加上最左1位的符号位,最终得到32位浮点数最大值为3.4e+38。为了方便阅读,从右向左每4位用短竖线隔开:
0111_1111_0111_1111_1111_1111_1111_1111
- 首位表示符号位,二进制值为
0
,表示正数。- 中间8位表示阶码位(用移码表示)即指数部分,二进制值为
1111_1110
=254,表示2254-127=2127≈1.7×1038。- 末尾23位表示尾数位即有效数字,二进制值为
111_1111_1111_1111_1111_1111
,再加上小数点前一位1,实际值为一个无限接近2的数字。指数最小值为2-126≈1.175×10-38,有效数字部分最小的二进制值是1.000…000(小数点后23个0),数值为1,所以得到最大的十进制数为1.18×10-38,再加上最左1位的符号位,最终得到32位浮点数最小值为1.18e-38。为了方便阅读,从右向左每4位用短竖线隔开:
0000_0000_1000_0000_0000_0000_0000_0000
- 首位表示符号位,二进制值为
0
,表示正数。- 中间8位表示阶码位(用移码表示)即指数部分,二进制值为
0000_0001
=1,表示21-127=2-126≈1.18×10-38。- 末尾23位表示尾数位即有效数字,二进制值为
000_0000_0000_0000_0000_0000
,再加上小数点前一位1,实际值为1。
为什么1.0 - 0.9的结果为0.100000024,而不是理论值0.1。
1.0 - 0.9等价于1.0 + (-0.9)首先分析1.0和 -0.9的二进制编码:
1.0的二进制为0011_1111_1000_0000_0000_0000_0000_0000
。
-0.9的二进制为1011_1111_0110_0110_0110_0110_0110_0110
。
从上可以得出二者的符号位、阶码位、尾数位,如表1-5所示。
由于尾数位最左端隐藏了一位1,所以实际尾数二进制分别为:1000_0000_0000_0000_0000_0000
和1110_0110_0110_0110_0110_0110
,红色为隐藏位。运算过程如下:
(1)对阶。1.0的阶码位是127,-0.9的阶码位是126,比较阶码位大小后需要向右移动-0.9尾数的补码,使-0.9的阶码位变为127,同时尾数位补码的高位补1,移动后的结果为1000_1100_1100_1100_1100_1101
,最做的1是高位补进的。
(2)尾数求和。因为尾数都已转换为补码,所以可以直接按位相加,注意符号位也要参与运算,如图1-5所示。
其中最左端为符号位,计算结果为0,表示结果是一个正数;尾数位计算结果为0000_1100_1100_1100_1100_1101
。
(3)规格化。上一步的结果斌不符合要求,尾数的最高位必须是1,所以需要将计算结果向左移动4位,同时阶码位减4,移动后阶码等于123(二进制为01111011
),尾数为1100_1100_1100_1100_1101_0000
。再隐藏尾数的最高位,进而变为100_1100_1100_1100_1101_0000
。
综上所述,得出运算结果是符号位是0、阶码位是0111_1110
、尾数位是100_1100_1100_1100_1101_0000
,三部分组合起来就是1.0 - 0.9的结果0011_1111_0100_1100_1100_1100_1101_0000
,对应的十进制值是0.100000024。
TCP/IP(Transmission Control Protocol / Internet Protocol)中文译为传输控制协议/因特网互联协议,TCP/IP是四层传输协议,从上到下分为①应用层、②传输层、③网际层( 用网际层这个名字是强调这一层是为了解决不同网络的互连问题) 、④网络接口层。另一个耳熟能详的ISO/OSI七层传输协议,OSI(Open System Interconnection) 是国际标准化组织(International Organization for Standardization,ISO) 设计的计算机通用的网络通信基本框架,但已被淘汰。不过从实质上讲,TCP/IP 只有最上面的三层,因为最下面的网络接口层并没有什么具体内容,因此在学习计算机网络的原理时往往采用折中的办法,即综合 OSI 和 TCP/IP 的优点,采用一种只有五层协议的体系结构,这样既简洁又能将概念阐述清楚,有时为了方便,也可把最底下两层称为网络接口层。
三次握手(TCP建立连接的三个步骤)
在学习TCP连接之前,先来了解一下TCP报文的头部结构。
上图中有几个字段需要重点介绍下:
(1)序号:seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
(2)确认序号:ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,ack=seq+1。
(3)标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等。具体含义如下:其中标志位有3个比较重要:SYN(Synchronize Sequence Numbers)用作建立连接的同步信号;ACK(Acknowledgement)用于对收到的数据进行确认,所确认的数据由确认序列号表示;FIN(Finish)表示后面没有数据需要发送,通常意味着所建立的连接需要关闭了。SYN、ACK、FIN都以置1表示有效。
需要注意:不要将确认序号ack和标志位ACK搞混淆了;确认方ack=发起方seq+1。
A机器是客户端角色,B机器是服务器角色,服务器需要客户端发起连接建立请求时先打开某个端口等待数据传输,否则无法正常建立连接。图1-18展示了正常情况下三次握手具体步骤:
第一次握手:客户端向服务器端发起连接请求,首先客户端随机生成一个初始序列号seq=x(假设x是100),客户端向服务器端发送的SYN报文段包含SYN标志位(即SYN=1),序列号seq=x=100。此时,客户端进入SYN_SENT
(同步已发送)状态。
初始序列号(即Initial Sequence Number,ISN)
第二次握手:服务器端收到客户端发过来的SYN报文段后,发现SYN=1,得知这是一个建立连接的请求,于是将客户端的序列号seq=x=100保存起来,并且随机生成一个服务器端的初始序列号seq=y(假设y是200)。然后服务器端向客户端发送一个确认报文段,ACK报文段包含SYN和ACK标志位(即SYN=1,ACK=1),序列号seq=y=200,确认序列号ack=x+1=101(客户端发送的序列号+1)。此时,服务器端进入SYN_RCVD
(同步已收到)状态。
第三次握手:客户端收到服务器端的ACK报文段后发现ACK=1且ack=101,知道服务器端收到了序列号为100的SYN报文段;同时发现SYN=1,表示服务器端同意了此次连接请求,于是将服务器端的序列号seq=y=200保存起来,然后向服务器端发送确认报文段,ACK报文段包含ACK标志位(即ACK=1),序列号seq=x+1=101( 第一次握手时客户端发送报文的序列号seq是100,所以第三次握手的序列号就seq从101开始,需要注意的是不携带数据的ACK报文是不占据序列号的,所以后面第一次正式发送数据时序列号seq还是101 ),确认序列号ack=y+1=201(服务器端序列号+1)。此时,客户端进入ESTABLISHED
(连接已建立)状态。服务器端发现收到的ACK报文段包含ACK=1且ack=201,就知道客户端收到了序列号为200的报文。 此时,服务器端也进入ESTABLISHED
(连接已建立)状态。这样客户端和服务器端就建立了TCP连接。
TCP建立连接为什么需要3次握手?
主要是确认通信双发收发数据的能力。通信双方只有确定4类信息(自己发报能力、自己收报能力、对方发报能力、对方收报能力),才能建立连接。在第2次握手以后,从B机器视角看还有两个红色的No信息无法确认。在第3次握手后,B机器才能确认自己的发报能力和对方的收报能力是正常的。
四次挥手(TCP断开连接的四个步骤)
TCP是面向连接的全双工通信,双方都能作为数据的发送方和接收方,TCP建立连接需要3次,但是断开连接却需要4次,图1-23展示了正常情况下四次挥手的具体步骤:
假如客户端的初始化序列号ISN=100,服务器端的初始序列号ISN=200,TCP连接建立后客户端总共发送了3000个字节的数据,服务器端在客户端发送FIN报文前总共回复了4000个字节的数据。
FIN_WAIT_1
(终止等待1)状态。需要注意的是客户端发出FIN报文段后只是不能发数据了,但是还可以正常收数据;另外FIN报文段即使不携带数据也要占据一个序列号。CLOSE_WAIT
(关闭等待)状态。这个状态可能要持续一段时间,而不是立刻给服务器端发送FIN报文段,因为服务器端可能还有数据没发完。客户端收到服务器端的ACK报文段后,就进入FIN_WAIT_2
(终止等待2)状态。等待服务器端发出FIN报文段。LAST_ACK
(最后确认)状态,等待客户端的确认。CLOSED
(关闭连接)状态。服务器端收到ACK报文段后,立即断开连接,进入CLOSED
(关闭连接)状态。这样客户端和服务器端就断开 了TCP连接。**TIME_WAIT:**表示主动关闭产生的阶段性状态,只有主动要求关闭的机器表示收到了对方的FIN报文,并发送出ACK报文,进入TIME_WAIT状态,等2MSL后即可进入CLOSED状态。如果FIN_WAIT_1状态下,同时收到待FIN标志和ACK标志的报文时,可以直接进入TIME_WAIT状态,则无须经过FIN_WAIT_2状态。
**CLOSE_TIME:**表示被动阶段产生的阶段性状态,被动要求关闭的机器收到对方请求关闭连接的FIN报文,在第一次ACK应答后,马上进入CLOSE_TIME状态。这种状态其实表示在等待关闭,并且通知机器发送完剩余数据,等待关闭相关资源。
为什么第四次挥手后客户端要等2MSL的时间才能释放TCP连接?
在TIME_WAIT等待的2MSL是报文在网络上生存的最长时间,超过阈值的报文会被丢弃。一般来说,MSL大于TTL衰减至0的时间。这里主要是要考虑丢包的问题,如果第四次挥手的报文丢失,服务器端没收到确认ACK报文端就会重发第三次挥手的报文,这样报文一去一回最长时间就是2MSL,所以需要等这么长时间来确认服务端确实已经收到了。
为什么TCP建立连接需要3次握手,而断开连接需要4次挥手?
当数据传输完毕时,通信的双方都可以请求断开连接。客户端第一次挥手发出FIN报文后只能保证客户端没有数据要发送,而服务器端还有没有数据要发客户端是不知道的。所以服务器端收到客户端的FIN报文后只能先回复客户端一个确认报文来告诉客户端 服务器端已经收到你的FIN报文了,但服务器端还有一些数据没发完,等这些数据发完 服务器端才能给客户端发FIN报文(所以不能一次性将确认报文和FIN报文发给客户端,就是这里多出来了一次)。
传统意义上,面向对象有三大特性:封装、继承、多态。本书明确将“抽象”作为面向对象的特征之一。抽象体现出程序员对业务的建模能力,以对象模型为核心,丰富模型的内涵,扩展模型的外延,通过模型的行为组合去共同解决某一类问题;封装是一种对象功能内聚的表现形式,是模块之间耦合度变低,更具有维护性;继承使子类能够继承父类,获得父类的部分属性和行为,使模块更有复用性;多态是模块在复用性基础上更加有扩展性。
接口和抽象类是对实体类进行更高层次的抽象,仅定义公共行为和特征。接口与抽象类的共同点是都不能被实例化,但可以定义引用变量指向实例对象。二者的不同点如下表:
内部类具体分为4种:
static class StaticInnerClass {}
private class InstanceInnerClass {}
(new Thread() {}).start()
public class OuterClass{
// 成员内部类
private class InstanceInnerClass {
}
// 静态内部类
static class StaticInnerClass {
}
public static void main(String[] args){
// 匿名内部类
(new Thread() {
}).start();
// 方法内部类
class MethodInnerClass {
}
}
}
Java中用于控制可见性的4个访问修饰符:
不同点 | 基本概念 | 查找范围 | 特殊功能 |
---|---|---|---|
this | 访问本类中属性和方法(包括构造方法) | 先找本类,没有则找父类 | 单独使用时,表示当前对象,如在同步代码块中synchronized(this){...} |
supper | 子类访问父类中的属性和方法(包括构造方法) | 直接查找父类 | 在子类复写父类方法时,可以使用super调用父类同名方法 |
共同点:如果this和super指代构造方法,则必须位于方法体的第一行。
Java中类之间的关系有:
继承和实现是比较容易理解的两种类关系。
聚合是一种可以拆分的整体与部分的关系,部分可以被拆出来给另一个整体。如:汽车包含轮胎和方向盘。
依赖就是一个类A中使用了另一个类B,即类A中import类B,那就是依赖关系。
工程师一般采用UML(Unified Modeling Language,统一建模语言)来绘制类图。在类图中,用**“空心的三角形“+”实线”来表示继承;用“空心的三角形+虚线”来表示实现;用“箭头+虚线”来表示依赖;用“空心的菱形+实线”**来表示聚合。
方法签名包括方法名和参数列表,是JVM标识方法的唯一索引,不包括方法返回值,更不包括访问权限修饰符、抛出异常等。假如方法返回值可以是方法签名的一部分,仅仅是从代码的可读性角度来考虑的。
注意区分【隐式参数、显示参数】和【形式参数、实际参数】的区别。
**形式参数:**是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数,简称“形参”。
**实际参数:**在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”,简称“实参”。
raiseSalary(double byPresent)
{
double raise = salary * byPresent /100;
salary += raise;
}
显示参数:显式参数是在方法中括号中声明的参数。例如:方法raiseSalary(double byPresent)
中形式参数byPresent就是显示参数。
隐式参数:隐式参数表示在方法中使用,但是没有写在方法参数中也没有在方法体内声明的变量。例如:raiseSalary
的方法体中salary表示隐式参数。
一般,我们使用this关键字来表示隐式参数。this关键字可以很清晰的把显示参数和隐式参数分开。上文中的代码还可以写为一下形式:
raiseSalary(double byPresent)
{
double raise = this.salary * byPresent /100;
this.salary += raise;
}
构造方法(Constructor)是方法名与类名相同的特殊方法,在创建对象是调用,它有如下特征:
在接口中不能定义构造方法,但在抽象类中可以定义。在枚举类中,构造方法时特殊的存在,它可以定义,但不能加public修饰,因为它是默认private的,是绝对的单例。
构造方法中不应该引入业务逻辑代码。如果在一个对象生产中,需要完成初始化上下游对象,分配内存,执行静态方法等工作,建议将此初始化业务逻辑放入某个方法中,比如init()
方法中。
父子类中静态代码块、构造代码块和构造方法的执行顺序:
public class Son extends Parent { // 静态代码块 static { System.out.println("Son: 静态代码块"); } // 构造代码块 { System.out.println("Son:构造代码块"); } // 构造方法 public Son() { System.out.println("Son:构造方法"); } public static void main(String[] args) { new Son(); new Son(); } } class Parent { // 静态代码块 static { System.out.println("Parent: 静态代码块"); } // 构造代码块 { System.out.println("Parent:构造代码块"); } // 构造方法 public Parent() { System.out.println("Parent:构造方法"); } }
执行结果:
从上面的执行结果可以看出,在创建对象时,会先执行父类和子类的静态代码块,然后在执行父类的构造代码块和构造方法,最后执行子类的构造代码块和构造方法。静态代码块只运行一次,在第二次对象实例化时,不会再运行。
**重写:**发生在子类实现接口,或者继承父类时。
@Override
注解。此时编译器会自动检查方法签名是否完全相同,避免了重写时因写错方法名或参数列表而导致重写失败。此外,@Override
注解还可以避免因访问权限修饰符范围导致的重写失败。Animal
,那么子类的方法返回值类型可以是Cat|Dog|Animal
。class Cat extends Animal {...}
class Dog extends Animal {...}
重写只能针对父类中的非静态、非final、非构造方法。
重载发生在同一个类中,方法名必须相同,参数列表不同(参数类型不同、参数个数不同、参数顺序不同),方法返回值和访问修饰符可以不同。
重写是用动态绑定完成,发生在运行时。
重载是用静态绑定完成,发生在编译时期。
泛型的本质是类型参数化,解决不确定具体对象类型的问题。
泛型可以定义在类、接口、方法中,编译器通过识别尖括号和尖括号内的字母来解析泛型。在泛型定义时,约定俗成的符号包括:E代表Element,用于集合中的元素;T代表Type,表示某个类型;K代表Key、V代表Value,用于键值对。
Java中8种基本数据类型的默认值、空间占用大小、表示范围及对应的包装类等信息如表所示。
类型 | 默认值 | 大小(字节) | 表示范围 | 包装类 | 缓存区间 |
---|---|---|---|---|---|
boolean | false | 1 | false、true | Boolean | 无 |
char | ‘\u0000’ | 1 | ‘\u0000’ ~ ‘\FFFF’ | Character | 0~127 |
byte | 0(byte) | 1 | -128 ~ 127 | Byte | -128~127 |
short | 0(short) | 2 | -215 ~ 215-1 | Short | -128~127 |
int | 0 | 4 | -231 ~ 231-1 | Integer | -128~127 |
long | 0L | 8 | -263 ~ 2 63-1 | Long | -128~127 |
float | 0.0F | 4 | ±1.18×10-38 ~ ±3.40×1038 | Float | 无 |
double | 0.0D | 8 | ±2.23×10-308±1.80×10308 | Double | 无 |
如图2-10所示,对象分为三块区域:对象头、实例数据、对齐填充。
(1)对象头(Object Header)
对象头占用12个字节,存储内容包括对象标记(MarkOop)和类元信息(klassOop)。对象标记存储对象本身运行时的数据,如哈希码、GC标记、锁信息、线程关联信息等,这部分数据在64位JVM上占用8个字节,称为“Mark Word”。为了存储更多的状态信息,对象标记的存储格式是非固定的(具体与JVM的实现有关)。类元信息存储的是对象指向它的类元数据(即Klass)的首地址,占用4个字节(开启压缩指针)。
(2)实例数据(Instance Data)
存储本类对象的实例成员变量和所有可见的父类成员变量。
(3)对齐填充(Padding)
对象的存储空间分配单位是8个字节,如果一个占用大小为16个字节的对象,为其增加一个成员变量byte类型,此时需要占用17个字节,但是也会分配24个字节进行对齐填充操作。
8种基本数据类型都有相应的包装类,但除了Float、Double和Boolean外,其他包装数据类型都会有缓存。以Integer为例,我们知道Integer的缓存区间在-127~128之间。对于Integer i = ?
在-128~127之间的赋值,Integer对象由IntegerCache.cache
产生,会复用已有对象,这个区间的Integer值可以直接使用==
进行判断,但是这个区间之外的的所有数据都会在对上产生,并不会复用已有对象。推荐所有包装类对象之间值的比较,全部使用equals()
方法。
Java中的字符串主要有三种:String
、StringBuilder
、StringBuffer
。String是只读字符串,典型的不可变字(immutable)符串,对于它的任何变动,其实都是创建一个新对象,再把引用指向该对象。String对象赋值操作后,会在字符串常量池中进行缓存,如果下次再重新创建相同字面值的对象时,缓存中已经存在,会直接返回该引用给创建者。不可变字符串的优点:编译器可以让字符串共享。StringBuilder和StringBuffer则可以在原对象上进行修改,StringBuffer是线程安全的,StringBuilder是非线程安全的。
命名规范参考阿里巴巴《开发手册》(泰山版)1.6.0
Java所有的指令有200个左右,一个字节(8位)可以存储256种不同的指令信息,一个这样的字节称为字节码(Bytecode)。字节码是为了解决Java跨平台运行问题的。
字节码必须通过类加载过程加载到JVM环境后,才可以执行。执行有三种模式:
第一,解释执行;第二,JIT编译执行;第三,JIT 编译与解释混合执行(主流JVM默认执行模式)。混合执行模式的优势在于解释器在启动时先解释执行,省去编译时间。随着时间推进,JVM通过热点代码统计分析,识别高频的方法调用、循环体、公共模块等,基于强大的JIT动态编译技术,将热点代码转换成机器码,直接交给CPU执行。JIT的作用是将Java字节码动态地编译成可以直接发送给处理器指令执行的机器码。简要流程如图4-3所示。
在冯•诺依曼定义的计算机模型中,任何程序都需要加载到内存才能与CPU进
行交流。字节码.class文件同样需要加载到内存中。ClassLoader的使命正是提前加载.class类文件到内存中。在加载类时,使用的是双亲委派模式(Parents Delegation Model)。
Java的类加载器是一个运行时核心基础设施模块, 如图4-4所示,主要是在启动之初进行类的Load、Link 和Init,即加载、链接、初始化。
第一步,Load阶段读取类文件产生二进制流,并转化为特定的数据结构,初步校验cafe babe魔法数、常量池、文件长度、是否有父类等,然后创建对应类的java.lang.Class实例。
第二步,Link阶段包括①验证、②准备、③解析三个步骤。验证是更详细的校验,比如final是否合规、类型是否正确、静态变量是否合理等;准备阶段是为静态变量分配内存,并设定默认值;解析类和方法确保类与类之间的相互引用正确性,完成内存结构布局。
第三步,Init 阶段执行类构造器clinit
方法,如果赋值运算是通过其他类的静态方法来完成的,那么会马. 上解析另外一个类,在虛拟机栈中执行完毕后通过返回值进行赋值。
类加载是一个将.class字节码文件实例化成Class对象并进行相关初始化的过程。在这个过程中,JVM会初始化继承树上还没有被初始化过的所有父类,并且会执行这个链路上所有未执行过的静态代码块、静态变量赋值语句等。某些类在使用时,也可以按需由类加载器进行加载。
cafe babe魔法数是Java之父Gosling定义的一个魔法数,意思是Coffee Baby,其十进制为3405691582。它的作用是:标志该文件是一个Java类文件,如果没有识别到该标志,说明该文件不是Java类文件或者文件已受损,无法进行加载。
内存是非常重要的系统资源,是硬盘和CPU的中间仓库及桥梁,承载着操作系统和应用程序的实时运行。JVM内存布局规定了Java在运行过程中内存申请、分配、管理的策略,保证了JVM的高效稳定运行。不同的JVM对于内存的划分方式和管理机制存在着部分差异。结合JVM虚拟机规范,来探讨一下经典的JVM内存布局,如图4-8所示。
1.Program Counter Register(程序计数寄存器)
在程序计数寄存器中(Program Counter Register,PC)中,Register的命名源于CPU的寄存器,CPU只有把数据装载到寄存器才能够运行。寄存器存储指令相关的现场信息,由于CPU的时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器用来存放执行指令的偏移量和行号指示器等,线程执行或恢复都需要依赖程序计数器。程序计数器在各个线程之间互不影响,此区域也不会发生OutOfMemoryError异常。
2. JVM Stack(虚拟机栈)
栈(Stack)是一个先进后出的数据结构。
Java虚拟机栈(Java Virtual Machine Stack,JVM Stack)是描述Java方法执行的内存区域,它与程序计数器一样,也是线程私有的,它的生命周期与线程相同。栈中的元素用于支持虚拟机进行方法调用,每个方法从开始调用到执行完成的过程,就是栈帧从入栈到出栈的过程。在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧。正在执行的方法称为当前方法,栈帧是方法运行的基本结构,在执行引擎运行时,所有指令都只能针对当前栈帧进行操作。而StackOverflowError表示栈溢出异常,导致内存耗尽,通常出现在递归方法中。操作栈的压栈与出栈操作如图所示:
虚拟机栈通过压栈和出栈的方式,对每个方法对应的活动栈帧进行运算处理,方法正常执行结束,肯定会跳转到另一个栈帧上。在执行的过程中,如果出现异常,会进行异常回溯,返回地址通过异常处理表确定。栈帧包括局部变量表、操作栈、动态连接、方法返回地址等。
(1)局部变量表
局部变量表是存放方法参数和局部变量的区域,相对于类属性变量的准备阶段和初始化阶段来说,局部变量表没有准备阶段,必须显式初始化。如果是非静态方法,则是在index[0]位置上存储的是方法所属对象的实例引用,随后存储的是参数和局部变量表。字节码指令中的STORE指令就是将操作栈中的计算完成的局部变量写回局部变量表的存储空间内。
(2)操作栈
操作栈是一个初始状态为空的桶式结构栈。在方法执行过程中,会有各种指令往栈中写入和读取信息。JVM的执行引擎是基于栈的执行引擎,其中的栈指的就是操作栈。
(3)动态连接
每个栈帧中包含一个在常量池中对当前方法的引用,目的是支持方法调用过程的动态连接。
(4)方法返回地址
方法执行是有两种退出情况:1、正常退出,即正常执行到任何方法的返回字节码指令,如RETURN、IRETURN、ARETURN等;2、异常退出。无论哪种退出情况,都将返回至方法当前被调用的位置。方法的退出过程相当于弹出当前栈帧,退出可能有3种方式:①返回值亚茹上层调用栈帧;②异常信息抛给能够处理的栈帧。③PC计数器指向方法调用的小一条指令。
3. Native Method Stack(本地方法栈)
本地方法栈(Native Method Stack)在和JVM Stack(虚拟机栈)一样,也是线程私有的。所以它的作用也和虚拟机栈非常相似。其区别只是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的本地(Native)方法服务。
4. Java Heap(Java堆)
堆(Heap)存储着几乎所有的实例对象【在《Java虚拟机规范》中对Java堆的描述是:“所有的对象实例以及数组都应在堆上分配”(原文:The heap is runtime data area from which memory for all class instance and arrays is allocated.)】,堆是由垃圾收集器自动回收,堆区由各子线程共享使用。通常情况下,它占用的空间是所有内存区域中最大的,但如果无节制地创建大量对象,也容易消耗完所有的空间。堆的内存空间既可以固定大小,也可以在运行时动态地调整,通过如下参数设定初始值和最大值,比如-Xms256M -Xmx1024M
,其中-X表示它是JVM运行参数,ms
是memory start
的简称,mx
是memory max
的简称,分别代表最小堆容量和最大堆容量。但是在通常情况下,服务器在运行过程中,堆空间不断地扩容与回缩,势必形成不必要的系统压力,所以在线上生产环境中,JVM的Xms和Xmx设置成一样大小, 避免在GC后调整堆大小时带来的额外压力。
堆分成两大块:新生代和老年代。对象产生之初在新生代,步入老年时进入老年代,但是老年代也接纳在新生代无法容纳的超大对象。新生代= 1个Eden区+ 2个Survivor区。绝大部分对象在Eden区生成,当Eden区装填满的时候,会触发YoungGarbage Collection,即YGC。垃圾回收的时候,在Eden区实现清除策略,没有被引用的对象则直接回收。依然存活的对象会被移送到Survivor区,这个区真是名副其实的存在。Survivor 区分为S0和S1两块内存空间,送到哪块空间呢?每次YGC的时候,它们将存活的对象复制到未使用的那块空间,然后将当前正在使用的空间完全清除,交换两块空间的使用状态。如果YGC要移送的对象大于Survivor区容量的上限,则直接移交给老年代。假如一些没有进取心的对象以为可以一直在新生代的Survivor区交换来交换去,那就错了。每个对象都有一-个计数器,每次YGC都会加1。-XX:MaxTenuringThreshold 参数能配置计数器的值到达某个阈值的时候,对象从新生代晋升至老年代。如果该参数配置为1,那么从新生代的Eden区直接移至老年代。默认值是15,可以在Survivor区交换14次之后,晋升至老年代。与图4-8匹配的对
象晋升流程图如图4-9所示。
图4-9中,如果Survivor区无法放下,或者超大对象的阈值超过上限,则尝试在老年代中进行分配;如果老年代也无法放下,则会触发Full Garbage Collection,即Full GC。如果依然无法放下,则抛出OOM。堆内存出现OOM的概率是所有内存耗尽异常中最高的。
在不同的JVM实现及不同的回收机制中,堆内存的划分方式是不一样的。
5. Metaspace(元空间)
本文中的虚拟机指的是最为流行的Hotspot。早在JDK8版本中,元空间的前身永久代已经被淘汰。在JDK7及之前的版本中,只有Hotspot才有永久代,它在启动时大小固定,很难进行调优,并且Full GC时,会移动类元信息。在某些场景下,如果动态加载类过多,容易产生永久代的OOM。除此之外,永久代在垃圾回收过程中还存在着诸多问题,所以,JDK8开始使用元空间替换永久代。在JDK8及以上的版本中,设定MaxPermSize参数,JVM在启动时并不会报错,但是会提示:Java Hotspot 64Bit Server VM waring:ignoring option MaxPermSize=2560m;support was removed in 8.0
。
区别于永久代,元空间在本内存中分配,在JDK8里,字符串常量池移至堆内存,其他内容包括类元信息、字段、静态属性、方法、常量等都移至元空间里。
最后,从线程共享的角度来看,堆和元空间是所有线程共享的,而虚拟机栈、本地方法栈、程序计数器是线程内部私有的。
方法区(MethodArea)、永久代(PremGen)、元空间(MateSpace):
说到“方法区”,不得不提一下“永久代”这个概念,尤其是在JDK8以前,许多程序员都是在Hotspot虚拟机上开发、部署程序,很多人更愿意把“方法区”称为“永久代”(Permanent Generation),或者将两者混为一谈。本质上这两者并不是等价的,因为仅仅是当时的HotSpot虚拟机设计团队选择把收集器的分代设计扩展至“方法区”,或者说使用”永久代“来实现“方法区”而已,这样使得Hotspot虚拟机的垃圾收集器能够像管理Java堆一样管理这部分内存。但是对于其他虚拟机来说,比如JRocket、IBM J9等来说,是不存在”永久代“的概念的。原则上如何实现”方法区“属于虚拟机内部实现细节,不受《Java虚拟机规范》管束,并不要求统一。但是现在回过头来看,当年使用“永久代”来实现“方法区”并不是一个好主意,这种设计导致了Java应用更容易遇到内存溢出的问题(永久代有-XX:MaxPermSize的上限,即使不设置也有默认大小,而J9和JRockit只要没有触碰到进程可用内存的上限,例如32位系统中的4GB限制,就不会出现在这种问题),而且有极少数方法(例如
String::intern()
)会因永久代的原因而导致不同虚拟机下有不同的表现。当Oracle收购BEA获得了JRockit的所有权后,准备把JRockit的中的优秀功能,比如Java Mission Control管理工具,移植到HotSpot虚拟机时,但因为两者对于”方法区“实现的差异而面临重重困难。考虑到HotSpot虚拟机未来的发展,在JDK6的时候HotSpot虚拟机开发团队就有放弃永久代,逐步改为本地内存(Native Memory)来实现“方法区”的计划了,到了JDK7的HotSpot,已经把原本放在”方法区“的字符串常量、静态变量等移除,而到了JDK8中,终于废弃了“永久代”的概念,改用与JRockit、J9一样在本地内存中实现的“元空间”(Meta Space)来代替,把JDK7中”永久代“剩余的内容(主要是类型信息)全部移到元空间中。那么在JDK8中完取消了永久代,是不是也就没有了“方法区”了呢?当然不是,方法区是《Java虚拟机规范》中定义的规范,”永久代“和“元空间”都是方法区的实现。规范没变,一直在那里,只是虚拟机内部的实现变了。
垃圾回收(Garbage Collection,GC)的主要目的就是清除不再使用的对象,自动释放内存。
在Java运行时内存中的程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,因此这几个内存区域的内存分配与回收都是具备确定性的,在这几个区域内就不需要过多考虑如何回收的问题,当方法结束或者线程结束时,内存就自然跟着回收了。
而Java堆和方法区这两个区域则有着很显著的不确定性:一个接口多个实现类需要的内存可能会不一样,一个方法所执行的不用条件分支所需要的内存可能不一样,只有处于运行期间,才会知道需要创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。垃圾回收器所关注的正式这部分内存该如何管理。
GC是如何判断对象是否可以被回收的
当前主流的JVM都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。JVM通过判断一个对象与“GC Roots”之间有没有直接或者间接的引用关系,比如失去任何引用的对象,或者循环依赖引用的对象等,这些对象都是标记为可以回收的。
什么对象可以作为GC Roots
Java中异常对象都是Throwable类的子类,分为Error(错误)和Exception(异常)。Exception又分为checked异常(受检查异常)和unchecked异常(非受检查异常)。综上所述,异常分类结构如图所示:
Error类及其子类:是应用程序无法处理的严重错误。大多数错误是因为JVM虚拟机出现的问题。例如:VirtualMachineError、OutOfMemoryError,这些异常发生时虚拟机一般会终止线程。
是程序本身可以捕获并且可以处理的异常。Exception 又分为:运行时异常和编译时异常两类。
运行时异常
RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。比如NullPointerException空指针异常、ArrayIndexOutBoundException、ClassCastException、ArithmeticExecption。此类异常能正常编译通过,一般是由程序逻辑错误引起的,在程序中可以选择捕获处理,也可以不处理。虽然 Java 编译器不会检查运行时异常,但是我们也可以通过 throws 进行声明抛出,也可以通过 try-catch 对它进行捕获处理。如果产生运行时异常,则需要通过修改代码来进行避免。
编译时异常
Exception 中除 RuntimeException 及其子类之外的异常。 比如 ClassNotFoundException、IOException,要么通过throws进行声明抛出,要么通过try-catch进行捕获处理,否则不能通过编译。
Java 的所有Exception(异常)可以分为受检异常(checked exception)和非受检异常(unchecked exception)。
编译器要求必须处理的异常。要么使用try-catch捕获处理,要么使用throws抛出该异常,否则编译不通过。一旦发生此类异常,就必须采用某种方式进行处理。除 RuntimeException 及其子类外,其他的 Exception 异常都属于受检异常。
编译器不会进行检查并且不要求必须处理的异常,当程序中出现此类异常时,即使我们没有try-catch捕获它,也没有使用throws抛出该异常,编译也会正常通过。该类异常包括运行时异常(RuntimeException及其子类)、错误(Error)。
try-catch-finally代码块、throw、throws是处理异常的金钥匙。下面分别说一下各个部分的作用:
(1)try代码块:监视代码执行过程,一旦发现异常直接跳转至catch,如果没有catch,则直接跳转至finally。
(2)catch代码块:可选执行的代码块,如果try中没有任何异常发生则不会执行;如果发现异常则进行处理或向上抛出。
(3)finally代码块:必选执行的代码块,不管是否有异常发生,都会执行。通常用于关闭资源等善后工作。
(4)throw:用于抛出异常。
(3)throws:放在方法签名后,用于声明该方法可能会抛出异常。
数据结构分类
数据结构是算法实现的基石。数据的逻辑结构可分为两大类:一是线性结构;二是非线性结构。
(1)线性结构:有且仅有一个开始结点和终端结点,并且所有的结点最多只有一个直接前驱和一个直接后继。
(2)非线性结构:结点可以有多个前驱和后继。如果一个结点最多只有一个前驱,却有多个后继,这种结构就是树。如果对结点的前驱和后继的个数都不做限制,这种结构就是图。
上文参考文章:
- 漫画算法:什么是红黑树?
- 红黑树剖析及Java实现
- 红黑树详细分析(图文解析),看了都说好
- 寻找红黑树的操作手册
Java中的集合是用于存储对象的工具类容器,它实现了常用的数据结构,提供了一系列公开的方法用于增加、删除、修改、查询和遍历数据,降低了日常开发成本。集合的种类非常多,形成了比较经典的继承关系树,称为Java集合框架图,如图6-1所示。框架图中主要分为两类:①第一类是按照单个元素存储的Collection,在继承树中List和Set都实现了Collection接口;②第二类是按照Key-Value存储的Map。
在图6-1 Java集合框架图中,红色代表接口,蓝色代表抽象类,绿色代表并发包中的类,灰色代表早期线程安全的类(基本已经废弃[图中是Hashtable、Vector、Stack])。可以看到,与Collection相关的4条线分别是List、Queue、Set、Map,它们的子类是实现了数据结构中线性结构、树、图、哈希的具体实现类。下面一起学习List、Set、Queue、Map这4个常用集合类型。
ArrayList的类继承关系图:
ArrayList 是List 接口的一个重要实现类。底层是Object数组,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。
ArrayList是非线程安全的,Vector是线程安全的;但是Vector已经过时,不建议在新代码中使用,如果有多线程访问,建议使用CopyOnWriteArrayList。
//添加一个特定的元素到list的末尾
public boolean add(E e) {
//先确保elementData数组的长度足够,size是数组中数据的个数,因为要添加一个元素,所以size+1,先判断size+1的这个个数数组能否放得下,在这个方法中去判断数组长度是否够用
ensureCapacityInternal(size + 1); // Increments modCount!!
//在数据中正确的位置上放上元素e,并且size++
elementData[size++] = e;
return true;
}
//在指定位置添加一个元素
public void add(int index, E element) {
rangeCheckForAdd(index);
//先确保elementData数组的长度足够
ensureCapacityInternal(size + 1); // Increments modCount!!
//将数据整体向后移动一位,空出位置之后再插入,效率不太好
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
// 校验插入位置是否合理
private void rangeCheckForAdd(int index) {
//插入的位置肯定不能大于size 和小于0
if (index > size || index < 0)
//如果是,就报越界异常
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
//添加一个集合
public boolean addAll(Collection<? extends E> c) {
//把该集合转为对象数组
Object[] a = c.toArray();
int numNew = a.length;
//增加容量
ensureCapacityInternal(size + numNew); // Increments modCount
//挨个向后迁移
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
//新数组有元素,就返回 true
return numNew != 0;
}
//在指定位置,添加一个集合
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
//原来的数组挨个向后迁移
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
//把新的集合数组 添加到指定位置
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
上面两种add
方法里都调用到了ensureCapacityInternal
这个方法,源码如下:
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
ArrayList的扩容机制是首先创建一个空数组elementData,第一次插入数据时直接扩充至10,然后如果elementData的长度不足,就扩充至1.5倍,如果扩充完还不够,就使用需要的长度作为elementData的长度。
//根据索引删除指定位置的元素
public E remove(int index) {
//检查index的合理性
rangeCheck(index);
//这个作用很多,比如用来检测快速失败的一种标志。
modCount++;
//通过索引直接找到该元素
E oldValue = elementData(index);
//计算要移动的位数。
int numMoved = size - index - 1;
if (numMoved > 0)
//移动元素,挨个往前移一位。
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
//将--size上的位置赋值为null,让gc(垃圾回收机制)更快的回收它。
elementData[--size] = null; // clear to let GC do its work
//返回删除的元素。
return oldValue;
}
//从此列表中删除指定元素的第一个匹配项,如果存在,则删除。通过元素来删除该元素,就依次遍历,如果有这个元素,就将该元素的索引传给fastRemobe(index),使用这个方法来删除该元素,fastRemove(index)方法的内部跟remove(index)的实现几乎一样,这里最主要是知道arrayList可以存储null值
public boolean remove(Object o) {
if (o == null) {
//挨个遍历找到目标
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
//快速删除
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
//内部方法,“快速删除”,就是把重复的代码移到一个方法里
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
//删除或者保留指定集合中的元素
//用于两个方法,一个removeAll():它只清除指定集合中的元素,retainAll()用来测试两个集合是否有交集。
private boolean batchRemove(Collection<?> c, boolean complement) {
//将原集合,记名为A
final Object[] elementData = this.elementData;
//r用来控制循环,w是记录有多少个交集
int r = 0, w = 0;
boolean modified = false;
try {
//遍历 ArrayList 集合
for (; r < size; r++)
//参数中的集合c一次检测集合A中的元素是否有
if (c.contains(elementData[r]) == complement)
//有的话,就给集合A
elementData[w++] = elementData[r];
} finally {
//发生了异常,直接把 r 后面的复制到 w 后面
if (r != size) {
//将剩下的元素都赋值给集合A
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
//这里有两个用途,在removeAll()时,w一直为0,就直接跟clear一样,全是为null。
//retainAll():没有一个交集返回true,有交集但不全交也返回true,而两个集合相等的时候,返回false,所以不能根据返回值来确认两个集合是否有交集,而是通过原集合的大小是否发生改变来判断,如果原集合中还有元素,则代表有交集,而元集合没有元素了,说明两个集合没有交集。
// 清除多余的元素,clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
//保留公共的
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
//将elementData中每个元素都赋值为null,等待垃圾回收将这个给回收掉
public void clear() {
modCount++;
//并没有直接使数组指向 null,而是逐个把元素置为空,下次使用时就不用重新 new 了
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
public E set(int index, E element) {
// 检验索引是否合法
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
public E get(int index) {
// 检验索引是否合法
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
E elementData(int index) {
// 返回的值都经过了向下转型(Object -> E)
return (E) elementData[index];
}
LinkedList的类继承关系图:
LinkedList是List接口的另外一个重要实现类。底层使用的是双向链表数据结构(JDK1.6之前为循环双向链表,JDK7取消了循环。)
/** 在链表尾部插入元素 */
public boolean add(E e) {
linkLast(e);
return true;
}
/** 在链表指定位置插入元素 */
public void add(int index, E element) {
checkPositionIndex(index);
// 判断 index 是不是链表尾部位置,如果是,直接将元素节点插入链表尾部即可
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
/** 将元素节点插入到链表尾部 */
void linkLast(E e) {
final Node<E> l = last;
// 创建节点,并指定节点前驱为链表尾节点 last,后继引用为空
final Node<E> newNode = new Node<>(l, e, null);
// 将 last 引用指向新节点
last = newNode;
// 判断尾节点是否为空,为空表示当前链表还没有节点
if (l == null)
first = newNode;
else
l.next = newNode; // 让原尾节点后继引用 next 指向新的尾节点
size++;
modCount++;
}
/** 将元素节点插入到 succ 之前的位置 */
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
// 1. 初始化节点,并指明前驱和后继节点
final Node<E> newNode = new Node<>(pred, e, succ);
// 2. 将 succ 节点前驱引用 prev 指向新节点
succ.prev = newNode;
// 判断尾节点是否为空,为空表示当前链表还没有节点
if (pred == null)
first = newNode;
else
pred.next = newNode; // 3. succ 节点前驱的后继引用指向新节点
size++;
modCount++;
}
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
// 遍历链表,找到要删除的节点
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x); // 将节点从链表中移除
return true;
}
}
}
return false;
}
public E remove(int index) {
checkElementIndex(index);
// 通过 node 方法定位节点,并调用 unlink 将节点从链表中移除
return unlink(node(index));
}
/** 将某个节点从链表中移除 */
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
// prev 为空,表明删除的是头节点
if (prev == null) {
first = next;
} else {
// 将 x 的前驱的后继指向 x 的后继
prev.next = next;
// 将 x 的前驱引用置空,断开与前驱的链接
x.prev = null;
}
// next 为空,表明删除的是尾节点
if (next == null) {
last = prev;
} else {
// 将 x 的后继的前驱指向 x 的前驱
next.prev = prev;
// 将 x 的后继引用置空,断开与后继的链接
x.next = null;
}
// 将 item 置空,方便 GC 回收
x.item = null;
size--;
modCount++;
return element;
}
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
private void checkElementIndex(int index) {
if (!isElementIndex(index))
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
/* Tells if the argument is the index of an existing element.*/
private boolean isElementIndex(int index) {
return index >= 0 && index < size;
}
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
/*
* 则从头节点开始查找,否则从尾节点查找
* 查找位置 index 如果小于节点数量的一半,
*/
if (index < (size >> 1)) {
Node<E> x = first;
// 循环向后查找,直至 i == index
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
LinkedList 底层基于链表结构,无法像 ArrayList 那样随机访问指定位置的元素。LinkedList 查找过程要稍麻烦一些,需要从链表头结点(或尾节点)向后查找,时间复杂度为 O(N)
。
add(E e)
方法时,ArrayList会默认在数组尾部插入元素,这种情况的时间复杂度是O(1)。但是如果要在指定位置i插入和删除元素,需要移动数组中i后元素的位置,此时时间复杂度就是O(n-i)。②LinkedList底层采用链表。比如执行add(E e)
方法,会在链表尾部插入,时间复杂度为O(i),但是如果在指定位置i插入、删除操作,时间复杂度为O(n),因为需要先查到该位置。Set是不允许出现重复元素的集合类型。Set体系最常用的是HashSet、TreeSet、LinkedHashSet。
HashSet的类继承关系图:
HashSet从源码分析是使用HashMap来实现的,知识Value固定位一个静态对象,使用Key保证集合元素的唯一性,但它不会保证集合元素的顺序。
TreeSet的类继承关系图:
TreeSet从源码分析是使用TreeMap来实现的,底层为红黑树结构,在添加新元素到集合中时,按照某种比较规整插入其合适的位置,保证插入后的集合仍然是有序的。但不保证该顺序是插入时的顺序。
LinkedHashSet的类继承图:
LinkedHashSet继承自HashSet,具有HashSet的优点,内部使用链表维护了元素插入时的顺序。
Queue (队列)是一种先进先出的数据结构,队列是-种特殊的线性表,它只允许在表的一端进行获取操作,在表的另-端进行插入操作。当队列中没有元素时,称为空队列。自从BlockingQueue (阻塞队列)问世以来,队列的地位得到极大的提升,在各种高并发编程场景中,由于其本身FIFO的特性和阻塞操作的特点,经常被作为Buffer (数据缓冲区)使用。
Java中的Map类集合是与Collection类集合平级的一个接口,在集合框架图上,它有一条依赖线指向Collection类,那是因为Map类的部分方法返回Collection视图(View),比如keySet()
方法返回所有的Key,values()
方法返回所有的Value,entrySet()
会返回所有的K-V键值对。源码加注释如下:
//返回Map中对象的K使用的Set视图
Set<K> keySet();
//返回Map中对象的所有Value集合的Collection视图
Collection<V> values();
//返回Map类对象中的key-value键值对的Set视图
Set<Map.Entry<K,V>> entrySet();
通常上述方法返回的视图是支持清除操作的(实际上是从映射中删除相应的K-V键值对),但是增加元素会抛出异常(UnsupportedOperationException),因为AbstractCollection没有实现add操作,但是实现了remove、clear等相关操作。
Map接口常用的实现类是HashMap、ConcurrentHashMap、LinkedHashMap、TreeMap。(Hashtable已过时,不建议在新代码中使用)Map类的继承关系如下:
Map集合实现 | Key | Value | Super | JDK | 说明 |
---|---|---|---|---|---|
Hashtable | 不允许为null | 不允许为null | Dictionary | 1.0 | 线程安全(过时) |
ConcurrentHashMap | 不允许为null | 不允许为null | AbstractMap | 1.5 | 分段锁技术或CAS(JDK8及以上) |
TreeMap | 不允许为null | 允许为null | AbstractMap | 1.2 | 线程不安全(有序) |
HashMap | 允许为null | 允许为null | AbstractMap | 1.2 | 线程不安全(resize死锁问题) |
在大多数情况下,直接使用ConcurrentHashMap替代HashMap没有任何问题,在性能上区别不大,而且更加安全。与HashMap的Key、Value均可存放null不同,ConcurrentHashMap的Value可以存放null值,但是Key不允许为空,如果不对Key进行判空就放入ConcurrentHashMap,会导致NPE。
TreeMap的类继承关系图:
在TreeMap的接口继承树中,有两个与众不同的接口:SortedMap和NavigableMap。SortedMap接口表示它的Key是有序不可重复的,插入的Comparable或提供额外的比较器Comparator,所以Key不允许为null,但是Value可以;NavigableMap接口继承了SortedMap接口,根据指定的搜索条件返回最匹配的K-V。
不同于HashMap使用hashCode和equals实现去重的,TreeMap是依靠Comparable或Comparator来实现Key去重,所以TreeMap并不一定要复写hashCode和equals方法来达到Key去重的目的。
如果Comparator不为null,优先使用比较器Comparator的compare方法;如果为null,则使用Key实现的Comparable接口的compareTo方法。如果两者都无法满足,则抛出异常。
如无特别说明,本节分析的HashMap基于JDK8。
HashMap的类继承关系图:
JDK8中HashMap的有些地方做了优化,数据的存储结构由由JDK7中数组+链表的方式,变为JDK8中数组+链表+红黑树的方式。当链表长度大于阈值(默认为8)时,将链表进化为红黑树,以减少搜索时间,在性能上进一步得到提升。
HashMap类中有一个非常重要的字段,就是Node
,即桶数组,明显它是一个Node的数组。 Node的源码如下:
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() {
return key; }
public final V getValue() {
return value; }
public final String toString() {
return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。上图中的每个黑色圆点就是一个Node对象。
当HashMap中的单链表长度大于8时,会进化为红黑树。TreeNode就是红黑树的节点。
/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/**
* Returns root of tree containing this node.
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
// 类中其他方法...
}
HashMap通过Key的Hash算法得到hash值,然后再通过Hash算法的后两步运算(高位运算和取模运算,下文有介绍)来定位该键值对在数组中的存储位置,如果当前位置不存在键值对,就直接把要存的键值对放入该位置;否则,就判断当前位置上的键值对与要存入键值对的Key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决哈希冲突。
如果数组很大,即使较差的Hash算法也会比较分散,如果数组数组很小,即使好的Hash算法也会出现较多碰撞,所以就需要在空间成本和时间成本之间权衡,其实就是在根据实际情况确定数组的大小,并在此基础上设计好的hash算法减少Hash碰撞。那么通过什么方式来控制map使得Hash碰撞的概率又小,桶数组(Node
在理解Hash和扩容流程之前,我们得先了解下HashMap的几个字段。从HashMap的默认构造函数源码可知,构造函数就是对下面几个字段进行初始化,源码如下:
transient int size; //HashMap中实际存储的K-V键值对个数
transient int modCount; //记录HashMap修改的次数
int threshold; //阈值,判断HashMap是否需要扩容
final float loadFactor; //加载因子,threshod = LoadFactor * length
size就是HashMap中实际存在的键值对数量。注意和table的长度length、容纳最大键值对数量threshold的区别。
modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。强调一点,内部结构发生变化指的是结构发生变化,例如put新键值对,但是某个key对应的value值被覆盖不属于结构变化。
threshold就是在此loadFactor和length对应下允许的最大元素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。默认的负载因子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改。
threshold是HashMap所能容纳Node(键值对)个数的极限。threshold = length * loadFactor。也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。Node[] table的初始化长度length,默认值是16;loadFactor为负载因子,默认值是0.75。
这里存在一个问题,即使加载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况,一旦出现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树;当红黑树节点个数太少(默认少于6)时,红黑树就退化为链表。
为了存取高效和减少碰撞,也就是尽量要把数据分配均匀。Hash值的范围是 -231~231-1,这么大范围的数组一般是不会出现碰撞的,但问题是这么长的数组,没有内存可以放下,所以这个范围不能直接拿来用。我们首先想到对数组的长度做取模运算,得到的余数就是是对应数组的下标。
但如果取余(%)操作中除数是2的次幂,则等价于其除数减一的与(&)操作,也就是说“hash % length
”等价于“hash & (length - 1)
”。这个方法非常巧妙,它通过“hash & (length - 1)
”来得到数组下标,而HashMap底层数组的长度总是2的次幂,这是HashMap在速度上的优化。 当length总是2的次幂时,&操作比%操作有更高的效率。
HashMap的内部功能实现很多,本文主要从根据key获取桶数组索引位置、put方法的详细执行、扩容过程三个具有代表性的点深入展开讲解。
不管增加、删除、查找键值对,定位到桶数组的位置都是很关键的第一步。前面说过JDK8中HashMap是数组+链表+红黑树的结构,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。 实际上,JDK8中是使用index = (n - 1) & hash
来计算数组下标的。hash()
方法源码如下:
static final int hash(Object key) {
int h;
// h = key.hashCode() 为第一步 去hashCode值
// h ^ (h >>> 16) 为第二步 高位参与运算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
在JDK8的实现中,优化了高位运算的算法,通过hashCode的高16位异或低16位(h = k.hashCode()) ^ (h >>> 16)
实现的,主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低位都参与到Hash的计算中,同时不会有太大的开销。
下面举例说明,n为table的长度。
HashMap的put方法执行过程可以通过下图来理解,自己有兴趣可以去对比源码更清楚地研究学习。
①.判断键值对数组table[i]``resize()
②.根据键值key
计算hash
值得到插入的数组索引i,如果table[i]==null
,直接新建节点添加,转向⑥,如果table[i]
不为空,转向③;
③.判断table[i]
的首个元素是否和key
一样,如果相同直接覆盖value
,否则转向④,这里的相同指的是hashCode
以及equals
;
④.判断table[i]
是否为treeNode
,即table[i]
是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;
⑤.遍历table[i]
,判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key
已经存在直接覆盖value
即可;
⑥.插入成功后,判断实际存在的键值对数量size
是否超多了最大容量threshold
,如果超过,进行扩容。
JDK1.8HashMap的put方法源码如下:
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 步骤1:table为空则创建
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 步骤2:计算index,并对null做判断处理
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 步骤3:节点Key存在,直接覆盖value
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 步骤4:判断是否为红黑树
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// 步骤5:否则为链表
else {
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 链表长度大于8,进化为红黑树进行处理
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
// Key已经存在,直接覆盖value
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
// existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 步骤6:超过阈值,扩容
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
**扩容(resize)**就是重新计算容量,向HashMap对象里不停的添加元素,而HashMap对象内部的数组无法装载更多的元素时,对象就需要扩大数组的长度,以便能装入更多的元素。当然Java里的数组是无法自动扩容的,方法是使用一个新的数组代替已有的容量小的数组,就像我们用一个小桶装水,如果想装更多的水,就得换大水桶。
我们分析下resize的源码,鉴于JDK1.8融入了红黑树,较复杂,为了便于理解我们仍然使用JDK1.7的代码,本质上两个版本区别不大,具体区别后文再说。
void resize(int newCapacity) {
// 传入新的容量
// 引用扩容前的Entry数组
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
// 扩容前数组大小如果已经达到最大(2^30)了
if (oldCapacity == MAXIMUN_CAPACITY) {
// 修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
threshold = Integer.MAX_VALUE;
return;
}
// 创建一个新的Entry数组
Entry[] newTable = new Entry[newCapacity];
// 将数据转移到新的Entry数组中
transfer(newTable);
// HashMap的table属性引用新的Entry数组
table = newTable;
// 修改阈值
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
// src引用旧的Entry数组
Entry[] src = table;
int newCapacity = newTable.length;
// 遍历旧Entry数组
for (int j = 0; j < src.length; j++) {
// 取得旧Entry数组的每一个元素
Entry<K,V> e = src[j];
if (e != null) {
// 释放旧Entry数组的对象引用(for循环后,旧Entry数组不在引用任何对象)
src[j] = null;
do {
Entry<K,V> next = e.next;
// **重新计算每个元素在数组中的位置
int i = indexFor(e.hash, newCapacity);
// 标记[i]
e.next = newTable[i];
// 将元素放在数组上
newTable[i] = e;
// 访问下一个Entry链上的元素
e = next;
} while (e != null);
}
}
}
//根据hashcode,和表的长度,返回存放的索引
static int indexFor(int h, int length) {
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
return h & (length-1);
}
e.next = newTable[i],也就是使用了单链表的头插入方式,同一位置上新元素总会被放在链表的头部位置;这样先放在一个索引上的元素终会被放到Entry链的尾((如果发生了hash冲突的话),这一点和Jdk1.8有区别,下文详解。在旧数组中同一条Entry链上的元素,通过重新计算索引位置后,有可能被放到了新数组的不同位置上。
下面举个例子说明下扩容过程。假设了我们的hash算法就是简单的用key % length(数组的长度)。其中的桶数组table的size=2, 所以key = 3、7、5,put顺序依次为 5、7、3。在mod 2以后都冲突在table[1]这里了。这里假设加载因子 loadFactor=1,即当键值对的实际大小size 大于 table的threshold时进行扩容。接下来的三个步骤是桶数组 resize成4,然后所有的Node重新rehash的过程。
下面我们讲解下JDK8做了哪些优化。**经过观测可以发现,我们使用的是2次幂的扩展(即table的长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。**看下图可以明白这句话的意思,n为table的长度,图(a)表示扩容前的key1和key2两种key确定索引位置的示例,图(b)表示扩容后key1和key2两种key确定索引位置的示例,其中hash1是key1对应的哈希与高位运算结果。
元素在重新计算hash之后,因为n变为2倍,那么n-1的mask范围在高位多1bit(红色),因此新的index就会发生这样的变化:
因此,我们在扩充HashMap的时候,不需要像JDK7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”,可以看看下图为16扩充为32的resize示意图:
这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,由于新增的1bit是0还是1可以认为是随机的,因此扩容的过程,均匀的把之前的冲突的节点分散到新的数组了。这一块就是JDK8的优化点。有一点注意区别,JDK7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,但是从上图可以看出,JDK1.8不会倒置。有兴趣的同学可以研究下JDK1.8的resize源码,写的很赞,如下:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认初始化容量,大小为16
static final int MAXIMUM_CAPACITY = 1 << 30; //table数组的最大长度,为2^30
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 如果原table数组的长度>=(2^30)
if (oldCap >= MAXIMUM_CAPACITY) {
// 阈值设置为(2^31-1),table数组无法扩容
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果原table数组的长度扩大2倍后<(2^30) 且 原table数组的长度>=16
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 阈值扩大2倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else {
// zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({
"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
// preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
不同点 | JDK7 | JDK8 |
---|---|---|
存储结构 | 数组+链表 | 数组+链表+红黑树 |
初始化方式 | 单独函数:inflateTable() |
直接集成到扩容函数resize() |
hash值计算方式 | 9次扰动=4次位运算+5次异或运算 | 2次扰动=1次位运算+1次异或运算 |
存放元素规则 | 无冲突时,存入桶数组;冲突是,存入链表 | 无冲突时,存放数组;冲突 , 链表长度 < 8:存放单链表;冲突 &,链表长度 > 8:树化并存放红黑树 |
插入元素方式 | 头插法(先将原位置的元素后移一位,在插入元素到该位置) | 尾插法(直接插入到链表尾部/红黑树) |
扩容后存储位置的计算方式 | 部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) |
ConcurrentHashMap的类继承关系图:
Hashtable是在JDK1.0中引入,以全互斥方式处理并发情况,性能极差,已淘汰,不建议在新的代码中使用。HashMap是在JDK1.2中引入的,是非线程安全的,它的最大问题是在并发写的情况下,会发生死锁、数据丢失的问题。虽然可以通过Collections的synchronizedMap(Map
将HashMap包装成一个线程安全的map。考虑到线程并发安全性,以及访问效率,应优先使用ConcurrentHashMap 。
实际上synchronizedMap实现依然是采用synchronized独占式锁进行安全的并发控制。
在JDK8之前,ConcurrentHashMap采用分段锁技术对整个桶数组进行了分割分段,每把锁只锁住桶数组的一部分,多线程访问桶数组不同片段的数据,就不会存在存在锁竞争,提高了并发访问率。分段锁是由内部类Segment 实现的,它继承于ReentrantLock,用来管理它负责片段里的各个HashEntry。到了JDK8的时候,已经抛弃了分段锁的概念,而是使用Synchrinized(JDK1.6以后,对synchronized锁做了很多的优化)和CAS来提供并发访问。
到了JDK8的时候,ConcurrentHashMap进行了脱胎换骨式的改造,使用了大量的lock-free技术来减轻因锁的竞争而对性能造成的影响。涉及volatile、CAS、锁、链表、红黑树等众多知识点。
size()
方法最大只能表示到231 - 1, ConcurrentHashMap 额外提供了mappingCount()
方法,用来返回集合内元素的数量,最大可以表示到263-1。ConcurrentHashMap内部维护了一个Node类型的数组,也就是table,中的加点transient volatile Node[] table;
数组的每一个位置table[i]
代表了一个桶,当插入键值对时,会根据Key的hash值映射到不同的桶位置,table一共可以包含4种不同类型的桶:Node、TreeBin、ForwardingNode、ReservationNode。上图中,不同的桶用不同颜色表示。可以看到,有的桶链接着链表,有的桶链接着红黑树,这也是JDK1.8中ConcurrentHashMap的特殊之处,后面会详细讲到。
需要注意的是:TreeBin所链接的是一颗红黑树,红黑树的结点用TreeNode表示,所以ConcurrentHashMap中实际上一共有五种不同类型的Node结点。
之所以用TreeBin而不是直接用TreeNode,是因为红黑树的操作比较复杂,包括构建、左旋、右旋、删除,平衡等操作,用一个代理结点TreeBin来包含这些复杂操作,其实是一种“职责分离”的思想。另外TreeBin中也包含了一些加/解锁的操作。
Node结点的定义非常简单,也是其它四种类型结点的父类。默认链接到table[i]
——桶上的结点就是Node结点。当出现hash冲突时,Node结点会首先以链表的形式链接到table上,当结点数量超过一定数目时,链表会转化为红黑树。因为链表查找的平均时间复杂度为O(n)
,而红黑树是一种平衡二叉树,其平均时间复杂度为O(logn)
。
/**
* 普通的Entry结点, 以链表形式保存时才会使用, 存储实际的数据.
*/
static class Node<K, V> implements Map.Entry<K, V> {
final int hash;
final K key;
volatile V val;
volatile Node<K, V> next; // 链表指针
Node(int hash, K key, V val, Node<K, V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
public final K getKey() {
return key;
}
public final V getValue() {
return val;
}
public final int hashCode() {
return key.hashCode() ^ val.hashCode();
}
public final String toString() {
return key + "=" + val;
}
public final V setValue(V value) {
throw new UnsupportedOperationException();
}
public final boolean equals(Object o) {
Object k, v, u;
Map.Entry<?, ?> e;
return ((o instanceof Map.Entry) &&
(k = (e = (Map.Entry<?, ?>) o).getKey()) != null &&
(v = e.getValue()) != null &&
(k == key || k.equals(key)) &&
(v == (u = val) || v.equals(u)));
}
/**
* 链表查找.
*/
Node<K, V> find(int h, Object k) {
Node<K, V> e = this;
if (k != null) {
do {
K ek;
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
} while ((e = e.next) != null);
}
return null;
}
}
TreeNode就是红黑树的结点,TreeNode不会直接链接到table[i]
上面,而是由TreeBin链接,TreeBin会指向红黑树的根结点。
/**
* 红黑树结点, 存储实际的数据.
*/
static final class TreeNode<K, V> extends Node<K, V> {
boolean red;
TreeNode<K, V> parent;
TreeNode<K, V> left;
TreeNode<K, V> right;
/**
* prev指针是为了方便删除.
* 删除链表的非头结点时,需要知道它的前驱结点才能删除,所以直接提供一个prev指针
*/
TreeNode<K, V> prev;
TreeNode(int hash, K key, V val, Node<K, V> next,
TreeNode<K, V> parent) {
super(hash, key, val, next);
this.parent = parent;
}
Node<K, V> find(int h, Object k) {
return findTreeNode(h, k, null);
}
/**
* 以当前结点(this)为根结点,开始遍历查找指定key.
*/
final TreeNode<K, V> findTreeNode(int h, Object k, Class<?> kc) {
if (k != null) {
TreeNode<K, V> p = this;
do {
int ph, dir;
K pk;
TreeNode<K, V> q;
TreeNode<K, V> pl = p.left, pr = p.right;
if ((ph = p.hash) > h)
p = pl;
else if (ph < h)
p = pr;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if (pl == null)
p = pr;
else if (pr == null)
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
else if ((q = pr.findTreeNode(h, k, kc)) != null)
return q;
else
p = pl;
} while (p != null);
}
return null;
}
}
TreeBin相当于TreeNode的代理结点。TreeBin会直接链接到table[i]
上面,该结点提供了一系列红黑树相关的操作,以及加锁、解锁操作。
/**
* TreeNode的代理结点(相当于封装了TreeNode的容器,提供针对红黑树的转换操作和锁控制)
* hash值固定为-3
*/
static final class TreeBin<K, V> extends Node<K, V> {
TreeNode<K, V> root; // 红黑树结构的根结点
volatile TreeNode<K, V> first; // 链表结构的头结点
volatile Thread waiter; // 最近的一个设置WAITER标识位的线程
volatile int lockState; // 整体的锁状态标识位
static final int WRITER = 1; // 二进制001,红黑树的写锁状态
static final int WAITER = 2; // 二进制010,红黑树的等待获取写锁状态
static final int READER = 4; // 二进制100,红黑树的读锁状态,读可以并发,每多一个读线程,lockState都加上一个READER值
/**
* 在hashCode相等并且不是Comparable类型时,用此方法判断大小.
*/
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
/**
* 将以b为头结点的链表转换为红黑树.
*/
TreeBin(TreeNode<K, V> b) {
super(TREEBIN, null, null, null);
this.first = b;
TreeNode<K, V> r = null;
for (TreeNode<K, V> x = b, next; x != null; x = next) {
next = (TreeNode<K, V>) x.next;
x.left = x.right = null;
if (r == null) {
x.parent = null;
x.red = false;
r = x;
} else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
for (TreeNode<K, V> p = r; ; ) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K, V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
r = balanceInsertion(r, x);
break;
}
}
}
}
this.root = r;
assert checkInvariants(root);
}
/**
* 对红黑树的根结点加写锁.
*/
private final void lockRoot() {
if (!U.compareAndSwapInt(this, LOCKSTATE, 0, WRITER))
contendedLock();
}
/**
* 释放写锁.
*/
private final void unlockRoot() {
lockState = 0;
}
/**
* Possibly blocks awaiting root lock.
*/
private final void contendedLock() {
boolean waiting = false;
for (int s; ; ) {
if (((s = lockState) & ~WAITER) == 0) {
if (U.compareAndSwapInt(this, LOCKSTATE, s, WRITER)) {
if (waiting)
waiter = null;
return;
}
} else if ((s & WAITER) == 0) {
if (U.compareAndSwapInt(this, LOCKSTATE, s, s | WAITER)) {
waiting = true;
waiter = Thread.currentThread();
}
} else if (waiting)
LockSupport.park(this);
}
}
/**
* 从根结点开始遍历查找,找到“相等”的结点就返回它,没找到就返回null
* 当存在写锁时,以链表方式进行查找
*/
final Node<K, V> find(int h, Object k) {
if (k != null) {
for (Node<K, V> e = first; e != null; ) {
int s;
K ek;
/**
* 两种特殊情况下以链表的方式进行查找:
* 1. 有线程正持有写锁,这样做能够不阻塞读线程
* 2. 有线程等待获取写锁,不再继续加读锁,相当于“写优先”模式
*/
if (((s = lockState) & (WAITER | WRITER)) != 0) {
if (e.hash == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
e = e.next;
} else if (U.compareAndSwapInt(this, LOCKSTATE, s,
s + READER)) {
TreeNode<K, V> r, p;
try {
p = ((r = root) == null ? null :
r.findTreeNode(h, k, null));
} finally {
Thread w;
if (U.getAndAddInt(this, LOCKSTATE, -READER) ==
(READER | WAITER) && (w = waiter) != null)
LockSupport.unpark(w);
}
return p;
}
}
}
return null;
}
/**
* 查找指定key对应的结点,如果未找到,则插入.
*
* @return 插入成功返回null, 否则返回找到的结点
*/
final TreeNode<K, V> putTreeVal(int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
for (TreeNode<K, V> p = root; ; ) {
int dir, ph;
K pk;
if (p == null) {
first = root = new TreeNode<K, V>(h, k, v, null, null);
break;
} else if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) {
TreeNode<K, V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.findTreeNode(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.findTreeNode(h, k, kc)) != null))
return q;
}
dir = tieBreakOrder(k, pk);
}
TreeNode<K, V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
TreeNode<K, V> x, f = first;
first = x = new TreeNode<K, V>(h, k, v, f, xp);
if (f != null)
f.prev = x;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
if (!xp.red)
x.red = true;
else {
lockRoot();
try {
root = balanceInsertion(root, x);
} finally {
unlockRoot();
}
}
break;
}
}
assert checkInvariants(root);
return null;
}
/**
* 删除红黑树的结点:
* 1. 红黑树规模太小时,返回true,然后进行 树 -> 链表 的转化;
* 2. 红黑树规模足够时,不用变换成链表,但删除结点时需要加写锁.
*/
final boolean removeTreeNode(TreeNode<K, V> p) {
TreeNode<K, V> next = (TreeNode<K, V>) p.next;
TreeNode<K, V> pred = p.prev; // unlink traversal pointers
TreeNode<K, V> r, rl;
if (pred == null)
first = next;
else
pred.next = next;
if (next != null)
next.prev = pred;
if (first == null) {
root = null;
return true;
}
if ((r = root) == null || r.right == null || // too small
(rl = r.left) == null || rl.left == null)
return true;
lockRoot();
try {
TreeNode<K, V> replacement;
TreeNode<K, V> pl = p.left;
TreeNode<K, V> pr = p.right;
if (pl != null && pr != null) {
TreeNode<K, V> s = pr, sl;
while ((sl = s.left) != null) // find successor
s = sl;
boolean c = s.red;
s.red = p.red;
p.red = c; // swap colors
TreeNode<K, V> sr = s.right;
TreeNode<K, V> pp = p.parent;
if (s == pr) {
// p was s's direct parent
p.parent = s;
s.right = p;
} else {
TreeNode<K, V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
p.left = null;
if ((p.right = sr) != null)
sr.parent = p;
if ((s.left = pl) != null)
pl.parent = s;
if ((s.parent = pp) == null)
r = s;
else if (p == pp.left)
pp.left = s;
else
pp.right = s;
if (sr != null)
replacement = sr;
else
replacement = p;
} else if (pl != null)
replacement = pl;
else if (pr != null)
replacement = pr;
else
replacement = p;
if (replacement != p) {
TreeNode<K, V> pp = replacement.parent = p.parent;
if (pp == null)
r = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
root = (p.red) ? r : balanceDeletion(r, replacement);
if (p == replacement) {
// detach pointers
TreeNode<K, V> pp;
if ((pp = p.parent) != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
p.parent = null;
}
}
} finally {
unlockRoot();
}
assert checkInvariants(root);
return false;
}
// 以下是红黑树的经典操作方法,改编自《算法导论》
static <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root,
TreeNode<K, V> p) {
TreeNode<K, V> r, pp, rl;
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root,
TreeNode<K, V> p) {
TreeNode<K, V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root,
TreeNode<K, V> x) {
x.red = true;
for (TreeNode<K, V> xp, xpp, xppl, xppr; ; ) {
if ((xp = x.parent) == null) {
x.red = false;
return x;
} else if (!xp.red || (xpp = xp.parent) == null)
return root;
if (xp == (xppl = xpp.left)) {
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
} else {
if (x == xp.right) {
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
} else {
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
} else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
static <K, V> TreeNode<K, V> balanceDeletion(TreeNode<K, V> root,
TreeNode<K, V> x) {
for (TreeNode<K, V> xp, xpl, xpr; ; ) {
if (x == null || x == root)
return root;
else if ((xp = x.parent) == null) {
x.red = false;
return x;
} else if (x.red) {
x.red = false;
return root;
} else if ((xpl = xp.left) == x) {
if ((xpr = xp.right) != null && xpr.red) {
xpr.red = false;
xp.red = true;
root = rotateLeft(root, xp);
xpr = (xp = x.parent) == null ? null : xp.right;
}
if (xpr == null)
x = xp;
else {
TreeNode<K, V> sl = xpr.left, sr = xpr.right;
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) {
xpr.red = true;
x = xp;
} else {
if (sr == null || !sr.red) {
if (sl != null)
sl.red = false;
xpr.red = true;
root = rotateRight(root, xpr);
xpr = (xp = x.parent) == null ?
null : xp.right;
}
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red;
if ((sr = xpr.right) != null)
sr.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateLeft(root, xp);
}
x = root;
}
}
} else {
// symmetric
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if (xpl == null)
x = xp;
else {
TreeNode<K, V> sl = xpl.left, sr = xpl.right;
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
} else {
if (sl == null || !sl.red) {
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl);
xpl = (xp = x.parent) == null ?
null : xp.left;
}
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateRight(root, xp);
}
x = root;
}
}
}
}
}
/**
* 递归检查红黑树的正确性
*/
static <K, V> boolean checkInvariants(TreeNode<K, V> t) {
TreeNode<K, V> tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (TreeNode<K, V>) t.next;
if (tb != null && tb.next != t)
return false;
if (tn != null && tn.prev != t)
return false;
if (tp != null && t != tp.left && t != tp.right)
return false;
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
if (t.red && tl != null && tl.red && tr != null && tr.red)
return false;
if (tl != null && !checkInvariants(tl))
return false;
if (tr != null && !checkInvariants(tr))
return false;
return true;
}
private static final sun.misc.Unsafe U;
private static final long LOCKSTATE;
static {
try {
U = sun.misc.Unsafe.getUnsafe();
Class<?> k = TreeBin.class;
LOCKSTATE = U.objectFieldOffset
(k.getDeclaredField("lockState"));
} catch (Exception e) {
throw new Error(e);
}
}
}
ForwardingNode结点仅仅在扩容时才会使用 。
/**
* ForwardingNode是一种临时结点,在扩容进行中才会出现,hash值固定为-1,且不存储实际数据。
* 如果旧table数组的一个hash桶中全部的结点都迁移到了新table中,则在这个桶中放置一个ForwardingNode。
* 读操作碰到ForwardingNode时,将操作转发到扩容后的新table数组上去执行;写操作碰见它时,则尝试帮助扩容。
*/
static final class ForwardingNode<K, V> extends Node<K, V> {
final Node<K, V>[] nextTable;
ForwardingNode(Node<K, V>[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
// 在新的数组nextTable上进行查找
Node<K, V> find(int h, Object k) {
// loop to avoid arbitrarily deep recursion on forwarding nodes
outer:
for (Node<K, V>[] tab = nextTable; ; ) {
Node<K, V> e;
int n;
if (k == null || tab == null || (n = tab.length) == 0 ||
(e = tabAt(tab, (n - 1) & h)) == null)
return null;
for (; ; ) {
int eh;
K ek;
if ((eh = e.hash) == h &&
((ek = e.key) == k || (ek != null && k.equals(ek))))
return e;
if (eh < 0) {
if (e instanceof ForwardingNode) {
tab = ((ForwardingNode<K, V>) e).nextTable;
continue outer;
} else
return e.find(h, k);
}
if ((e = e.next) == null)
return null;
}
}
}
}
保留结点,ConcurrentHashMap中的一些特殊方法会专门用到该类结点。
/**
* 保留结点.
* hash值固定为-3, 不保存实际数据
* 只在computeIfAbsent和compute这两个函数式API中充当占位符加锁使用
*/
static final class ReservationNode<K, V> extends Node<K, V> {
ReservationNode() {
super(RESERVED, null, null, null);
}
Node<K, V> find(int h, Object k) {
return null;
}
}
/**
* 最大容量.
*/
private static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认初始容量
*/
private static final int DEFAULT_CAPACITY = 16;
/**
* The largest possible (non-power of two) array size.
* Needed by toArray and related methods.
*/
static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 负载因子,为了兼容JDK1.8以前的版本而保留。
* JDK1.8中的ConcurrentHashMap的负载因子恒定为0.75
*/
private static final float LOAD_FACTOR = 0.75f;
/**
* 链表转树的阈值,即链接结点数大于8时, 链表转换为树.
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 树转链表的阈值,即树结点树小于6时,树转换为链表.
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 在链表转变成树之前,还会有一次判断:
* 即只有键值对数量大于MIN_TREEIFY_CAPACITY,才会发生转换。
* 这是为了避免在Table建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
/**
* 在树转变成链表之前,还会有一次判断:
* 即只有键值对数量小于MIN_TRANSFER_STRIDE,才会发生转换.
*/
private static final int MIN_TRANSFER_STRIDE = 16;
/**
* 用于在扩容时生成唯一的随机数.
*/
private static int RESIZE_STAMP_BITS = 16;
/**
* 可同时进行扩容操作的最大线程数.
*/
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
/**
* The bit shift for recording size stamp in sizeCtl.
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
static final int MOVED = -1; // 标识ForwardingNode结点(在扩容时才会出现,不存储实际数据)
static final int TREEBIN = -2; // 标识红黑树的根结点
static final int RESERVED = -3; // 标识ReservationNode结点()
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
/**
* CPU核心数,扩容时使用
*/
static final int NCPU = Runtime.getRuntime().availableProcessors();
/**
* Node数组,标识整个Map,首次插入元素时创建,大小总是2的幂次.
*/
transient volatile Node<K, V>[] table;
/**
* 扩容后的新Node数组,只有在扩容时才非空.
*/
private transient volatile Node<K, V>[] nextTable;
/**
* 控制table的初始化和扩容.
* 0 : 初始默认值
* -1 : 有线程正在进行table的初始化
* >0 : table初始化时使用的容量,或初始化/扩容完成后的threshold
* -(1 + nThreads) : 记录正在执行扩容任务的线程数
*/
private transient volatile int sizeCtl;
/**
* 扩容时需要用到的一个下标变量.
*/
private transient volatile int transferIndex;
/**
* 计数基值,当没有线程竞争时,计数将加到该变量上。类似于LongAdder的base变量
*/
private transient volatile long baseCount;
/**
* 计数数组,出现并发冲突时使用。类似于LongAdder的cells数组
*/
private transient volatile CounterCell[] counterCells;
/**
* 自旋标识位,用于CounterCell[]扩容时使用。类似于LongAdder的cellsBusy变量
*/
private transient volatile int cellsBusy;
// 视图相关字段
private transient KeySetView<K, V> keySet;
private transient ValuesView<K, V> values;
private transient EntrySetView<K, V> entrySet;
使用ConcurrentHashMap最长用的也应该是put和get方法了吧
无论是调用get方法、还是put方法,都需要先计算出Key的hash值。 而在JDK1.8的ConcurrentHashMap中通过spread()方法获取。
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
spread()方法将key的hash值进行再hash,让hash值的高位也参与hash运算,从而减少哈希冲突。
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算Key的hash
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// table[i]桶节点的key与查找的key相同,则直接返回
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 当前节点hash小于0说明为树节点,在红黑树中查找即可
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
// 从链表中查找,查找到则返回该节点的value,否则就返回null即可
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
当调用get方法时,先看当前的hash桶数组节点即table[i]是否为查找的节点,若是则直接返回;若不是,再看当前是不是树节点?通过看节点的hash值是否为小于0,如果小于0则为树节点。如果是树节点在红黑树中查找节点;如果不是树节点,那就只剩下为链表的形式的一种可能性了,就向后遍历查找节点,若查找到则返回节点的value即可,若没有找到就返回null。
调用put方法时实际具体实现是putVal方法,源码如下:
/** 插入键值对,均不能为null */
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 计算key的hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 如果当前table还没有初始化先调用initTable方法将table进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// tab中索引为i的位置的元素为null,则直接使用CAS将值插入即可
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 当前正在扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
// 当前为链表,在链表中插入新的键值对
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 当前为红黑树,将新的键值对插入到红黑树中
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 插入完键值对后再根据实际大小看是否需要转换成红黑树
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 对当前容量大小进行检查,如果超过了阈值(length*loadFactor)就需要扩容
addCount(1L, binCount);
return null;
}
当ConcurrentHashMap容量不足的时候,需要对table进行扩容。这个方法的基本思想跟HashMap是很像的,但是由于它是支持并发扩容的,所以要复杂的多。原因是它支持多线程进行扩容操作,而并没有加锁。我想这样做的目的不仅仅是为了满足concurrent的要求,而是希望利用并发处理去减少扩容带来的时间影响。transfer方法源码为:
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//1. 新建Node数组,容量为之前的两倍
if (nextTab == null) {
// initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) {
// try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
//2. 新建forwardingNode引用,在之后会用到
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
// 3. 确定遍历中的索引i
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//4.将原数组中的元素复制到新数组中去
//4.5 for循环退出,扩容结束修改sizeCtl属性
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
//4.1 当前数组中第i个元素为null,用CAS设置成特殊节点forwardingNode(可以理解成占位符)
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//4.2 如果遍历到ForwardingNode节点 说明这个点已经被处理过了 直接跳过 这里是控制并发扩容的核心
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) {
//4.3 处理当前节点为链表的头结点的情况,构造两个链表,一个是原链表 另一个是原链表的反序排列
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
//在nextTable的i位置上插入一个链表
setTabAt(nextTab, i, ln);
//在nextTable的i+n的位置上插入另一个链表
setTabAt(nextTab, i + n, hn);
//在table的i位置上插入forwardNode节点 表示已经处理过该节点
setTabAt(tab, i, fwd);
//设置advance为true 返回到上面的while循环中 就可以执行i--操作
advance = true;
}
//4.4 处理当前节点是TreeBin时的情况,操作和上面的类似
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
整个扩容操作分为两个部分:
第一部分是构建一个nextTable,它的容量是原来的两倍,这个操作是单线程完成的。新建table数组的代码为Node
,在原容量大小的基础上右移一位。
第二个部分就是将原来table中的元素复制到nextTable中,主要是遍历复制的过程。根据运算得到当前遍历的数组的位置i,然后利用tabAt方法获得i位置的元素再进行判断:
最后用一个流程图来进行总结:
本节参考:
J.U.C之collections框架:ConcurrentHashMap(1) 原理
J.U.C之collections框架:ConcurrentHashMap(2) 扩容
Java中两个对象相比较的方法常用在元素排序中,常用的两个接口是Comparable和Comparator,前者是自己和自己比,它的比较方法是compareTo()
,可以看作是自营性质的比较器;后者是第三方比较器,它的比较方法是compare()
,可以看做平台性质的比较器。
如果业务中有比较需求,那么就需要修改这个类的比较方法compareTo()
,然而我们都知道开闭原则,即最好不要对自己已经交付的类进行修改。另外,如果另一个业务方也在使用这个比较方法呢?甚至再极端一点,需要比较的类是他人提供的,我们可能连源码都没有。所以,我们其实需要在外部定义比较器,即Comparator。
约定俗成,不管是Comparable还是Comparator,小于的情况返回-1,等于的情况返回0,大于的情况返回1。
hashCode和equals用来标识对象,两个方法协同工作可用来判断两个对象是否相等。众所周知,用哈希表,可以使存取元素更快。对象通过调用Object.hashCode()
生成哈希值;由于不可避免地会存在哈希值冲突的情况,因此当hashCode相同时,还需要再调用equals进行一次值的比较;但是,若hashCode不同,将直接判定对象不同,跳过equals,这加快了冲突处理效率。Object 类定义中对hashCode和equals要求如下:
(1)如果两个对象的equals的结果是相等的,则两个对象的hashCode 的返回结果也必须是相同的。
(2)任何时候重写equals, 都必须同时重写hashCode。
**fail-fast 机制:**是一种对集合遍历操作时的错误检测机制,在遍历中途出现意料之外的修改时,通过unchecked异常暴力地反馈出来。这种机制经常出现在多线程环境下,当前线程会维护一个计数比较器,即expectedModCount,用来记录已经修改的次数。在进入遍历前,会把实时修改次数modCount赋值给expectedModCount,如果这两个数据不相等,则抛出异常。java.util下的所有集合类都是fail-fast,而concurrent包中的集合类都是fail-safe。