
synchronized
定义
- Multi-threaded programs may often come to a situation where multiple threads try to access the same resources and finally produce erroneous and unforeseen results.So it needs to be made sure by some synchronization method that only one thread can access the resource at a given point of time.
应用方式
- 对于普通同步方法,锁是当前
实例对象
- 对于静态同步方法,锁是当前类的
class对象
- 对于同步方法块,锁是synchronized括号里配置的
对象
原理
JVM基于进入和退出Monitor对象来实现方法、代码块同步的,两者实现的细节不一样,但都是使用
monitorenter
和monitorexit
指令来实现的。monitorenter
指令是在编译后插入到同步代码块的开始位置,而monitorexit
是插入到方法的结束处和异常处,JVM要保证每个monitorenter
必须有对应的monitorexit
与之配对。
- 任何对象都有一个
Monitor
与之 关联,当且一个Monitor
被持有后,它将处于锁定状态。 - 线程执行到monitorenter指令时,将会尝试获取对象的锁。
- 而对于没有获取到对象锁的线程会阻塞在方法的入口,直到获取锁的线程执行
monitorexit
之后才能尝试继续获取锁。
流程如下:

验证
源码
1 | Object o = new Object(); |
使用jclasslib查看汇编指令

对象在内存中的存储
- 由三部分组成:
markword
(8个字节)、class pointer
(4个字节)、instance data
- 内存对齐,每个对象在内存中占的字节数必须是
8的倍数
,即%8=0,如果不满足,则会补齐至8的倍数。
对象头
- 在64位虚拟机下,markword是64位
- 用markword中最低的三位代表锁状态,其中1位是偏向锁位:
无锁—0 01
、偏向锁—1 01
- 后面两位是普通锁位:
轻量级锁—00
、重量级锁—01

锁升级
描述
JDK较早的版本,都是使用synchronized
,很多人称为重量锁
, 重量锁
效率比较低,因为它要向操作系统申请资源。jdk1.6
对synchronized
进行了优化,为了减少获取和释放锁带来的消耗,引入了轻量级锁
和偏向锁
的概念。锁一共有4种状态,级别从低到高分别是无锁 - 偏向锁 - 轻量级锁(自旋锁)-重量级锁
无锁
Object o = new Object()
锁的状态为:0 01 无锁态
1 | 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000001 |
执行o.hashCode()
后,对象头变为hashcode+0 01
1 | 00000000 00000000 00000000 00000111 10000000 11001011 01110111 00000001 |
偏向锁在JDK 6及以后的JVM里是默认启用的。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。
关闭偏向锁 -XX:+UseBiasedLocking -client -Xmx512m -Xms512m
问题:新实例化对象o是无锁,当对象使用sync(o)之后,为什么对象是轻量级锁状态而不是偏向锁状态呢?
因为偏向锁的启动有4s的时延,为什么要有时延呢?JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。
可使用以下命令关闭时延
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 -client -Xmx1024m -Xms1024m
偏向锁
偏向锁,偏向加锁的第一个线程。
适用于锁不存在多线程竞争,并且应由一个线程多次获得锁。markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个。
获得锁
当线程访问同步块时,会使用 CAS
将线程 ID 更新到锁对象的 Mark Word
中,如果更新成功则获得偏向锁,以后该线程进入或者退出同步块时不需要进行 CAS
操作来加锁和释放锁。只需检查一下锁对象的 Mark Word
中是否存储了当前线程的ID,如果是,表示当前已经获得偏向锁,否则再去检查一下 Mark Word
中偏向锁位是否是1,如果不是,使用 CAS
竞争锁,如果是,使用 CAS
将对象头的偏向锁指向当前线程。
释放锁
当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,偏向锁的撤销需要等到全局安全点(在这个时间点上没有正在执行的字节码),接着会暂停拥有偏向锁的线程,根据锁对象目前是否被锁,来判定将对象头中的 Mark Word
设置为无锁或者是轻量锁状态。
偏向锁可以提高带有同步却没有竞争的程序性能,但如果程序中大多数锁都存在竞争时,那偏向锁就起不到太大作用。这时应该关闭偏向锁,使程序直接进入轻量锁状态。
轻量级锁
锁升级为轻量级锁 - 每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁。
加锁
线程在进入同步块之前,如果同步对象为无锁状态时,当前线程会在栈帧中创建一个锁记录(Lock Record
)区域,同时将锁对象的对象头中 Mark Word
拷贝到锁记录中,再尝试使用 CAS
将 Mark Word
更新为指向锁记录的指针。
如果更新成功,当前线程就获得了锁。
如果更新失败 JVM
会先检查锁对象的 Mark Word
是否指向当前线程的锁记录。
如果是则说明当前线程拥有锁对象的锁,可以直接进入同步块。
不是则说明有其他线程抢占了锁,如果存在多个线程同时竞争一把锁,轻量锁就会膨胀为重量锁。
解锁
轻量锁的解锁过程也是利用 CAS
来实现的,会尝试锁记录替换回锁对象的 Mark Word
。如果替换成功则说明整个同步操作完成,失败则说明有其他线程尝试获取锁,这时就会唤醒被挂起的线程(此时已经膨胀为重量锁
)
轻量锁能提升性能的原因:
认为大多数锁在整个同步周期都不存在竞争,所以使用
CAS
比使用互斥开销更少。但如果锁竞争激烈,轻量锁就不但有互斥的开销,还有CAS
的开销,甚至比重量锁更慢。
重量级锁
自旋超过10次,升级为重量级锁(如果太多线程自旋 CPU消耗过大,不如升级为重量级锁)。当升级为重量级锁时,其他线程试图获取锁时,会阻塞进入等待队列(不消耗CPU),直到持有锁的线程释放锁后会唤醒这些线程,被唤醒的线程会再次一起竞争锁。
其他优化
适应性自旋
在使用 CAS
时,如果操作失败,CAS
会自旋再次尝试。由于自旋是需要消耗 CPU
资源的,所以如果长期自旋就白白浪费了 CPU
。JDK1.6
加入了适应性自旋:
如果某个锁自旋很少成功获得,那么下一次就会减少自旋。
总结
综上,偏向锁通过对比Mark Word解决加锁问题,避免执行CAS操作。而轻量级锁是通过用CAS操作和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。重量级锁是将除了拥有锁的线程以外的线程都阻塞。