通俗易懂 悲观锁、乐观锁、可重入锁、自旋锁、偏向锁、轻量/重量级锁、读写锁、各种锁及其 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 操作来保证原子性。包含 AtomicIntegerAtomicBooleanAtomicLongAtomicReference 等众多原子类。

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;
    }
}

原文:乐观锁 | MyBatis-Plus