synchronized是jvm级别的一种重量级锁,但是随着jdk对synchronized的不断优化,现在它已经变得没有我们想象的那么重了。由于synchronized使用简单,也不用手动释放锁,因此我们平时开发中用到最多的锁就是它了。
synchronized锁的三种形式
- 普通方法:锁是当前对象实例
- 静态方法:锁是当前类的Class对象
- 同步方法块:锁是synchronized括号里的对象
实现原理
同步代码块在编译后会在前后分别插入monitorenter和monitorexit指令,每个对象在同一时刻只会与一个monitor相关联,当线程执行到monitorenter指令时就会尝试获取对象所对应的monitor的所有权,如果这个monitor已经被其他线程获取,则需要等待锁释放。
对象头
synchronized锁是存在对象头中的。如果对象是数组类型,则虚拟机用3个字宽存储对象头,如果对象是非数组类型,则用2个字宽存储对象头。在32位虚拟机中,1字宽等于4字节,即32bit。
补充一点:Java对象保存在内存中,由三部分组成:对象头、实例数据、对齐填充字节。Java头由三部分组成:Mark Word、指向类的指针、数组长度(只有数组对象才有)。
32位jvm的Mark Word的存储结构如下
锁状态 | 25bit | 4bit | 1bit是否偏向锁 | 2bit锁标志位 |
---|---|---|---|---|
无锁状态 | 对象的hashCode | 对象分代年龄 | 0 | 01 |
Mark Word中的数据随着锁标志位的变化而变化,如下
锁的升级
java1.6以后,为了减少获取锁和释放锁的性能消耗,引入了“偏向锁”和”轻量级锁“。锁的状态可以从无锁状态->偏向锁->轻量级锁->重量级锁,随着竞争情况逐渐升级,但是不能降级。
偏向锁
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得,为了让线程获得锁的代价更低引入了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解释,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果是,则直接获得锁,执行同步块;如果不是,则使用CAS操作更改线程ID,更改成功获得锁,更改失败开始撤销偏向锁。
撤销偏向锁
偏向锁只有存在锁竞争的情况下才会释放。撤销偏向锁需要等待全局安全点(在这个时间点上没有正在执行的字节码),首先暂停偏向锁持有的线程,然后检查此线程是否活着,如果线程不处于活动状态,则转成无锁状态;如果还活着,升级为轻量级锁。下图展示了偏向锁的获得与撤销过程
轻量级锁
- 加锁:线程在执行同步块之前,jvm会先在线程的栈帧中创建用于存储锁记录的空间,然后将对象的Mark Word复制到锁记录中,官方称Displaced Mark Word,再重试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,则获取锁;如果失败,表示其他线程竞争锁,当前线程使用自旋来获取锁。
- 解锁:轻量级锁解锁时,会使用CAS操作将Displaced Mark Word替换回对象头中,如果成功,表示没有竞争发生;如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图展示锁膨胀流程图。
重量级锁
因为自旋会消耗CPU,为了避免无用的自旋,一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。