前言
java中的锁大家很快就能想到synchronized和lock,那么synchronized实现原理是怎样呢?本文将深入讲解synchronized实现原理。
简介
synchronized关键字保证方法或者代码块在运行时,同一时刻只有一个方法可以进入临界区,同时它还可以保证共享变量的内存可见性。
实现原理
synchronized 可以作用于方法和代码块,具体如下图:
作用于方法和代码块的实现原理是不同的,代码块采用的是monitorenter、monitorexit指令。而方法采用的是ACC_SYNCHRONIZED指令。
作用于代码块
public synchronized void test1(){
synchronized (this)
{
System.out.println("this is test method");
}
}
说明:从编译后的源码可以得知,代码块的同步jvm的实现采用的是关键字monitorenter、monitorexit。每个对象都与一个monitor相关联。当且仅当拥有所有者时(被拥有),monitor才会被锁定。执行到monitorenter指令的线程,会尝试去获得对应的monitor。
加锁的实现
说明:加锁采用是monitorenter指令,每个对象维护着一个记录着被锁次数的计数器, 对象未被锁定时,该计数器为0。线程进入monitor(执行monitorenter指令)时,会把计数器设置为1.当同一个线程再次获得该对象的锁的时候,计数器再次自增.当其他线程想获得该monitor的时候,就会阻塞,直到计数器为0才能成功。
释放锁的实现
说明:释放锁采用的是monitorexi指令,线程执行monitorexit指令,就会让monitor的计数器减一。如果计数器为0,表明该线程不再拥有monitor。其他线程就允许尝试去获得该monitor。
作用于方法
说明:当调用一个设置了ACC_SYNCHRONIZED标志的方法,执行线程需要先获得monitor锁,然后开始执行方法,方法执行之后再释放monitor锁,当方法不管是正常return还是抛出异常都会释放对应的monitor锁。实现的流程图如下:
小结
不管是作用于方法还是同步块,其加锁的原理都是通过monitor来实现的,那么monitor如何实现呢?
Monitor实现原理
关于Java Monitor的工作机理如图所示:
- 获取monitor的线程,会进入_EntryList队列。
- 当某个线程获取到对象的monitor后,进入_Owner区域,设置为当前线程,同时计数器_count加1。
- 如果线程调用了wait()方法,则会进入_WaitSet队列。它会释放monitor锁,即将_owner赋值为null,_count自减1,进入_WaitSet队列阻塞等待。
- 如果其他线程调用 notify() / notifyAll() ,会唤醒_WaitSet中的某个线程,该线程再次尝试获取monitor锁,成功即进入_Owner区域。
- 同步方法执行完毕了,线程退出临界区,会将monitor的owner设为null,并释放监视锁。
小结
虽然monitor我们已经知道了相关的实现原理,但是monitor与对象又是如何进行关联呢?
对象与monitor关联
对象与monitor的关联其原理图如下:
对象结构
在虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头、实例数据和对象填充。
- 实例数据:对象真正存储的有效信息,存放类的属性数据信息,包括父类的属性信息;
- 对齐填充:由于虚拟机要求 对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。
- 对象头:Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Class Pointer(类型指针)。
对象头
而对象头又分为:Mark Word、类型指针、数组长度。
- Mark Word:主要用于存储自身运行时数据
- 类型指针:指向方法区中该 class的对象,JVM 通过此字段来判断当前对象是哪个类的实例
- 数组长度:当且仅当对象是数组时才会有该字段。
Mark Word
Mark Word 用于存储对象自身的运行时数据,包含哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。
说明:当锁其膨胀成重量级锁后,其他竞争的线程进来就不会自旋了,而是直接阻塞等待,并且 Mark Word 中的内容会变成一个监视器(monitor)对象,用来统一管理排队的线程。而monitor对象与每个对象都会关联。monitor对象本质上是一个同步机制,保证了同时只有一个线程能够进入临界区,在 HotSpot的虚拟机中,是由C++类ObjectMonitor实现的。