图解乐观锁和悲观锁的区别 & 实现 & 使用场景
文章目录
- 图解乐观锁和悲观锁的区别 & 实现 & 使用场景
- 悲观锁
- synchronized 与 ReentrantLock
- 乐观锁
- CAS 机制
- 版本号机制
- 原子类
- 总结两种锁各自的使用场景
悲观锁
悲观主义者,认为这个资源不上锁,就一定会别的线程来争抢,造成数据错误
-
它每次操作资源都会上锁
-
共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程
-
像 Java 中
synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现
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
包下面的原子变量类(比如AtomicInteger
、LongAdder
)就是使用了乐观锁的一种实现方式 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);
}
}
总结两种锁各自的使用场景
- 悲观锁适用于并发写入多、竞争激烈场景,避免无用尝试。
- 乐观锁适用于多读少写或并发不激烈场景,提高性能。