synchronized 详解

该片的最全版在第二个版本

Synchronized的性能变化

  • java5以前,只有Synchronized,这个是操作系统级别的重量级操作,重量级锁,假如锁的竞争比较激烈的话,性能下降。

  • 在Java早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个 Java 线程需要操作系统切换 CPU 状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高,这也是为什么早期的 synchronized 效率低的原因

  • .Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,需要有个逐步升级的过程,别一开始就直接使用重量级锁

Synchronized 原理

为什么每一个对象都可以成为锁?

  • Java对象是天生的 Monitor ,每一个Java对象都有成为 Monitor 的潜质,因为在 Java 的设计中 ,每一个 Java 对象自打娘胎里出来就带了一把看不见的锁,它叫做内部锁或者 Monitor 锁。
  • Monitor 的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的转换,成本非常高

每个 Monitor 对象 都包括 ContentionList 、EntryList 、 WaitList 、OnDeck 、 Owner、!Owner 这6个属性,每个属性的数据都代表不同的状态。

  • ContentionList :锁竞争队列,所有请求锁的线程都被放在锁竞争队列中。
  • EntryList:竞争候选队列,在 ContentionList 中有资格成为候选者来竞争锁资源的线程都被移动到了 EntryList 中。
  • WaitSet:等待集合,调用 wait 方法后被阻塞的线程放在 WaitSet 中
  • OnDeck:竞争候选者,在同一时刻最多只有一个线程在竞争锁资源,该线程的 Id 就被记录在 Ondeck 中
  • Owner:竞争到锁资源的线程 Id 被记录在 Owner 中。
  • !Owner:在 Owner 中记录的线程释放锁后,会被移动到 !Owner 中。

Synchronized 在收到新的锁请求时,先自旋,如果通过自旋也没获取到锁资源,则将被放入 ContentionList 中。当 Owner 线程释放锁时,将 ContentionList 中的部分线程移动到 EntryList 中,并指定 EntryList 中某个线程为 OnDeck 线程(一般为最先进入EntryList 的线程),Owner 线程并没有把 锁传递给 OnDeck ,而是把锁竞争的权力交给 OnDeck 线程,让 OnDeck 线程重新参与竞争锁,这是锁膨胀以及获取锁的一个简略过程,实际还有锁重入的判断。

1661862551920

无锁 0 0 1

引入 jar 包

1
2
3
4
5
6
7
8
9
10
<!--
JAVA object layout
官网:http://openjdk.java.net/projects/code-tools/jol/
定位:分析对象在JVM的大小和分布
-->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>

代码展示

1
2
3
4
5
6
7
8
9
public class MyObject{
public static void main(String[] args){
Object o = new Object();
System.out.println("10进制hash码:"+o.hashCode());
System.out.println("16进制hash码:"+Integer.toHexString(o.hashCode()));
System.out.println("2进制hash码:"+Integer.toBinaryString(o.hashCode()));
System.out.println( ClassLayout.parseInstance(o).toPrintable());
}
}

1661864798935

每次读取 4 个字节 前 8 个字节为 对象头中的 markword,后 4 个字节为 类型指针,最后是填充,类型指针原本是8字节,由于 JVM 优化对象头所占用的空间对其进行了压缩,这个可以配置。

可以发现对象头最后一个字节 的末尾 是 001 ,说明这是 当前对象是 无锁状态

偏向锁 1 0 1

轻量锁 X 0 0

重量锁 X 1 0