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,分代年龄和锁标识位。
64位虚拟机下
锁的升级与对比
java 1.6为了减少锁性能开支引入了偏向锁和轻量级锁。
synchronized一共有四种状态,由低到高分别是无锁,偏向锁,轻量级锁,重量级锁。
锁的升级只能由低级向高级升级,不能降级。
这种锁只能升级不能降级的策略,目的是提高获得和释放锁的效率。
偏向锁
HotSpot的作者发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获取锁的代价更低,引入了偏向锁。
当一个线程访问同步块并获取锁的时候,会在对象的头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要通过CAS来加锁和解锁,只需要测试一下头的MardWord中是否存储着指向该线程的偏向锁。如果成功,表示线程已经获得了锁。如果失败,需要再判断一下MarkWord中的偏向锁标识是否为1(表示当前)。如果没有设置,使用CAS竞争锁。如果设置了,使用CAS将对象头的偏向锁指向当前线程。
- 偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制。偏向锁的撤销,需要等到一个全局安全点。它会检查持有偏向锁的线程是否存活,如果线程不处于活动状态,将对象头设置为无锁状态。如果线程仍然存活,会遍历偏向锁对象的锁记录,要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁。
- 关闭偏向锁
偏向锁在Java6和7是默认启用的,但是在应用程序启动后几秒才会激活,可以使用JVM参数来关闭延迟:
-XX:BiasedLockingStartupDelay=0
也可以通过JVM参数来关闭偏向锁:
-XX:- UseBiasedLocking=false
轻量级锁
- 轻量级锁加锁
线程在执行同步之前,会现在当前线程的栈帧中创建用于存储锁记录的空间,并将当前对象头中的MarkWord复制到锁记录中,官方称为Displace Mark Word。然后线程尝试使用CAS来尝试将MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁。如果失败,表示其他线程竞争锁,当前线程尝试使用自旋获得锁。
- 轻量级锁解锁
轻量级锁解锁时,会使用原子的CAS将Displace Mark Word替换回对象头,如果成功,说明没有竞争发生。如果失败,表示存在竞争,锁会膨胀成重量级锁。
因为自旋会消耗CPU,为了避免无效的自旋,一旦锁升级成重量级锁,就不会再恢复为轻量级锁的状态。
在这个状态下,其他线程尝试获取锁时都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程会进行新的一轮抢锁。