synchronized的实现原理与应用
synchronized的同步基础:Java中的每一个对象都可以作为锁。
- 对于普通同步方法,锁是当前实例对象
- 对于静态同步方法,锁是当前类的Class对象
- 对于同步代码块,锁是Synchonized括号里配置的对象
static synchronizedTest instance=new synchronizedTest();
public void run() {
synchronized(instance){
//同步代码块
//*******
}
}
void synchronized method1() {} //类中的同步方法
void static synchronized method2() {} 类中静态同步方法
JVM基于进入和退出Monitor对象来实现方法同步和代码块的同步。
- 代码块同步是 使用monitorenter和monitorexit的指令实现的。
- 方法同步时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放monitor。在方法执行期间,其他任何线程都无法再获得同一个monitor对象。
monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM会保证每个monitorenter必须有对应的monitorexit与之配对。每个对象都会有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。
Java对象头
1.对象头:Java对象头一般占有2个机器码(在32位虚拟机中,1个机器码等于4字节,也就是32bit,在64位虚拟机中,1个机器码是8个字节,也就是64bit),但是 如果对象是数组类型,则需要3个机器码,因为JVM虚拟机可以通过Java对象的元数据信息确定Java对象的大小,但是无法从数组的元数据来确认数组的大小,所以用一块来记录数组长度。
2.实例数据:存放类的属性数据信息,包括父类的属性信息;
3.对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐;
java对象头里面的Mark Word里默认存储对象的HashCode、分代年纪和锁标志位。
对象头信息是与对象自身定义的数据无关的额外存储成本,但是考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,也就是说,Mark Word会随着程序的运行发生变化,可能变化为存储以下4种数据:
在64位虚拟机啊下,Mark Word是64bit大小的
锁的升级与优化
锁一共有四种状态: 无锁、偏向锁、轻量级锁、重量级锁。锁可以升级,但是不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。目的是为了提高获得锁和释放锁的效率。
偏向锁
因为很多时候不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低引入了偏向锁。
当一个线程访问同步块并获取锁时,会在对象头和栈帧的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
- 如果测试成功,则表示线程已经获得了锁
- 如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否被设置成了1
-
- 如果没有则使用CAS竞争锁
- 如果有,则尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销
偏向锁使用了一种等待竞争出现才会释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
撤销的过程:
偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)
- 首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置为无锁状态。如果线程还活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向与其它线程,要么恢复到无锁或者轻量级锁,最后唤醒暂停的线程。
轻量级锁
作用:在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥产生的性能消耗。
轻量级锁获得过程:
- 在代码块即将进入同步代码块的时候,如果此同步对象没有被锁定(锁的标志位为"01"状态),虚拟机首先将在线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝。
- 然后,虚拟机将使用CAS操作尝试把对象的Mark Word 更新为执行Lock Recoder的指针。如果这个操作成功了,则代表该线程拥有了这个对象的锁,并且Mark Word的锁标志位转为为"00",标识此对象处于轻量级锁状态。
- 如果更新失败了,那么久意味着至少存在一条线程与当前线程竞争获取该对象的锁
- 虚拟机首先会检查对象的Mark Words是否指向当前的栈帧。
- 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数
- 虚拟机首先会检查对象的Mark Words是否指向当前的栈帧。
-
- 否则则说明这个锁的对象已经被其他线程抢占了。
- 如果出现两条以上的线程争用同一个锁的情况,那么轻量级锁就不在有效,必须膨胀为重量级锁,锁的状态值变为"10",此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。
锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有 竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
- 当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁
- 这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
- 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址 然后自己进入 Monitor 的 EntryList BLOCKED
- 当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁 流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程