synchronized的实现原理以及如何进化
首先说一下synchronized的使用方法
- 对于普通的同步方法,锁的是当前实例对象
- 对于静态方法,锁的是当前类的Class对象
- 对于同步方法块,锁的是synchronized括号里面配置的对象
对象头
为什么要说对象头呢,因为synchronized所用的锁是存在于对象头里面的,如果对象是数组类型,则虚拟机用3个字宽存储对象头,如果对象是非数字组类型,则为2个字宽。
Java对象头中默认储存的是对象的HashCode 、分代年龄标志 、和锁标记位。32位的JVM虚拟机中的Mark Word中默认的存储结构如图所示:
在运行期间,Mark Word中的数据会随着锁标志位的变化而变化。
64位也一样
锁的升级
在jdk1.6之前,synchronized是重量级锁的代名词,但是在jdk1.6之后,为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁“的概念。从此,synchronized锁便有了四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这几个状态会随着竞争的情况而不断升级。锁可以升级但是不能降级,目的就是为了提高获得锁和释放锁的效率。
1.偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是一个线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。当一个线程访问同步块并且获取锁的时候,会在对象头和栈帧中的锁记录里存储锁偏向的线程id,以后该线程在进入和退出同步块时就不需要进行CAS操作来加锁和解锁,只需要简单的测试一下对象头里面的Mark Word是否存储着指向当先线程的偏向锁。如果测试成功,表示线程已经获得了锁,如果测试失败,则需要再测试一下Mark Word中的偏向锁标志是否为1,如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。
1.1 偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码文件)。他会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还存活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么中心偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。
1.2 关闭偏向锁
偏向锁在jdk1.6和1.7里是默认启用的,但是他在应用程序启动几秒钟之后才会激活,如果有必要可以使用JVM参数来关闭延迟。
2.轻量级锁
2.1 轻量级锁加锁
线程在执行同步块之前,JVM会在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当先线程获得锁,如果失败,表示线程竞争锁,就会膨胀为重量级锁,但是线程不会立即阻塞,而是自旋次数达到一定阈值,没有达到阈值当前线程便尝试使用自旋获取锁。
2.2 轻量级锁解锁
轻量级锁解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败则表示当前锁存在竞争,所就会膨胀位重量级锁。
因为自旋会消耗CPU,为了避免无用的自旋(比如线程被阻塞住了),一旦锁升级成为重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态的时候,其他线程尝试获取锁时都会被阻塞住,当持有锁的线程释放锁后会唤醒这些线程,被唤醒的线程会进行新一轮的争夺。