【Java并发】月薪30K必须知道的Java锁机制

并发环境下,多个线程会对同一个资源进行争抢,可能导致数据不一致的问题
编程语言引入锁机制,对资源进行锁定

“锁”是一个抽象概念
锁放在对象头中。
锁中记录了当前对象被哪个线程所占用
Java对象:
对象头、实例数据、对齐填充字节
对象头存放对象的运行时信息
对象头包含
- MarkWork
- 存储和当前对象运行时状态有关的数据
- hashcode
- 锁状态标志
- 指向锁记录的指针
- 偏向锁id
- 锁标志位
- ClassPoint
- 指针
- 指向当前对象类型所在方法去中断类型数据

32bit

无锁、偏向锁、轻量级锁、重量级锁
synchronized 编译后生成两个字节码指令
- monitorenter
- monitorexit
依赖此二指令实现线程同步
javac 编译 javac *.java
javap 反编译 javap -c *.class
得到可读性较高的字节码
synchronized同步机制围绕Monitor展开


monitor是依赖操作系统的mutex lock来实现的
Java系统是对操作系统线程的映射
每当挂起或者唤醒,都需要切换操作系统的内核态——这是“重量级的操作”,对程序性能有严重影响。


synchronized是如何优化的
(锁)四种状态是如何变化的?

“偏向”:monitor偏向于对指定的线程交出锁

MarkWord中,当锁标志位是01时,判断倒数第3个bit是否为1,
- 若为0,则非偏向锁。
- 若为1,则当前对象的锁状态为偏向锁
- 若为偏向锁,再读取MarkWord前23bit,即对象的线程ID,根据线程ID,判断是不是“老顾客”
- 若是: 则直接调用对象
- 若否:即多个对象同时竞争,则偏向锁将升级为轻量锁。
当锁状态为“偏向锁”时,通过MarkWord的线程ID来找到占有该锁的线程。
当锁状态升级为轻量级锁时,如何判断你线程与锁的绑定关系
MarkWord前30bit改为指向栈中锁记录的指针

线程判别所标志位是否为00轻量级锁
若是,线程在自己的虚拟机栈中开辟Lock Record的空间
(Lock Record 是线程私有的)
Lock Record 存放对象的
- MarkWord副本
- owner指针
“线程通过CAS尝试获取锁,一旦获得将复制对象头中的MarkWord道Lock Record中,并将LockRecord的owner指针指向该对象”

另一方面
对象的前30bit,生成一个指针,指向虚拟机栈中的Lock Record
如此边双向关联了 markwork <-> 线程虚拟机栈
“实现了线程和对象锁的绑定(他们互相知道了对方的存在)”

此时该对象已被锁定,获取道对象的线程就可以去执行一些任务

此时如果有其他线程也想获取这个对象
其他的线程将会“自旋”等待

“自旋”:一种轮询,线程不断地自我循环,检查目标对象的锁有没有被释放
- 若释放,获取之
- 若未释放,则进行下一轮循环
若对象锁很快被释放,相比OS挂起,自旋不需要进行系统中断和现场恢复,效率更高。
自旋:相当于CPU在空转。长时间自旋,会浪费CPU资源。

自适应自旋:自旋的实践不固定,
由两个条件决定:
- 上一次在同一个锁上的自旋时间
- 锁状态
“在同一个锁上,当前正在自选等待的线程,刚刚已经成功获得过锁,但是锁目前被其他线程占用着,那么虚拟机就会认为这次自旋也很有可能会再次成功,进而它将允许更长的自选时间”

一旦自旋等待的线程数量超过1个,那么轻量级锁将会升级为重量级锁

若对象锁状态被标记为重量级锁,则需要通过Monitor来对线程进行控制,此时将会完全锁定资源,对线程的管控最为严格
(升级道重量锁,等于变回了OS Mutex)

锁、Java锁、对象头、MarkWord
synchronized -> monitor -> mutex lock
无锁->偏向->轻量->重量
