1、锁的四种状态
无锁、偏向锁、轻量级锁、重量级锁
2、Java对象头描述
以下为32位对象头描述
以下为64位对象头描述
3、锁的升级过程(Synchronized加锁/膨胀流程)
1)简单过程如下图
2)详细过程
当线程访问同步代码块时,首先判断当前锁状态是否为可偏向状态(对象头中偏向锁=1,锁标志=01)在JDK1.6以上默认开启,开启后程序启动几秒后才会被激活
(1)偏向锁
如果是可偏向状态,检查MarkWord存储的是否是当前线程ID
- 是,获得偏向锁,执行同步代码块
- 不是,CAS操作竞争锁,替换线程ID
- 替换成功,MarkWord的线程ID设置为当前线程ID(线程复用),执行同步代码块
- 替换失败,锁撤销,升级为轻量级锁
同一类对象多次撤销升级达到阈值20,则偏向锁认为,后面的锁需要重新偏向新的线程(批量重偏向)
如果阈值达到40次,则偏向锁认为偏向锁撤销过于频繁,后面直接使用轻量级锁
(2)轻量级锁
升级为轻量级锁的情况
- 如果不是可偏向状态,直接升级为轻量级锁
- 偏向锁撤销次数过多
加锁时,会在当前线程栈帧中划出一块空间,作为该锁记录,并且将锁对象MarkWord复制到该锁记录中,CAS操作将MarkWord更新为该锁记录的指针,锁记录中的owner指针指向对象头的MarkWord。
- 更新成功,MarkWord锁标志位为00,表示轻量级锁状态
- 更新失败,检查锁对象MarkWord是否指向当前线程栈帧中的锁记录
- 是,表示锁重入,在当前线程栈帧中锁记录+1
- 否,自旋等待(默认10次),等待次数达到阈值,升级为重量级锁
(3)重量级锁
升级为重量级锁的情况
- 竞争加剧,CAS自旋到一定次数升级为重量级锁
- 自旋线程数超过CPU核数的一半, 1.6之后,加入自适应自旋Adapative Self Spinning,JVM自己控制
获取锁成功,进入EntryList(获取锁的缓冲区、入口)
- 在调用wait方法后,会进入等待唤醒队列(WaitSet)等待
- 在调用notify方法后,则可能进入EntryList
获取锁失败,进入一个等待拿锁队列(cxq)等待
具体重量级锁加锁过程:
1、分配⼀个ObjectMonitor对象,把MarkWord锁标志置为‘10’,然后MarkWord存储指向ObjectMonitor对象的指针。ObjectMonitor对象有两个队列和⼀个指针,每个需要获取锁的线程都包装成ObjectWaiter对象
2、多个线程同时执行同⼀段同步代码时,ObjectWaiter先进⼊EntryList队列,当某个线程获取到对象的monitor以后进⼊Owner区域,并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count+1
说明:
monitor:每个Java对象都有一把锁,称为内部锁或monitor锁
owner,指向的是当前获得线程的地址,用来判断当前锁是被哪个线程持有
4、拓展
1)synchronized效率低?
用户态:偏向锁、轻量级锁
内核态:重量级锁
首先来了解下synchronized重量级锁实现原理?
通过对象内部的一个叫做监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操作系统的 Mutex Lock(互斥锁)来实现的。而操作系统实现线程之间的切换需要从用户态转换到核心态,这个成本非常高。
在JDK6以前,只有重量级锁,阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间。
在JDK6中,为了提高性能,引入了偏向锁和轻量级锁。
2)为什么要有偏向锁?
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了减少线程获得锁的代价,所以引入了偏向锁
3)为什么要有重量级锁?
自旋锁消耗CPU资源,重量级锁有等待队列,不会消耗CPU资源
4)偏向锁是否一定比自旋锁效率高?
不一定,在多线程竞争情况下,偏向锁会涉及锁撤销,这时候应该直接使用自旋锁
5)锁重入
重入次数必须记录,才能知道要解锁几次
- 轻量级锁,记录在线程栈,每插入一次,LockRecord+1
- 重量级锁,记录在ObjectMonitor字段上
6)Hopspot对象头就是MarkWord?
不是的,Hopspot对象头主要包括两部分数据:MarkWord(标记字段) 和 classPointer(类指针)
7)锁可以降级?
不行的,是一个不可逆的过程,主要是为了提高获得锁和释放锁的效率
8)锁对比,适用场景?
偏向锁:适用于一个线程,不会有锁消耗,锁撤销
轻量级锁:适用于多个线程竞争,但同步代码块执行快的情况下,因为自旋会消耗CPU
重量级锁:适用于多个线程竞争,但同步代码块执行慢的情况下,不消耗CPU,可以提高吞吐量