synchronized 原理
基本特定
- 开始时是一个乐观锁,如果锁冲突频繁,就变成悲观锁
- 开始是轻量级锁,如果锁被持有的时间比较长,就变成重量级锁
- 实现轻量级锁的时候大概率用到自旋锁的策略
- 是一种不公平锁
- 是一种可重入锁
- 不是读写锁
加锁过程
JVM 将 synchronized锁分为 无锁,偏向锁,轻量级锁,重量级锁,会依据情况来进行锁的升级
- 偏向锁
第一个尝试加锁的过程, 优先进入偏向锁状态
偏向锁不是真的 “加锁”, 只是给对象头中做一个偏向锁的标记, 记录这个锁属于哪个线程
如果后续没有其他线程来竞争该所, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)
如果后续有其他线程来竞争该所(刚才已经在锁对象中记录了当前锁属于哪个线程了,很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态
偏向锁本质上相当于"延迟加锁", 能不加锁就加锁, 尽量来避免不必要的加锁开销.
但是该做标记还是得做的, 否则无法区分何时需要真正加锁
- 轻量级锁
随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁)
此处的轻量级锁就是 通过CAS来实现
- 通过CAS检查并更新一块内存(比如 null => 该线程引用)
- 如果更新成功, 则认为加锁成功
- 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃CPU)
自旋操作时一直让CPU空转, 比较浪费CPU资源
因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不在自旋了
也就是所谓的"自适应"
- 重量级锁
如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁
此处的重量级锁就是指用到内核提供的 mutex
- 执行加锁操作, 先进入内核态
- 在内核态判定当前锁时是否已经被占用
- 如果该锁被占用, 则加锁失败, 此时线程加入锁的等待队列, 挂起, 等待被操作系统唤醒
- 如果锁没有被占用, 则加锁成功, 此时线程进入锁的等待队列, 挂起, 等待被操作系统唤醒
- 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程,于是唤醒这个线程, 尝试重新获取锁
其他的优化操作
锁消除
编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除.
什么是 “锁消除”
锁粗化
一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.