2023.10.28 关于 synchronized 原理
目录
synchronized 特性
synchronized 优化机制
锁升级(锁膨胀)
其他优化机制
锁消除
锁粗化
synchronized 特性
- 开始时是乐观锁,如果锁冲突频繁,就转为悲观锁
- 开始是轻量级锁,如果锁被持有的时间较长,就转化成重量级锁
- 实现轻量级锁的时候大概率用到的自旋锁策略
- 是一个不公平锁
- 是一种可重入锁
- 不是读写锁
synchronized 优化机制
锁升级(锁膨胀)
- JVM 将 synchronized 分为以下四个状态,会根据情况,依次进行升级
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁
无锁状态
- 即代码还未执行到加锁的代码块中,该状态称为无锁状态
偏向锁状态
- 偏向锁的获取和释放操作开销非常低,几乎可以忽略不记,因为它不需要进行线程间的竞争和同步
- 当一个线程第一次尝试获取锁时,会将锁的标记设置为偏向锁,并将线程ID 记录在锁的元数据中
- 当同一个线程再次尝试获取锁时,无需竞争,直接获取到锁,不需要进行任何同步操作
- 如果在整个使用锁的过程中,都没有出现锁竞争,那在 synchronized 执行完之后,取消偏向锁即可
- 如果期间另一个线程尝试获取锁,偏向锁会自动撤销,升级为真正的加锁状态,从而另一个线程也就只能阻塞等待了
注意
- 偏向锁的使用是 JVM 自动进行的,开发人员无需显式地使用偏向锁
- JVM 会根据锁的竞争情况自动选择使用偏向锁、轻量级锁、重量级锁,以优化锁的性能和吞吐量
轻量级锁状态
- 当 synchronized 发生锁竞争的时候,就会从偏向锁,升级成轻量级锁
- 此时 synchronized 相当于通过 CAS 操作,以不断自旋的方式来进行加锁
- 如果别人很快就释放锁了,自旋是划算的,但是如果迟迟拿不到锁,一直自旋显然是不划算的,因为会长期占用的 CPU 资源,造成性能损失
重量级锁状态
- 当然 synchronized 自旋不是无休止的自旋,自旋到一定程度之后,就会再次升级成 重量级锁(挂起等待锁)
- 如果线程进行了重量级锁的加锁,并且发生锁竞争,此时线程就会被放到阻塞队列中,暂时不参与 CPU 调度了
- 然后直到锁被释放,这个线程才有机会被调度到,并且有机会获取到锁
- 一旦 线程被切换出 CPU,此时就会变得比较低效了
注意:
- 在 JVM 主流实现中,只有锁升级,没有锁降级
其他优化机制
锁消除
- 编译器智能的判定,看当前的代码是否真的要加锁
- 如果这个场景不需要加锁,程序员也加了,就自动把锁给干掉
实例理解
- StringBuffer 中的关键方法都带有 synchronized
- 如果在单线程中使用 StringBuffer,synchronized 加了也白加,此时编译器就会直接把这些加锁操作消除掉了
锁粗化
锁的粒度
- synchronized 包含的代码越多,粒度就越粗
- 包含的代码越少,粒度就越细
通常理解
- 一般情况下,认为锁的粒度细一点是比较好的
- 加锁部分的代码,是不能并发执行的
- 锁的粒度越细能并发的代码就越多,反之越少
- 但有些情况下,锁的粒度粗一些反而更好
- 如上图,这里的间隙非常小,就算并发了,也没啥太大效果
- 然而每次加锁都是带有开销的
- 此时并发节省的时间,反而不如加锁的开销大
- 所以我们不如将其转变为直接加一把大锁
- 上述过程就相当于锁粗化