多线程常见的锁策略
Posted 我要睡觉了∑
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程常见的锁策略相关的知识,希望对你有一定的参考价值。
文章目录
1. 乐观锁 VS 悲观锁
锁策略 | 说明 | 实现 |
---|---|---|
乐观锁 | 认为锁冲突是小概率事件,假设数据一般情况下不会产生并发冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生并发冲突进行检测,如果发现并发冲突了,则让返回用户错误的信息,让用户决定如何去做。 | 引入一个版本号,借助版本号识别出当前的数据访问是否冲突,如果冲突则将数据更新。 |
悲观锁 | 认为锁冲突是大概率事件,总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。 | 先加锁(比如借助操作系统提供的 mutex),获取到锁再操作数据们获取不到就等待。 |
乐观锁和悲观锁并无高下之分,主要还是看应用场景。
synchronized 既是一个乐观锁,又是一个悲观锁。能够根据实际场景,自动进行适应。
- 当锁冲突概率比较低时,synchronized 内部就是以乐观锁策略实现的。
- 当锁冲突概率比较高时,synchronized 内部就是以悲观锁策略实现的。
2. 读写锁
读写锁就是把读操作和写操作分别加锁,适用于读多写少的场景中,Java 标准库提供了 ReentrantReadWriteLock
类,该类实现了读写锁:
锁策略 | 实现类 | 补充 |
---|---|---|
读锁 | ReentrantReadWriteLock.ReadLock | 这个对象提供了 lock/unlock 方法进行加锁和解锁。 |
写锁 | ReentrantReadWriteLock.WriteLock | 这个对象提供了 lock/unlock 方法进行加锁和解锁。 |
其中读锁和写锁之间的关系如下:
- 读锁和读锁之间,不互斥
- 写锁和写锁之间互斥
- 读锁和写锁之间互斥
synchronized 不是读写锁。
3. 重量级锁 VS 轻量级锁
锁的核心特性原子性,追根溯源是 CPU 这样的硬件设备提供的:
- CPU 提供了原子操作指令
- 操作系统基于 CPU 的原子指令,实现了
mutex
互斥锁 - JVM 基于操作系统提供的互斥锁,实现了 synchronized 和 ReetrantLock 等关键字和类来进行加锁。
锁策略 | 说明 | 特点 |
---|---|---|
重量级锁 | 加锁机制重度依赖了 OS 提供的 mutex | 涉及到了大量的内核态和用户态的切换,易引发线程的调度。 |
轻量级锁 | 加锁机制尽可能不使用 mutex,而尽量在用户态代码完成,实在搞不定了才使用mutex | 很少会涉及内核态和用户态的切换,不容易引发线程调度 |
- synchronized 开始是一个轻量级锁,如果锁冲突比较严重,就会变成重量级锁。
- synchronized 内部的轻量级锁是基于自旋的方式实现的,重量级锁是基于挂起等待的方式实现的。
4. 自旋锁 VS 挂起等待锁
锁策略 | 说明 | 优点 | 缺点 |
---|---|---|---|
自旋锁 | 如果获取锁失败,就会立即再次获取锁,循环下曲直到获取到锁为止 | 没有放弃 CPU,不涉及到线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁 | 如果锁被其他线程持有的时间比较久,那就就会持续消耗 CPU 资源 |
挂起等待锁 | 如果获取锁失败,就会挂起等待,会放弃 CPU,进入内核的阻塞队列,直到锁被释放且 CPU 调度到该线程 | 节省 CPU 资源 | 速度比较慢,一旦锁释放不能够第一时间直到 |
- 自旋锁可以认为是轻量级锁的一种典型实现,也可以认为是乐观锁的一种典型实现。
- 挂起等待锁可以认为是重量级锁的一种典型实现,也可以认为是悲观锁的一种典型实现。
- sunchronized 开始是自旋锁,后面锁竞争激烈了就变成了挂起等待锁。
5. 公平锁 VS 非公平锁
如果有 A、B、C 三个线程,A 线程在获取到锁后,B 紧接着获取锁但失败了,C 最后也获取锁也失败了。当 A 释放锁后,如果按照先来后到的顺序获取锁,即 B 先获取锁,则是一个公平锁。如果不遵循先来后到,B 和 C 都有可能获取锁,则是一个非公平锁,
锁策略 | 说明 |
---|---|
公平锁 | 遵循先来后多 |
非公平锁 | 不遵循先来后到,随即调度 |
- 操作系统内部的线程调度可以视为是随机的,如果不做额外的限制,锁就是非公平锁。如果要实现公平锁,则需要依赖额外的数据结构来记录线程的先后顺序。
- synchronized 是非公平锁。
6. 可重入锁 VS 不可重入锁
锁策略 | 说明 | 实现 |
---|---|---|
可重入锁 | 允许同一个线程多次获取同一把锁 | 在锁中记录锁持有的线程的身份,以及一个计数器(记录加锁的次数),如果当前加锁的线程就是持有锁的线程,则直接计数自增。 |
不可重入锁 | 不允许同一个线程多次获取同一把锁 |
- Java 里只要以 Reentrant 开头命名的锁就是可重入的,而且 JDK 提供的所有现成的 Lock 实现类,包括 synchronized 关键字都是可重入的。
- Linux 系统提供的 mutex 是不可重入锁。
- 不可重入锁会产生死锁操作(即同一线程加锁后,在没释放锁之前又进行加锁,那么此时由于是不可重入锁,该线程则会阻塞等待,但是线程又需要释放锁了才能解除当前状态,所以就会死锁)。
- synchronized 是可重入锁。
以上是关于多线程常见的锁策略的主要内容,如果未能解决你的问题,请参考以下文章