java锁的剖析
无锁状态,偏向锁状态,轻量级锁状态,重量级锁状态
内存中的java对象(HotSpot虚拟机)
- 在32位系统下,存放Class指针的空间大小是4字节,MarkWord是4字节,对象头为8字节。
- 在64位系统下,存放Class指针的空间大小是8字节,MarkWord是8字节,对象头为16字节。
- 在64位开启指针压缩的情况下 -XX:+UseCompressedOops,存放Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节。
- 如果对象是数组,那么额外增加4个字节
64位开启指针压缩的对象内存大小计算:
// 对象A: 对象头12B + 内部对象s引用 4B + 内部对象i 基础类型int 4B + 对齐 4B = 24B
// 内部对象s 对象头12B + 2个内部的int类型8B + 内部的char[]引用 4B + 对齐0B = 24B
// 内部对象str的内部对象char数组 对象头12B + 数组长度4B + 对齐0B = 16B
// 总: 对象A 24+ 内部对象s 24B + 内部对象s的内部对象char数组 16B =64B
class A {
String s = new String();
int i = 0;
}
markword和klass 。第一部分markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。 另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。
普通对象(32位)
|--------------------------------------------------------------|
| Object Header (8byte->64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|
数组对象(32位)
|---------------------------------------------------------------------------------|
| Object Header (8+4=12byte->96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|
Mark Word结构(32位)
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased偏向锁 |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|
Mark Word结构(64位)
|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01| Normal |
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01| Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00| Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10| Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11| Marked for GC |
|--------------------------------------------------------------------|--------------------|
Monitor
管程,监视器。存在着semaphore和mutex,即信号量和互斥量。重量级锁
一个重要特点是,在同一时间,只有一个线程/进程能进入monitor所定义的临界区,这使得monitor能够实现互斥的效果。无法进入monitor的临界区的进程/线程,应该被阻塞,并且在适当的时候被唤醒。显然,monitor作为一个同步工具,也应该提供这样管理线程/进程的机制。
C语言不支持monitor,而java支持monitor机制。
如果使用
synchronized
给对象上重量级锁之后,指定的锁对象通过某些操作将对象头中的ptr_to_heavyweight_monitor
指向monitor 的起始地址与之关联,同时monitor 中的Owner存放拥有该锁的线程的唯一标识,确保一次只能有一个线程执行该部分的代码,线程在获取锁之前不允许执行该部分的代码。
上图仅为简易的内存模型,实际还有以下属性:
Owner:初始时为NULL表示当前没有任何线程拥有该monitor,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ(对应entryList-block):关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor失败的线程。
RcThis(对应waitset):表示waiting在该monitor上的所有线程的个数。
Nest:用来实现重入锁的计数。
HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值:0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。
synchronized
可以保证方法或代码块在运行时,同一时刻只有一个线程可以进入到临界区(互斥性),同时它还保证了共享变量的内存可见性。
编译成字节码:
monitorenter
设置被锁对象的mark word为monitor指针- 进行块内操作
- 重置mark word,唤醒entryList
- 异常处理,跳转exception_table
保证互斥性的措施:
- 同步代码块使用了 monitorenter 和 monitorexit 指令实现。
- 同步方法中依靠方法修饰符上的 ACC_SYNCHRONIZED 实现。
轻量级锁
有多线程访问,但没有竞争的条件(无阻塞)
void synchronized method1() {
method2();
}
void synchronized method2() {
//......
}
加锁时,Object中的hashCode会赋给锁记录中的头数据,并在object指向锁记录,并通过CAS替换将01改成00;
- 若该对象再加其他锁,CAS替换失败<竞争,锁膨胀>;对于锁膨胀,将采用重量级锁,被锁对象头改为Monitor地址并更改状态。
- 若该对象再加该锁(进入method2),再加一条记录;
自旋锁
重量级锁竞争的时候,还可以使用自旋的方式,避免切换上下文,当自旋失败后再阻塞,针对多CPU有效,JDK7以后,自动切换自旋;
偏向锁
Java6中引入偏向锁来优化,第一次使用CAS将线程ID设置到对象的mark word中,之后发现这个线程ID是自己的,就表示没有竞争,不用重新CAS(因为CAS原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟),只要以后不发生竞争,这个对象就归线程所有。
- 默认是开启的,对象的头是101
- 由于有延迟,所以刚开始的时候是001,再变成101;通过jvm参数:-xx:BiasedLocking startup Delay=0 #禁止延迟
- 禁用偏向锁,通过jvm参数:-xx:-UseBiasedLocking
- 对象调用 .hashCode()
- 当有多个线程调用锁,这个对象偏向锁撤销->转为轻量锁
- 当对象调用wait()/notify()也会撤销->转为重量锁
参考博客1
参考博客2
获取锁(monitorenter)的大概过程:
1. 当没有被当成锁时,这就是一个普通的对象,Mark Word记录对象的HashCode,锁标志位是01,是否偏向锁那一位是0。
- 当对象被当做同步锁并有一个线程A抢到了锁时,锁标志位还是01,但是否偏向锁那一位改成1,前23bit记录抢到锁的线程id,表示进入偏向锁状态。
- 当线程A再次试图来获得锁时,JVM发现同步锁对象的标志位是01,是否偏向锁是1,也就是偏向状态,Mark Word中记录的线程id就是线程A自己的id,表示线程A已经获得了这个偏向锁,可以执行同步锁的代码。
- 当线程B试图获得这个锁时,JVM发现同步锁处于偏向状态,但是Mark Word中的线程id记录的不是B,那么线程B会先用CAS操作试图获得锁,这里的获得锁操作是有可能成功的,因为线程A一般会自动释放偏向锁。如果抢锁成功,就把Mark Word里的线程id改为线程B的id,代表线程B获得了这个偏向锁,可以执行同步锁代码。如果抢锁失败,则继续执行步骤5。
- 偏向锁状态抢锁失败,代表当前锁有一定的竞争,偏向锁将升级为轻量级锁。JVM会在当前线程的线程栈中开辟一块单独的空间(锁记录),里面保存指向对象锁Mark Word的数据,同时在对象锁Mark Word中保存锁记录地址。上述两个保存操作都是CAS操作,如果保存成功,代表线程抢到了同步锁,就把Mark Word中的锁标志位改成00,可以执行同步锁代码。如果保存失败,表示抢锁失败,竞争太激烈,继续执行步骤6。
- 轻量级锁抢锁失败,JVM会使用自旋锁,自旋锁不是一个锁状态,只是代表不断的重试,尝试抢锁。从JDK1.7开始,自旋锁默认启用,自旋次数由JVM决定。如果抢锁成功则执行同步锁代码,如果失败则继续执行步骤7。
- 自旋锁重试之后如果抢锁依然失败,同步锁会升级至重量级锁,锁标志位改为10。在这个状态下,未抢到锁的线程都会被阻塞。
释放锁(monitorexit)的大概过程:
- 检查该对象是否处于膨胀状态并且该线程是这个锁的拥有者,如果发现不对则抛出异常。
- 检查Nest字段是否大于1,如果大于1则简单的将Nest减1并继续拥有锁,如果等于1,则进入到步骤3。
- 检查rfThis是否大于0,设置Owner为NULL然后唤醒一个正在等待的线程再一次试图获取锁,如果等于0则进入到步骤4。
- 缩小(deflate)一个对象,通过将对象的头置换回原来的HashCode等值来解除和monitor之间的关联来释放锁,同时将monitor放回到线程私有的可用monitor列表。
批量重偏向
如果多个线程访问但没有竞争,这时线程2获取的对象仍被线程1锁住,重偏向会重置被锁对象的线程Id;
当撤销偏向锁阈值超过20次,jvm会将被锁对象偏向于当前线程;
当批量撤销重偏向超过40次,还不断重偏向,就变为不可重偏向;(就是结束同步块时,被锁对象不会从00->101而是001)
锁消除
当加锁是局部变量时,JIT(即时编译编译器)会判断这个锁是无用的就去掉了,这个锁消除可以通过xx:-vm设置
wait()/notify()
当Owner线程在执行过程中发现条件不满足时,调用wait()方法进入waitset变成waiting状态;
waitset和lentryList都处于阻塞状态,不占用时间片;
entryList中的线程会在Owener线程释放时唤醒;
waitset线程会在owner调用notify()或notifyAll()时唤醒,仍进入entryList竞争;
sleep()和wait()的区别
- sleep()是 Thread方法,wait()是Object方法;
- sleep()不需要加sync;
- sleep()不会释放锁对象,但wait()会释放锁对象;