我们学习的synchronized是一个非常全面的锁,但是,如果想要进一步扩展锁的功能与使用,需要关注锁策略。 这些锁策略不是局限于java,任何的语言涉及到锁相关的内容都可以谈到这样的锁策略。
不是针对某一种锁,而是某一种锁有悲观or乐观的属性 描述的是加锁时遇到的场景 悲观锁:加锁的时候,预测到接下来锁的竞争会很激烈,需要针对这样的激烈情况额外做一些工作 乐观锁:加锁的时候,预测到接下来锁的竞争不会很激烈,不需要额外做一些工作
遇到场景之后的解决方案 轻量级锁:应对乐观的场景,付出的代价小=》更高效 重量级锁:应对悲观的场景,付出更多的代价=》更低效
挂起等待锁:重量级锁的典型实现 --操作系统内核级别,加锁的时候发现竞争,就会使线程进入阻塞态,后续需要内核进行唤醒 自旋锁:应用程序级别的锁,加锁的时候发现竞争,一般不会阻塞,而是通过忙等的形式来等待。 忙等在上一章中定时器部分提到过,忙等只适用于乐观锁,本身遇到竞争的概率就小,忙等在短时间内就能拿到锁,结束忙等,所以可以接受忙等。

自旋锁优缺点: 优点: 没有放弃 CPU, 不涉及线程阻塞和调度, ⼀旦锁被释放, 就能第⼀时间获取到锁. 缺点: 如果锁被其他线程持有的时间⽐较久, 那么就会持续的消耗 CPU 资源. (⽽挂起等待的时候是不 消耗 CPU 的).
以上三种锁之间的联系: 乐观锁=》轻量级锁=》自旋锁 悲观锁=》重量级锁=》挂起等待锁
synchronized是什么锁? synchronized是一个自适应性的锁,锁冲突小时它就变成轻量级锁、自旋锁,锁冲突大时它就变成重量级锁,挂起等待锁。
读写锁:写锁和写锁、写锁和读锁之间互斥,读锁与读锁之间不互斥。 互斥锁:都互斥 多个线程读写同一个数据,这个本身就是线程安全。 但是多个线程读写数据,两三个线程修改数据会涉及线程安全问题。
读写锁正是适用于这种读多写少的场景。如果读锁和读锁之间也互斥,那么锁冲突很严重,并且没必要,影响效率。保证线程安全的前提下,提高效率。
可重入锁:一个线程,一把锁,加锁多次,没有变成死锁 核心要点: 锁要记录当前是哪个线程在持有这把锁 使用计数器,记录加锁的次数,在合适位置解锁
当一个锁被释放时,这个锁给谁? 两种情况: 给最先开始等待这把锁的线程 概率均等,随机给
其实这两种概念都可以说是“公平”,但是定义这个的人认为第一种是公平,那么自然第二种也就是非公平。 synchronized采用随机调度方法,是非公平锁。
结合上⾯的锁策略, 我们就可以总结出, synchronized 具有以下特性(只考虑 JDK 1.8):
jvm将锁分为下面四种状态,根据情况进行升级:

第⼀个尝试加锁的线程, 优先进⼊偏向锁状态. 偏向锁不是真的 “加锁”, 只是给对象头中做⼀个 “偏向锁的标记”, 记录这个锁属于哪个线程.如果后续没有其他线程来竞争该锁, 那么就不⽤进⾏其他同步操作了(避免了加锁解锁的开销)如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进⼊⼀般的轻量级锁状态.偏向锁本质上相当于 “延迟加锁” . 能不加锁就不加锁, 尽量来避免不必要的加锁开销.但是该做的标记还是得做的, 否则⽆法区分何时需要真正加锁。
偏向锁本质上也是懒汉模式的思想的体现。
随着其他线程进⼊竞争, 偏向锁状态被消除, 进⼊轻量级锁状态(⾃适应的⾃旋锁). 此处的轻量级锁就是通过 CAS 来实现. • 通过 CAS 检查并更新⼀块内存 (⽐如 null => 该线程引⽤) • 如果更新成功, 则认为加锁成功 • 如果更新失败, 则认为锁被占⽤, 继续⾃旋式的等待(并不放弃 CPU). ⾃旋操作是⼀直让 CPU 空转, ⽐较浪费 CPU 资源.因此此处的⾃旋不会⼀直持续进⾏, ⽽是达到⼀定的时间/重试次数, 就不再⾃旋了.也就是所谓的 "⾃适应“,此时就会将锁变成重量级锁。
执⾏加锁操作, 先进⼊内核态. • 在内核态判定当前锁是否已经被占⽤ • 如果该锁没有占⽤, 则加锁成功, 并切换回⽤⼾态. • 如果该锁被占⽤, 则加锁失败. 此时线程进⼊锁的等待队列, 挂起. 等待被操作系统唤醒. • 经历了⼀系列的沧海桑⽥, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁.
编译器优化的体现。编译器会判定当前代码是否需要加锁,如果加的锁是无意义的锁,编译器会自动把synchronized消除,以此来提高效率。 此处的优化不像之前提到的优化那样造成线程安全问题。这里的优化是很保守的,在保证线程安全的情况下进行优化。所以这意味着,编译器只会优化掉它百分百确定的多加的锁,所以可能会漏掉该优化的锁。所以不能单单依靠编译器的优化来避免多使用锁,而是使用时就需要注意。
锁的粗化是用来修饰锁的粒度的。 加锁和解锁之间包含的代码越多,就认为锁粒度越粗,包含的代码越少,就认为锁粒度越细。(代码的多少不是指行数,而是指实际执行的次数/时间,比如循环代码就是很多)

一段代码中,反复针对细粒度的代码加锁,就可能被替换成更粗粒度的加锁,如上。如果是细粒度的写法,先不说加锁解锁的消耗,每次加锁还可能涉及到竞争。 代价: 粗粒度的代码会导致程序并发程度不高,代码串行化,所以不是任何时候锁粗化都是好的,而是有条件的。


本文围绕锁策略与synchronized原理展开,锁策略以 “场景权衡” 为核心,涵盖悲观 / 乐观、轻量 / 重量级等六大分类,且存在 “乐观锁→轻量锁→自旋锁”“悲观锁→重量级锁→挂起等待锁” 的对应关联;而 JDK1.8 的synchronized是自适应锁,兼具可重入、非公平等特性,通过 “无锁→偏向锁→轻量级锁→重量级锁” 的动态升级适配不同竞争场景,搭配锁消除、锁粗化优化,最终在线程安全与执行效率间实现最优平衡。