1.锁的分类
2.悲观锁和乐观锁
- 悲观锁(互斥同步锁)
- 典型例子:Synchronized、lock
- 乐观锁(非互斥同步锁)
- 典型例子:原子类、并发容器
- 使用CAS算法实现
3.悲观锁和乐观锁的开销对比
- 悲观锁的原始开销要高于乐观锁,但是特点是一劳永逸,临界区持锁时间就算越来越长,也不会对互斥锁的开销造成影响
- 乐观锁一开始的开销比悲观锁小,但是如果自旋时间很长或者不停重试,那么消耗的资源也会越来越多
4.使用场景
- 悲观锁:适合并发写入多的情况,适用于临界区持锁时间比较长的情况,悲观锁可以避免大量的无用自旋等消耗
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
- 乐观锁:适合并发写入少,大部分是读取的场景,不加锁的能让读取性能大幅提升
5.可重入锁
- 典型例子:Synchronized、ReentranceLock
- getHoldCount() 获取可重入的次数
6.读锁插队策略和升降级
- 公平锁
- 不允许插队
- 非公平锁
- 写锁可以随时插队
- 读锁仅在等待队列头节点不是想获取写锁线程的时候可以插队,否则可能会造成饥饿现象
升降级策略:只能降级,不能升级
栗子1:读锁插队
1 | /** |
7.读写锁的适用场景
ReentranceReadWriteLock适用于读多写少的情况,合理使用可以进一步提高并发效率。
8.自旋锁
- 阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,这种状态转换需要耗费处理器时间
- 如果同步代码快中的内容过于简单,状态转换消耗的时间很可能比用户代码执行的时间还要长
- 在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失
- 如果锁被占用的时间很长,那么自旋的线程只会白浪费处理资源
- atomic包下的类基本都是自旋锁的实现
- 自旋锁实现原理是CAS,AtomicInteger中调用unsafe进行自增操作的源码中的do-while循环就是一个自旋操作
9.自旋锁的适用场景
- 自旋锁一般用于多核的服务器,在并发度不是特别高的情况下,比阻塞锁的效率高
- 适用于临界区比较短小的情况,否则如果临界区很大(线程一旦拿到锁,很久以后才会释放),那也是不适合的
10.锁优化
- 缩小同步代码块
- 尽量不要锁住方法
- 减少请求锁的次数
- 锁中尽量不要再包含锁
- 选择合适的锁类型或合适的工具类