乐观锁和悲观锁的区别 & 使用 & 使用场景 | 图解

时间:2024-11-16 07:56:27

图解乐观锁和悲观锁的区别 & 实现 & 使用场景

文章目录

  • 图解乐观锁和悲观锁的区别 & 实现 & 使用场景
    • 悲观锁
      • synchronized 与 ReentrantLock
    • 乐观锁
      • CAS 机制
      • 版本号机制
      • 原子类
    • 总结两种锁各自的使用场景

悲观锁

悲观主义者,认为这个资源不上锁,就一定会别的线程来争抢,造成数据错误

  • 它每次操作资源都会上锁

  • 共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

  • 像 Java 中synchronizedReentrantLock等独占锁就是悲观锁思想的实现

在这里插入图片描述

synchronized 与 ReentrantLock

使用 synchronized 修饰代码块:

public void method() {
    // 加锁代码
    synchronized (this) {
        // ...
    }
}

ReentrantLock 在使用之前需要先创建 ReentrantLock 对象,然后使用 lock 方法进行加锁,使用完之后再调用 unlock 方法释放锁,具体使用如下:

public class LockExample {
    // 创建锁对象
    private final ReentrantLock lock = new ReentrantLock();
    public void method() {
        // 加锁操作
        lock.lock();
        try {
            // ...
        } finally {
            // 释放锁
            lock.unlock();
        }
    }
}

乐观锁

乐观主义者,不担心没有发生的事,在修改数据不会上锁,但会判断与预期的是否一致。

  • 在提交修改的时候去验证对应的资源是否被其它线程修改了,就不去修改了【不会锁住被操作对象,不会不让别的线程来接触它】,如在MySQL中加个version字段(版本号机制)
  • 在 Java 中juc.atomic包下面的原子变量类(比如AtomicIntegerLongAdder)就是使用了乐观锁的一种实现方式 CAS 实现的。

在这里插入图片描述

CAS 机制

CAS 机制 是 Java 中 Unsafe 类中的一个方法,全称是 CompareAndSwap 比较交换的意思。它用于多线程下对共享变量修改的一个原子性。

CAS 涉及到三个操作数:

  • E:预期值(Expected)
  • V:要更新的当前变量值(Var)
  • N:拟写入的新值(New)

当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。


如下图,该线程要修改变量的值为 1,变量原来的值为 0

也就是 【V=0,E=0,N=1】

  • 线程 与 0 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 1
  • 线程 与 0 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败

在这里插入图片描述

版本号机制

在数据库中新增 version 列,代表版本号,通过版本号是否与之前的一致来判断数据是否被修改过。

  • 当读取数据时,会同时读取版本号。
  • 当更新数据时,会检查版本号是否发生变化。如果版本号与读取时的版本号一致,说明在此期间没有其他线程修改过数据,那么更新操作可以成功执行;如果版本号不一致,则说明数据已经被其他线程修改,此时需要重新读取数据并尝试更新。

如下图,user1 和 user2 同时在操作 id = 1 的数据,在修改这条数据时,会判断版本号是否一致,由于 user1 先修改成功了,并且把 version 自增了,所以 user2 的 version 与一开始查询的不一致会更新失败。

这样就能保证在多线程环境操作共享资源安全的问题,本质还是依靠数据库底层的排它锁实现的。

在这里插入图片描述

原子类

原子类是具有原子性的类,原子性的意思是对于一组操作,要么全部执行成功,要么全部执行失败,不能只有其中某几个执行成功,它的底层也是用到了 CAS 机制。

在JDK中J.U.C包下提供了种类丰富的原子类。

详情: - java.util.concurrent.atomic (Java SE 11 & JDK 11 )

以下用常用的AtomicInteger作为演示:

import java.util.concurrent.atomic.AtomicInteger;
 
public class AtomicIntegerDemo {
 
    private static final int THREADS_CONUT = 20;
    public static AtomicInteger count = new AtomicInteger(0);
 
    public static void increase() {
        count.incrementAndGet();
    }
 
    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_CONUT];
        for (int i = 0; i < THREADS_CONUT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 1000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
 
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(count);
    }
}

总结两种锁各自的使用场景

  • 悲观锁适用于并发写入多、竞争激烈场景,避免无用尝试。
  • 乐观锁适用于多读少写或并发不激烈场景,提高性能。