synchronized在1.6进行了优化后,已经不是那么重了,引入了偏向锁和轻量级锁,减少获得锁和释放锁消耗的性能。
Java中的每个对象都可以作为锁,具体表现为:

  • 对于普通同步方法,锁是当前实例对象
  • 对于静态同步方法,锁是当前类的class对象
  • 对于同步代码块,锁是synchronized括号后的对象

当一个线程试图访问同步代码块时,首先必须要获取锁,当退出或出现异常时,必须要释放锁。

JVM基于进入和退出monitor对象来实现方法和代码块的同步。

monitorenter指令是编译后插入到同步代码块开始的位置,而monitorexit插入到方法的结束和异常位置。
JVM要保证每个monitorenter都必须有相应都monitorexit与之匹配。
任何对象都有一个monitor与之关联,且当一个monitor被持有之后,它将会是锁定状态。线程执行到monitorenter时,会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁。

Java对象头

之前好像写过一篇new Object()会占几个字节, 但是找不到了。

synchronized用的锁信息是记录在Java的对象头上的。
对象头

大小 内容 说明
8字节 Mark Word 存储对象的hashCode或锁信息
8字节 Class Metadata Address 指向对象所对应的类
4字节 Array Length 如果对象是数组,会有这个

Mark Word
32位虚拟机下
无锁状态,Mark Word可以分为下面的结构

锁状态 25bit 4bit 1bit 是否是偏向锁 2bit 锁标识位
无锁状态 hashCode 分代年龄 0 01

Mark Word中存储对象的hashCode,分代年龄和锁标识位。
image.png

64位虚拟机下
image.png

锁的升级与对比

java 1.6为了减少锁性能开支引入了偏向锁和轻量级锁。
synchronized一共有四种状态,由低到高分别是无锁,偏向锁,轻量级锁,重量级锁。
锁的升级只能由低级向高级升级,不能降级。
这种锁只能升级不能降级的策略,目的是提高获得和释放锁的效率。

偏向锁
HotSpot的作者发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获取锁的代价更低,引入了偏向锁。
当一个线程访问同步块并获取锁的时候,会在对象的头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要通过CAS来加锁和解锁,只需要测试一下头的MardWord中是否存储着指向该线程的偏向锁。如果成功,表示线程已经获得了锁。如果失败,需要再判断一下MarkWord中的偏向锁标识是否为1(表示当前)。如果没有设置,使用CAS竞争锁。如果设置了,使用CAS将对象头的偏向锁指向当前线程。

  1. 偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制。偏向锁的撤销,需要等到一个全局安全点。它会检查持有偏向锁的线程是否存活,如果线程不处于活动状态,将对象头设置为无锁状态。如果线程仍然存活,会遍历偏向锁对象的锁记录,要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁。

  1. 关闭偏向锁

偏向锁在Java6和7是默认启用的,但是在应用程序启动后几秒才会激活,可以使用JVM参数来关闭延迟:
-XX:BiasedLockingStartupDelay=0
也可以通过JVM参数来关闭偏向锁:
-XX:- UseBiasedLocking=false

轻量级锁

  1. 轻量级锁加锁

线程在执行同步之前,会现在当前线程的栈帧中创建用于存储锁记录的空间,并将当前对象头中的MarkWord复制到锁记录中,官方称为Displace Mark Word。然后线程尝试使用CAS来尝试将MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁。如果失败,表示其他线程竞争锁,当前线程尝试使用自旋获得锁。

  1. 轻量级锁解锁

轻量级锁解锁时,会使用原子的CAS将Displace Mark Word替换回对象头,如果成功,说明没有竞争发生。如果失败,表示存在竞争,锁会膨胀成重量级锁。

因为自旋会消耗CPU,为了避免无效的自旋,一旦锁升级成重量级锁,就不会再恢复为轻量级锁的状态。
在这个状态下,其他线程尝试获取锁时都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程会进行新的一轮抢锁。