Synchronized 天天用,实现原理你懂吗?
Java技术栈
www.javastack.cn
关注阅读更多优质文章
来源:小小木的博客
www.cnblogs.com/wyc1994666/p/11748212.html
Synchronized 关键字算是Java的元老级锁了,一开始它撑起了Java的同步任务,其用法简单粗暴容易上手。但是有些与它相关的知识点还是需要我们开发者去深入掌握的。
比如,我们都知道通过 Synchronized 锁来实现互斥功能,可以用在方法或者代码块上,那么不同用法都是怎么实现的,以及都经历了了哪些优化等等问题都需要我们扎实的理解。
1.基本用法
2.实现原理
2.1 同步代码块的实现
2.2 同步方法的实现
3.锁升级
3.1 Java对象头介绍
3.2 什么是锁升级
1.基本用法
通常我们可以把 Synchronized 用在一个方法或者代码块里,方法又有普通方法或者静态方法。
对于普通同步方法,锁是当前实例对象,也就是this
public class TestSyn{
private int i=0;
public synchronized void incr(){
i++;
}
}
对于静态同步方法,锁是Class对象
public class TestSyn{
private static int i=0;
public static synchronized void incr(){
i++;
}
}
对于同步代码块,锁是同步代码块里的对象
public class TestSyn{
private int i=0;
Object o = new Object();
public void incr(){
synchronized(o){
i++;
}
}
}
2.实现原理
在JVM规范中介绍了 Synchronized 的实现原理,JVM基于进入和退出Monitor对
象来实现方法同步和代码块同步,但两者的实现细节不一样。
代码块同步是使用monitorenter和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,通过一个方法标志(flag) ACC_SYNCHRONIZED来实现的。
2.1 同步代码块的实现
monitorenter 和 monitorexit
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html#jvms-6.5.monitorenter (参考来源)
下面看下JVM规范里对moniterenter 和 monitorexit的介绍
重点来了,上面这段介绍了两点:
通过monitorenter和monitorexit指令来实现Java语言的同步代码块(后面有代码示例)
monitorenter和monitorexit指令没有被用在同步方法上!!!
2.2 同步方法的实现
https://docs.oracle.com/javase/specs/jvms/se6/html/Compiling.doc.html#6530 (参考来源)
public class TestSyn {
private int i=0;
// 同步方法
public synchronized void incer(){
i++;
}
// 同步代码块
public void decr(){
synchronized (this) {
i--;
}
}
}
可以通过反编译字节码来查看底层是怎么实现的
// 得到字节码javac TestSyn.java
// 反编译字节码javap -v TestSyn.class
同步代码块的反编译结果如下:
同步方法的反编译结果如下:
3.锁升级
3.1 Java对象头介绍
对象的内存布局
在我们常见的HotSpot虚拟机中对象由三部分组成,分别是对象头,实例数据,以及对齐填充位。
其中对象头是跟锁信息相关的部分,在对象头里会存储该对象运行时数据,包括哈希吗,GC分代年龄,锁状态(无锁,偏向锁,轻量级锁,重量级锁),是否偏向锁,偏向线程ID等信息。
对象头的结构表示如下图:
mark word的表示如下图:
3.2 什么是锁升级
下面举个抢茅坑的例子来解释一下锁升级过程。
当只有一个线程访问时叫做偏向锁
假设我们每个厕所都有一把钥匙,要想使用厕所首先必须得获得锁。某天上午员工甲急急忙忙的打完卡上厕所了,并在厕所门上贴了 “工号007使用中”的标签,说明目前被工号007(相当于线程id)的员工占用呢,他再次向进入的时候只要上面的标签还显示工号007,他自己可以随便进入,不需要再次上锁了,有点偏向工号007员工的意思,所以这叫偏向锁。
发生竞争的时候升级成轻量级锁 (自旋等待)
员工甲正在使用厕所的时候,又来了两个人想用厕所,但发现厕所被人使用着呢,无法获得锁。所以只能在外面等着甲出来,他们等的过程叫做“自旋”,这个叫做轻量级锁。
那么又有一个问题,当甲出来之后正等着的那两个人谁活得锁呢?有两种方式,按到达的顺序来排队或者不排队,这两种都可以实现,前者叫做公平锁,后者叫做非公平锁。
自旋等待没结果的时候升级成重量级锁
但那两个人自旋一段时间之后发现甲还没出来(JDK1.6规定为10次),一直这么等也不是个法子啊,所以打算向上升级,找厕所管理员(操作系统)反馈,升级成了重量级锁了。
锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。另外关注公众号Java技术栈回复JVM46获取一份46页的JVM调优教程。
锁升级过程中mark word的变化如下:
偏向锁
偏向锁也是JDK 1.6中引入的一项锁优化, 引入它是为了优化在没有锁竞争场景下的锁消除。比如一段同步代码一直是由单个线程调用,在这种场景下就没必要使用同步锁了,这里指的同步锁不是指 synchronized,而是说没不要到操作系统层面的互斥量了。
偏向锁的偏向是指该同步代码会一直偏向第一个调用它的线程,直到有别的线程过来竞争这把锁,在第一次调用同步代码并获得锁时会在对象头和栈帧锁记录行(Lock Record)里存储偏向线程Id,该线程在此进入的时候就不需要重新申请锁了。只需检测对象头的Mark Word里是否存储着指向该线程的ID即可。
直到又有线程来竞争这把锁的时候偏向锁会撤销偏向。
轻量级锁
轻量级锁是JDK 1.6之中加入的新型锁机制, 它名字中的“轻量级”是相对于使用操作系统。
互斥量来实现的传统锁而言的, 因此传统的锁机制就称为“重量级”锁。它并不是用来代替重量级锁的, 它的本意是在统的重量级锁使用操作系统互斥量产生的性能消耗。
线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。
然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁.一直原地自旋,如果自旋数达到10次了则升级为重量级锁。
重量级锁
竞争的线程自旋一段时间未能获取锁之后会升级为重量级锁,这个时候锁的获取与释放都会由操作系统来分配了,如果持有锁的线程释放锁之后操作系统会唤醒所有阻塞的哪些线程,并进入新一轮的争抢模式,需要注意的是这些阻塞的线程没有获得锁的优先级,也就是说synchronized锁是非公平的。
除此之外synchronized对中断操作也是无感的,不会因为被中断而放弃阻塞等待,它要么得到锁要么一直阻塞。
点击「阅读原文」获取面试题大全~