synchronized是JVM内置锁,重量级锁基于Monitor机制实现,依赖jvm去调用操作系统的互斥原语 Mutex(互斥量),会涉及到内核态的切换,性能较低。
在jdk1.5之后版本做了重大的 优化,如
锁粗化、
锁消除、
轻量级锁、
偏向锁、
自适应自旋 等技术,并发性能已经基本与Lock持平。

字节码指令

同步方法是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;
image.png
同步代码 块是通过monitorenter和monitorexit来实现
图中可以看到有两个monitorexit,第二个monitorexit用于抛出异常时释放锁
image.png

monitor 管程

管程是指管理共享变 量以及对共享变量操作的过程,让它们支持并发。
管程模型主要是解决并发编程中的两个核心问题,互斥同步
互斥是指同一时刻只允许一个线程访问共享资源,
同步则是指线程之间如何通信、写作。

MESA模型

image.png
一个完整的“等待—通知”机制如下:
线程首先获取互斥锁,当线程要求条件不满足时,释放互斥锁,进入等待状态;当条件满足时,通知等待的线程,重新获取锁
还需要注意,通知的时候虽然条件满足了,但是不代表该线程再次获取到锁时,条件还是满足的。
所以有一个编程范式:

  1. while(条件不满足) {
  2. wait();
  3. }

synchronized管程

Java 对 MESA 模型进行了精简。synchronized管程里只有一个条件变量。
synchronized关键字和wait()、notify()、notifyAll()这三个方法是 Java中实现管程技术的组成部分。
java.lang.Object 类定义了 wait(),notify(),notifyAll() 方法,这些方法的具体实现,依赖 于是 JVM 内部用C++ 实现 的ObjectMonitor

  1. ObjectMonitor() {
  2. _recursions = 0; // 锁的重入次数
  3. _owner = NULL; // 标识拥有该monitor的线程(当前获取锁的线程)
  4. _WaitSet = NULL; // 等待线程(调用wait)组成的双向循环链表,_WaitSet是第一个节点
  5. _cxq = NULL ; //多线程竞争锁会先存到这个单向链表中 (FILO栈结构)
  6. FreeNext = NULL ;
  7. _EntryList = NULL ; //存放在进入或重新进入时被阻塞(blocked)的线程 (也是存竞争锁失
  8. 败的线程)
  9. }

cxq 是先入后出的栈结构,所以synchronized是非公平的
image.png
这里就对应地就涉及到了java线程的状态:
_BLOCKED
(阻塞状态,和Synchronized相关)、
WAITING、TIMED_WAITING (等待)、

对象头Mark Word存储锁状态

image.png
为什么要对象填充?
(1)提升性能,假设对象是不等长的,那么为了获取一个完整的对象,就必须一个字节一个字节地去读,直到读到结束符,但是如果8字节对齐后,获取对象就可以以8个字节为单位进行读取,快速获取到一个对象,也不失为一种以空间换时间的设计方案
为什么是8个字节的倍数?
有两个原因,第一,我们对象头最大是16字节,而实例数据区最大的数据类型是8个字节,所以如果选择16字节对齐,假设有一个18字节的对象,那么我们需要将其填充成为一个32字节的对象,而选择8字节填充则只需要填充到24字节即可,这样不会造成更大的空间浪费。第二个原因,指针压缩会将原先的8字节指针压缩到4字节,
4字节在64位系统寻址不够用,
如果Java中对象存在8字节对齐的特性,所有对象的内存地址后三位永远是0
存储的时候,JVM会将对象内存地址的后三位的0抹去(右移3位),在使用的时候,将对象的内存地址后三位补0(左移3位),将仅占有32位的对象指针,变成实际上可以使用35位,也就是最大可以表示32GB的内存地址

锁状态被记录在每个对象的对象头的Mark Word中
Mark Word被设计成一个非固定的数据 结构,让同一个内存区域在不同阶段存储不同的内容
image.png
线程id存储的是操作系统线程的地址,不是java线程对象

markWord中锁标记枚举
image.png

synchronized锁升级

执行过程: 偏向锁——>轻量级锁——>重量级锁
1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁
3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
5. 如果失败,表示其他线程竞争锁,升级为重量级锁,当前线程先尝试使用自旋来获取锁, 如果自旋失败则线程park,park操作就涉及到了用户态到内核态的切换,开销大

也有可能是 无锁——>轻量级锁——>重量级锁