本文总结下Synchronized关键字的底层实现原理。
synchronized是JVM内置锁,通过内部对象Monitor(监视器锁)来实现,基于进入与退出monitor对象来实现方法与代码块的同步,监视器锁的实现,最终依赖操作系统的Mutex lock(互斥锁)来实现。
synchronized 主要有3种使用方式。
1.同步类方法
public synchronized void method()
{
// todo
}
锁的是当前类对象;
2.同步代码块
public class TestNotes {
private static Object object;
public String decStock() {
synchronized (object) {
//todo
}
return "下单成功";
}
}
或者
public void run() {
synchronized(this) {
//todo
}
}
锁的是括号里面的对象;
3.修饰一个类
lass ClassName {
public void method() {
synchronized(ClassName.class) {
// todo
}
}
}
Mark Word
其中,锁的不同状态,就是存在对象头中的Mark Word区域中,
下图是32位系统的Mark Word的区域具体分布:
synchronized锁有如下4种状态:
锁的升级过程:无锁-》偏向锁-》轻量级锁-》重量级锁。注意,升级并不一定是一级级升的,有可能跨级别,比如由无锁状态,直接升级为轻量级锁。
下面,我们以这段代码为例,分析以下锁的具体升级过程:
public class TestNotes {
private static Object object;
//减库存
public String decStock() {
synchronized (object) {
//todo
}
return "下单成功";
}
}
这段代码中,synchronized锁的是object实例对象,锁的具体状态,就保存在object对象头部的markword区域。
步骤说明:
1.synchronized锁的object对象头部markword区域,最开始锁状态标志位,默认值就是001,也就是无锁状态。
某刻,线程1执行到同步代码块,虚拟机会使用CAS尝试修改状态标志位,修改为偏向锁状态,并且把线程1的线程ID记录到markword区域的23bit位,进入偏向锁状态,如下图:
进入偏向锁状态后,如果没有其他线程竞争,线程1后续再次访问同步代码块时,犹如没有锁一样,jvm不会再进行CAS加锁、解锁等步骤,直接运行同步块代码。直到线程1执行完毕后,jvm会释放偏向锁,将markword的标识位恢复初始状态。
线程1在持有偏向锁期间,线程2来了,下图右侧部分是线程2执行过程:
线程2访问同步代码块,尝试获取锁;此时jvm会检查线程1的状态,因为线程1还持有锁,jvm不能撤销线程1的锁,此时,jvm就会把锁升级位轻量级锁,也就是这个23bit区域存了线程1的地址,指向线程1的线程栈中的某块区域;同时线程栈的这块内存也保存了指向markword的引用,相当于两块区域互换了内容。
上文中描述的过程,是由无锁,然后变为偏向锁,然后是轻量级锁;
但是有些场景,锁会直接由无锁升级为轻量级锁,比如下图过程:
上图中,某一时刻,同时有两个线程执行到同步代码块,但实际肯定只能有一个线程先进入,假如是线程1,那么此时就会直接进轻量级锁状态。
此时,线程2就会进行CAS自旋,寻找机会获取轻量级锁,如下图:
上图中,进入轻量级锁状态后,线程2还会继续自旋尝试获取锁;这个时候,synchronized并不会立即进入重量级锁状态,而是等到线程2 自旋达到一定次数后,jvm才膨胀为下图的重量级锁。这个自旋次数,jdk7及以后可以通过jvm参数设置。
线程1执行完同步代码块,jvm尝试释放锁,修改markword为初始的无锁状态,在释放锁的时候,发现已经是重量锁了,说明有其他线程竞争,并且其他线程肯定已经进入了阻塞状态,那么jvm在释放锁之后,还会唤醒其他进入阻塞状态的线程。
synchronized在jdk 1.6版本进行了优化,性能有了巨大提升,基本上和java锁性能没有什么差异,所以在生产环境中,synchronized能满足的场景,尽量使用synchronized,简单方便。
优化的关键,是加入了轻量级锁,使用CAS,还有自适应机制,避免了向底层操作系统申请互斥量,避免了用户态和内核态的切换,也就是在一定程度上避免了线程上下文的切换,暂时不进入重量级锁的状态。