synchronized 被称为对象锁。
常见三种使用方法:
1)普通同步方法,锁是当前实例;
2)静态同步方法,锁是当前类的Class实例,Class数据存在永久代中,是该类的一个全局锁;
3)对于同步代码块,锁是synchronized括号里配置的对象。
public class SynchronizedBlock {
public void method() {
//上锁
synchronized (this) {
System.out.println("...");
}
System.out.println("...");
}
}
反汇编后的代码(javap -c)
public void method();
0 aload_0 [this]
1 dup
2 astore_1
3 monitorenter //在同步块开始位置插入monitorenter指令
4 getstatic java.lang.System.out : java.io.PrintStream [15]
7 ldc <String "..."> [21]
9 invokevirtual java.io.PrintStream.println(java.lang.String) : void [23]
12 aload_1
13 monitorexit //在同步块结束位置插入
14 goto 20
17 aload_1
18 monitorexit //在抛出异常位置释放锁
19 athrow //抛出异常指令
20 getstatic java.lang.System.out : java.io.PrintStream [15]
23 ldc <String "..."> [21]
25 invokevirtual java.io.PrintStream.println(java.lang.String) : void [23]
28 return
通过反汇编代码可以观察到:
同步代码块是使用MonitorEnter和MoniterExit指令实现的,在编译时,MonitorEnter指令被插入到同步代码块的开始位置,MoniterExit指令被插入到同步代码块的结束位置和异常位置。任何对象都有一个Monitor与之关联,当Monitor被持有后将处于锁定状态。MonitorEnter指令会尝试获取Monitor的持有权,即尝试获取锁。
同步方法依赖flags标志ACC_SYNCHRONIZED实现,字节码中没有具体的逻辑,可能需要查看JVM的底层实现(同步方法也可以通过Monitor指令实现)。ACC_SYNCHRONIZED标志表示方法为同步方法,如果为非静态方法(没有ACC_STATIC标志),使用调用该方法的对象作为锁对象;如果为静态方法(有ACC_STATIC标志),使用该方法所属的Class类在JVM的内部对象表示Klass作为锁对象。
下面是摘自《Java虚拟机规范》的话:
Java虚拟机可以支持方法级的同步和方法内部一段指令序列的同步,这两种同步结构都是使用管程(Monitor)来支持的。
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。虚拟机可以从方法常量池中的方法表结构中的ACC_SYNCHRONIZED访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程将先持有管程,然后再执行方法,最后在方法完成(无论是正常完成还是非正常完成)时释放管程。在方法执行期间,执行线程持有了管程,其他任何线程都无法再获得同一个管程。如果一个同步方法执行期间抛出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的管程将在异常抛到同步方法之外时自动释放。
同步一段指令集序列通常是由Java语言中的synchronized块来表示的,Java虚拟机的指令集中有monitorenter和monitorexit两条指令来支持synchronized关键字的语义,正确实现synchronized关键字需要编译器与Java虚拟机两者协作支持。
Java虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现。无论是显式同步(有明确的monitorenter和monitorexit指令)还是隐式同步(依赖方法调用和返回指令实现的)都是如此。
编译器必须确保无论方法通过何种方式完成,方法中调用过的每条monitorenter指令都必须有执行其对应monitorexit指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时monitorenter和monitorexit指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行monitorexit指令。
Java对象头
对象头含有三部分:Mark Word(存储对象自身运行时数据)、Class Metadata Address(存储类元数据的指针)、Array length(数组长度,只有数组类型才有)。
重点在Mark Word部分,Mark Word数据结构被设计成非固定的,会随着对象的不同状态而变化,如下表所示。
锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit锁标志位 | |
无锁状态 | hashCode | age | 0 | 01 | |
轻量级锁 | 执行栈中锁记录的指针 | 00 | |||
重量级锁 | 执行栈中锁记录的指针 | 10 | |||
GC标记 | 空 | 11 | |||
偏向锁 | 线程ID | Epoch | age | 1 | 01 |
Monitor
Monitor可以理解为一种同步工具,也可理解为一种同步机制,常常被描述为一个Java对象。当使用monitorenter和monitorexit指令的时候,就会进入它的一种模式,而被synchronized对像头的Mark word就相当于参数。
图中所知,锁的级别从低到高:无锁、偏向锁、轻量级锁、重量级锁。
优点 | 缺点 | 适用情况 | |
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步代码相差无几。 | 如果线程存在锁竞争,需要额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块的情况 |
轻量级锁 | 竞争的线程不会阻塞,提高了响应速度 | 长时间得不到锁的线程使用自旋消耗CPU | 追求响应速度。同步代码执行非常快 |
重量级锁 | 线程竞争不会使用自旋,不会消耗CPU | 线程出现竞争时会阻塞,响应速度慢 | 追求吞吐量。同步代码执行时间长 |
- 对象正常情况下是无锁状态
- 当线程A请求资源,则用cas的方式设置对象头,升级为偏向锁。
- 当线程B请求资源,用cas的方式设置对象头失败,则暂停A线程任务升级为轻量锁,再继续执行A。线程B以cas的方式继续请求资源
- 当线程B以cas的方式继续请求资源次数过多,升级为重量锁
重量级锁原理
- 所有请求锁的被阻塞的先会入到ContentionList中
- 当碰到锁释放时,ContentionList会被移入EntryList(排队),并唤醒EntryList的head节点成为就绪节点(OnDeck)
- Ower线程并不是把锁传递给OnDeck线程,OnDeck线程会和 正在请求锁的线程(进ContentionList之前)竞争锁资源(不公平)
- Ower线程中调用wait,会进入blocking队列
- 如果碰到notity/notityAll,blocking队列会移入EntryList