文章目录
1. 概念补充
1.1. CAS(Compare-and-Swap)
即比较并替换,是一种实现并发算法时常用到的技术.一般来说CPU就会支持该指令。
比较并替换,简单来说就是 在替换前进行比较检查,如果跟我上次保存的值相同,说明没人操作过,那么替换,否则不替换。
private int value;
public synchronized int compareAndSwap(int expect, int new) {
int old = this.value;
if (old == expect) {
this.value = new;
}
return old;
}
value :内存地址中实际存放的值
expect:预期值
old :老的值
new :更新后的值
1.2. 自旋锁
如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将自旋在那里,直到该自旋锁的保持者释放了锁
自旋锁就是线程不挂起,不阻塞,弄个死循环每隔一段时间就尝试去获取锁,直到成功为止。
优点:通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)
缺点:过多占用cpu资源,自己死循环,一直占有cpu不释放,自己不执行任务,也不让别人执行,导致cpu计算资源的大量浪费
1.3. 自适应自旋锁
自适应自旋解决的是“锁竞争时间不确定”的问题
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间
如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源
1.4. 全局安全点
1.5. 偏向锁
“偏向”的意思是,偏向锁假定将来只有第一个申请锁的线程会使用锁(不会有任何线程再来申请锁),这是一中乐观锁策略
加锁
只需要在Mark Word中CAS记录owner(本质上也是更新,但初始值为空),如果记录成功,则偏向锁获取成功,记录锁状态为偏向锁,以后当前线程等于owner就可以零成本的直接获得锁.
锁膨胀
在退出同步块时不会释放锁。只有在第二个线程过来产生竞争的时候才会在 全局安全点
进行释放或者膨胀为轻量锁的操作。
批量再偏向
偏向锁这个机制很特殊, 别的锁在执行完同步代码块后, 都会有释放锁的操作, 而偏向锁并没有直观意义上的“释放锁”操作。
那么,如果一个对象先偏向于某个线程, 执行完同步代码后, 另一个线程如何重新获得偏向锁呢,这就要依靠JVM 提供的批量再偏向(()Bulk Rebias)机制
该机制的主要工作原理如下:
- 引入一个概念 epoch, 其本质是一个时间戳 , 代表了偏向锁的有效性, epoch 存储在可偏向对象的 MarkWord 中。
- 除了对象中的 epoch, 对象所属的类 class 信息中, 也会保存一个 epoch 值
- 每当遇到一个全局安全点时, 如果要对 class 进行批量再偏向, 则首先对 class 中保存的 epoch 进行增加操作, 得到一个新的 epoch_new
- 然后扫描所有持有 class
实例对象
的线程栈, 根据线程栈的信息判断出该线程是否锁定了该对象, 仅将 epoch_new 的值赋给被锁定的对象中。 - 退出安全点后, 当有线程需要尝试获取偏向锁时, 直接检查 class 中存储的 epoch 值是否与目标对象中存储的 epoch 值相等, 如果不相等, 则说明该对象的偏向锁已经无效了, 可以尝试对此对象重新进行偏向操作。
1.6. 轻量级锁
在jvm中禁用偏向锁时,如果线程取锁会自动建立轻量级锁
在jvm中使用偏向锁时,轻量锁只能由偏向锁膨胀而来
加锁
- 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word
- 拷贝对象头中的Mark Word复制到锁记录(Lock Record)中
- 使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,并将Lock record里的owner指针指向object mark word
- 如果第三步更新成功了,那么这个线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,如果失败,就检查下
释放锁
以一种是在推出同步代码块的时候会释放锁。释放的方式为使用原子的CAS操作来将Displaced Mark Word替换回到对象头。
锁膨胀
如果加锁失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。 如果自旋成功则依然处于轻量级状态。如果自旋失败,则升级为重量级锁。
轻量级解锁时,会使用原子的CAS操作来将Displaced Mark Word替换回到对象头,如果成功,则表示同步过程已完成。如果失败,表示有其他线程尝试过获取该锁,锁被升级为重量锁
1.7. 重量级锁
重量级锁依赖于操作系统的互斥量(mutex) 实现
加锁
由轻量锁切换到重量锁,是发生在轻量锁释放锁的期间,之前在获取锁的时候它拷贝了锁对象头的markword,在释放锁的时候如果它发现在它持有锁的期间有其他线程来尝试获取锁了,并且该线程对markword做了修改,两者比对发现不一致,则切换到重量锁。应该也是创建一个数据结构,里面包含了mutex,然后把markword里的指针指向该数据结构。
2. 对象内容
2.1. 对象头 Header
java的对象头由以下三部分组成:
- Mark Word x32:4byte | x64:8byte
- 指向类的指针 x32:4byte | x64:8byte
- 数组长度(只有数组对象才有)
2.1.1. Mark Word
锁状态 | 25bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否偏向锁 | 锁标志位 | ||
无锁 | 对象的HashCode | 分代年龄 | 0 | 01 | |
偏向锁 | 线程ID | Epoch | 分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向重量级锁的指针 | 10 | |||
GC标记 | 空 | 11 |
2.1.2. 对象指针
指向对象在堆里的存储位置。堆里存储的位置包含了两个部分 一个事对象实例数据 一个是到对象类型数据的指针。
2.1.3. 数组长度
int类型吧,没有找到太多具体的信息。
2.2. 实例数据 Instance Data
实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是在子类中定义的,都需要记录起来,这部分还包含了指向对象元数据类型的指针。
2.3. 对齐填充 Padding
第三部分对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说,就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。