通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其 Java 实现! - 知乎
基本概念
悲观锁 (Pessimistic Lock):认为每次拿数据时,别人都会修改数据,因此每次操作都会上锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁 (Optimistic Lock):拿数据时不会上锁,但是更新数据时会判断期间数据是否被更新。数据没有更新,则更新数据;否则放弃操作。
实现方式
悲观锁 - 加锁
悲观锁的实现方式是加锁,加锁既可以是对代码块加锁(如 Java 的 synchronized 关键字),也可以是对数据加锁(如 MySQL 中的 排它锁 )。
乐观锁 - CAS 算法
CAS 全称为 Compare And Swap,即比较并交换。CAS 算法使用了三个操作数,待操作数的内存地址 V,待操作数初值 A,待操作数新值 B。
具体过程为:执行乐观锁后,内存地址 V 记录要操作数的内存地址,并当前操作数的值赋予 A;当需要修改数据、将数据修改为 B 时,查看内存地址 V 对应值,如果该值与 A 相等,即期间数据没有变动,则将数更新为 B;否则不进行操作。
许多 CAS 的操作是自旋的:如果操作不成功,会一直重试,直到操作成功为止。
CAS 是由 CPU 支持的原子操作,由硬件层面来保证原子性。
乐观锁 - 版本号机制
版本号机制的基本思路是在数据中增加一个字段 version,表示该数据的版本号,每当数据被修改,版本号加 1。当某个线程查询数据时,将该数据的版本号一起查出来;当该线程更新数据时,判断当前版本号与之前读取的版本号是否一致,如果一致才进行操作。
这里版本号并非一定是类似 APP 版本号的形式,也可以时间戳等能标志数据版本的数据。
下面以“更新玩家金币数”为例(数据库为 MySQL,其他数据库同理),看看悲观锁和版本号机制是如何应对并发问题的。
考虑这样一种场景:游戏系统需要更新玩家的金币数,更新后的金币数依赖于当前状态(如金币数、等级等),因此更新前需要先查询玩家当前状态。
优缺点 & 应用场景
乐观锁
- 相较于整个锁,更快
- 适用于多读的应用类型
- 只能保证一个变量操作的原子性
- 自旋时循环时间长
- 查询表 A,更新表 B 也无法使用版本号机制
应用场景
- 当竞争不激烈 (出现并发冲突的概率小)时,乐观锁更有优势,因为悲观锁会锁住代码块或数据,其他线程无法同时访问,影响并发,而且加锁和释放锁都需要消耗额外的资源。
- 当竞争激烈 (出现并发冲突的概率大) 时,悲观锁更有优势,因为乐观锁在执行更新时频繁失败,需要不断重试,浪费 CPU 资源。
补充内容
乐观锁是否加锁
(1)乐观锁本身是不加锁的,只是在更新时判断一下数据是否被其他线程更新了; AtomicInteger 便是一个例子。
(2)有时乐观锁可能与加锁操作合作,例如,在前述 updateCoins () 的例子中,MySQL 在执行 update 时会加排它锁。但这只是乐观锁与加锁操作合作的例子,不能改变“乐观锁本身不加锁”这一事实。
CAS 有哪些缺点
ABA 问题
假设有两个线程——线程 1 和线程 2,两个线程按照顺序进行以下操作:
(1)线程 1 读取内存中数据为 A;
(2)线程 2 将该数据修改为 B;
(3)线程 2 将该数据修改为 A;
(4)线程 1 对数据进行 CAS 操作
在第(4)步中,由于内存中数据仍然为 A,因此 CAS 操作成功,但实际上该数据已经被线程 2 修改过了。这就是 ABA 问题。
一些场景下 ABA 却会带来隐患,例如栈顶问题:一个栈的栈顶经过两次 (或多次) 变化又恢复了原值,但是栈可能已发生了变化。
比较有效的方案是引入版本号,内存中的值每发生一次变化,版本号都+1;在进行 CAS 操作时,不仅比较内存中的值,也会比较版本号,只有当二者都没有变化时,CAS 才能执行成功。Java 中的 AtomicStampedReference 类便是使用版本号来解决 ABA 问题的。
高竞争下的开销问题
在并发冲突概率大的高竞争环境下,如果 CAS 一直失败,会一直重试,CPU 开销较大。针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。当然,更重要的是避免在高竞争环境下使用乐观锁。
功能限制
CAS 的功能是比较受限的,例如 CAS 只能保证单个变量(或者说单个内存值)操作的原子性,这意味着:(1)原子性不一定能保证线程安全,例如在 Java 中需要与 volatile 配合来保证线程安全;(2)当涉及到多个变量(内存值)时,CAS 也无能为力。
除此之外,CAS 的实现需要硬件层面处理器的支持,在 Java 中普通用户无法直接使用,只能借助 atomic 包下的原子类使用,灵活性受到限制。
实际案例
Java 自增分析
i=0;两个线程分别对 i 进行++100 次,值是多少? 2-200
3 个步骤:取值,++ ,赋值。
| 线程 A | 线程 B |
|---|---|
| i=0 取值 | i=0 取值 |
| 99 次++ 计算 | |
| i=99 赋值 | |
| ++ 计算 | |
| i=1 赋值 | |
| i=1 取值 | |
| 99 次 ++计算 | |
| i=100 赋值 | |
| ++计算 | |
| i=2 赋值 | |
Java 悲观锁 & CAS 自旋锁
Java 默认的自增表达式并非原子操作。
Java 使用 synchronized 来提供悲观锁。
Java 使用 java.util.concurrent.atomic 包来提供 CAS 自旋锁。该包提供的原子类,利用 CPU 提供的 CAS 操作来保证原子性。包含 AtomicInteger 、 AtomicBoolean 、 AtomicLong 、 AtomicReference 等众多原子类。
public class Test {
//value1:线程不安全
private static int value1 = 0;
//value2:使用乐观锁
private static AtomicInteger value2 = new AtomicInteger(0);
//value3:使用悲观锁
private static int value3 = 0;
private static synchronized void increaseValue3(){
value3++;
}
public static void main(String[] args) throws Exception {
//开启1000个线程,并执行自增操作
for(int i = 0; i < 1000; ++i){
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
value1++;
value2.getAndIncrement();
increaseValue3();
}
}).start();
}
//打印结果
Thread.sleep(1000);
System.out.println("线程不安全:" + value1);
System.out.println("乐观锁(AtomicInteger):" + value2);
System. out. println ("悲观锁 (synchronized):" + value3);
}
}AtomicInteger 源码分析
Unsafe 是用来帮助 Java 访问操作系统底层资源的类(如可以分配内存、释放内存),通过 Unsafe,Java 具有了底层操作能力,可以提升运行效率;强大的底层资源操作能力也带来了安全隐患 (类的名字 Unsafe 也在提醒我们这一点),因此正常情况下用户无法使用。AtomicInteger 在这里使用了 Unsafe 提供的 CAS 功能。
offset 可以理解为 value 在内存中的偏移量,对应了 CAS 三个操作数 (V/A/B) 中的 V;偏移量的获得也是通过 Unsafe 实现的。
value 域的 volatile 修饰符:Java 并发编程要保证线程安全,需要保证原子性、可视性和有序性;CAS 操作可以保证原子性,而 volatile 可以保证可视性和一定程度的有序性;在 AtomicInteger 中,volatile 和 CAS 一起保证了线程安全性。关于 volatile 作用原理的说明涉及到 Java 内存模型 (JMM),这里不详细展开。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}
}
public final class Unsafe {
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset, int expected, int x) {
return compareAndSetInt(o, offset, expected, x);
}
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset, int expected, int x);
@HotSpotIntrinsicCandidate
public native int getIntVolatile(Object o, long offset);
}MySQL 悲观锁 & 版本号机制
无线程安全。
@Transactional
public void updateCoins(Integer playerId){
//根据player_id查询玩家信息
Player player = query("select coins, level from player where player_id = {0}", playerId);
//根据玩家当前信息及其他信息,计算新的金币数
Long newCoins = ……;
//更新金币数
update("update player set coins = {0} where player_id = {1}", newCoins, playerId);
}悲观锁
在查询玩家信息时,使用 select …… for update 进行查询;该查询语句会为该玩家数据加上排它锁,直到事务提交或回滚时才会释放排它锁;在此期间,如果其他线程试图更新该玩家信息或者执行 select for update,会被阻塞。
@Transactional
public void updateCoins(Integer playerId){
//根据player_id查询玩家信息(加排它锁)
Player player = queryForUpdate("select coins, level from player where player_id = {0} for update", playerId);
//根据玩家当前信息及其他信息,计算新的金币数
Long newCoins = ……;
//更新金币数
update("update player set coins = {0} where player_id = {1}", newCoins, playerId);
}版本号机制。
在初次查询玩家信息时,同时查询出 version 信息;在执行 update 操作时,校验 version 是否发生了变化,如果 version 变化,则不进行更新。
@Transactional
public void updateCoins(Integer playerId){
//根据player_id查询玩家信息,包含version信息
Player player = query("select coins, level, version from player where player_id = {0}", playerId);
//根据玩家当前信息及其他信息,计算新的金币数
Long newCoins = ……;
//更新金币数,条件中增加对version的校验
update("update player set coins = {0}, version = version + 1 where player_id = {1} and version = {2}", newCoins, playerId, player.version);
}MyBatis-Plus 乐观锁
MyBatis-Plus 的乐观锁通过 版本控制机制 实现。
步骤一:乐观锁配置
spring xml 方式:
<bean class="com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor" id="optimisticLockerInnerInterceptor"/>
<bean id="mybatisPlusInterceptor" class="com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor">
<property name="interceptors">
<list>
<ref bean="optimisticLockerInnerInterceptor"/>
</list>
</property>
</bean>spring boot 注解方式:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}步骤二:通过注解配置版本
说明: - 支持的数据类型只有: int, Integer, long, Long, Date, Timestamp, LocalDateTime - 整数类型下
newVersion = oldVersion + 1-newVersion会回写到entity中 - 仅支持updateById(id)与update(entity, wrapper)方法 - 在update(entity, wrapper)方法下,wrapper不能复用!!!
@Version
private Integer version;总结
Spring Boot 方式示例:
@Configuration
@MapperScan("按需修改")
public class MybatisPlusConfig {
// 旧版
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
return new OptimisticLockerInterceptor();
}
// 新版
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mybatisPlusInterceptor;
}
}