一、乐观锁与悲观锁
1.二者区别
乐观锁在写的时候不上锁,写之前检查版本是否被修改,如果被修改,就放弃这次操作;悲观锁是每次写的时候都需要上锁。
2.乐观锁实现
数据库中添加version字段,取出记录时,获取当前version
更新时, version + 1,如果where语句中的version版本不对,则更新失败
java
UPDATE product SET price=price+50, `version`=`version` + 1 WHERE id=1 AND `version`=1
如果versio已经被改变,就会重试,再次使用select语句重新获取新的数据
3.悲观锁实现
悲观锁依赖数据库的锁机制实现,主要分为两种:
- 行级锁:锁定单行数据(InnoDB 引擎支持,基于索引),粒度细,并发度高;
- 表级锁:锁定整张表(MyISAM 引擎默认,InnoDB 也可通过语法触发),粒度粗,并发度低。
核心前提:InnoDB 引擎下,必须在事务(BEGIN/START TRANSACTION) 中使用悲观锁,且事务未提交前,锁不会释放。
3.1行级悲观锁
通过 SELECT ... FOR UPDATE 或 SELECT ... LOCK IN SHARE MODE 实现,前者是排他锁(X 锁)(禁止其他事务修改 / 加排他锁),后者是共享锁(S 锁)(允许其他事务加共享锁,但禁止加排他锁)。
(1)排他锁 (FOR UPDATE ):最常用的悲观锁
适用场景:需要修改数据,防止其他事务同时修改。
sql
-- 步骤1:开启事务(会话1)
BEGIN;
-- 步骤2:查询并锁定目标行(必须通过索引条件,否则会升级为表锁)
SELECT * FROM user WHERE id = 1 FOR UPDATE;
-- 步骤3:修改锁定的数据(此时其他事务无法修改id=1的行)
UPDATE user SET balance = balance - 100 WHERE id = 1;
-- 步骤4:提交事务(释放锁)
COMMIT;
并发测试:
- 会话 1 执行到步骤 2 但未提交时,会话 2 执行 UPDATE user SET balance = balance - 100 WHERE id = 1 会阻塞,直到会话 1 提交 / 回滚;
- 如果会话 1 长时间不提交,会话 2 会等待直到超时(由 innodb_lock_wait_timeout 参数控制,默认 50 秒)。
(2)共享锁(LOCK IN SHARE MODE)
适用场景:只需要读取数据,防止其他事务修改,但允许其他事务读取。
sql
BEGIN;
-- 加共享锁
SELECT * FROM user WHERE id = 1 LOCK IN SHARE MODE;
-- 读取数据后提交
COMMIT;
3.2表级悲观锁(不推荐,仅作了解)
通过 LOCK TABLES 显式锁定整张表,会阻塞所有对该表的读写操作,InnoDB 下尽量避免使用。
sql
-- 锁定user表(读锁):允许当前会话读,禁止其他会话写
LOCK TABLES user READ;
-- 锁定user表(写锁):仅允许当前会话读写,禁止其他会话任何操作
LOCK TABLES user WRITE;
-- 解锁表(必须手动解锁,或会话断开自动解锁)
UNLOCK TABLES;
3.3在多线程层面使用synchronized
二、什么是CAS
锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文的切换和重新调度开销。
Java提供了非阻塞的volatile关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,但是volatile只能保证共享变量的可见性,不能解决读一改一写等的原子性问题。
CAS即CompareandSwap,其是JDK提供的非阻塞原子性 操作,它通过硬件保证了比较一更新操作的原子性。
它包含 3 个参数 CAS(V,E,N),V表示要更新变量的值,E表示预期值,N表示新值。仅当 V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做了更新,则当前线程则什么都不做。最后,CAS 返回当前V的真实值。(E其实就是旧的version,V是现在的version)
三、CAS具体示例
使用AtomicInteger来修饰共享变量,使用getAndIncrement函数进行自增操作
java
public class CasTest {
//使用AtomicInteger定义a
static AtomicInteger a = new AtomicInteger();
public static void main(String[] args) {
CasTest test = new CasTest();
Thread[] threads = new Thread[5];
for (int i = 0; i < 5; i++) {
threads[i] = new Thread(() -> {
try {
for (int j = 0; j < 10; j++) {
//使用getAndIncrement函数进行自增操作
System.out.println(a.incrementAndGet());
Thread.sleep(500);
}
} catch (Exception e) {
e.printStackTrace();
}
});
threads[i].start();
}
}
}
四、CAS原理
这里我们以上面的a.incrementAndGet()为例,来看看 Java 是如何实现原子操作的。
先看一下AtomicInteger里面都有什么,后面更好理解
java
public class AtomicInteger extends Number implements java.io.Serializable {
private volatile int value; // 核心变量:volatile保证内存可见性
private static final long valueOffset; // value的内存偏移量
private static final Unsafe unsafe = Unsafe.getUnsafe();
static {
try {
// 计算value字段在AtomicInteger中的内存偏移量
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
// 步骤1调用的方法:incrementAndGet
public final int incrementAndGet() {
// 步骤2的核心调用:委托给Unsafe的getAndAddInt,增量为1
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
1.先看incrementAndGet()的底层源码
java
public final int incrementAndGet() {
// 核心:调用Unsafe的getAndAddInt,增量为1,返回旧值+1(新值)
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
这条代码的关键是委托给getAndAddInt ------ 而getAndAddInt正是基于 "自旋 + CAS 原子指令" 实现的,这是原子性的根本来源。
2.线程 1 调用 unsafe.getAndAddInt (a, valueOffset, 1)
- var1 = a:AtomicInteger 实例(对应源码中的this);
- var2 =valueOffset:AtomicInteger中value字段的内存偏移量(通过objectFieldOffset计算,用于定位value的物理内存地址);
- var4 =1:自增的增量(对应源码中的1)。
java
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
首先,在 do while 循环开始,通过this.getIntVolatile(var1, var2)获取当前对象指定字段的值,将其存入临时变量 var5 中。这里的 getIntVolatile 方法能保证读操作的可见性,即读取的结果是物理内存中的最新值(而非 CPU 缓存的脏数据)。== 这是第一次获取想要更新的值,相当于预期值 ==。
3.线程 1 执行 compareAndSwapInt (a, valueOffset, 0, 1)
这是原子性的核心保障:
- compareAndSwapInt是 native 方法,底层调用 CPU 的lock cmpxchg指令;
- lock前缀会锁定 CPU 的系统总线 / 缓存行,确保多核 CPU 下,当前核心对内存地址 V 的 "比较(V 的值是否等于 0)+ 交换(更新为 1)" 操作不可中断(硬件级原子性);
- 整个操作是 "一次性完成" 的,不会被其他线程 / CPU 核心打断,彻底解决了普通i++"读取 - 修改 - 写回" 三步拆分的非原子问题。
4.while循环
- 执行compareAndSwapInt(var1, var2, var5, var5 + var4)进行 CAS 操作。如果对象 var1 在内存地址 var2 处的值(第二次获取这个值,相对于是想要更新的那个变量的最新值)等于== 预期值 var5==,则将该位置的值更新为 var5 + var4,并返回 true;否则,不做任何操作并返回 false。
- 如果 CAS 操作成功,说明我们成功地将 var1 对象的 var2 偏移量处的字段的值更新为 var5 + var4,并且这个更新操作是原子性的,因此我们跳出循环并返回 var5。
- 如果 CAS 操作失败,说明在我们尝试更新值的时候,有其他线程修改了该字段的值,所以我们继续循环 ,重新获取该字段的值,然后再次尝试进行 CAS 操作。
五、unsafe类
JDK的rt.jar包中的Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,它们使用JNI的方式访问本地C++实现库。下面我们来了解一下Unsafe提供的几个主要的方法以及编程时如何使用Unsafe类做一些事情。
long objectFieldOffset(Field field)方法:返回指定的变量在所属类中的内存偏移地址,
该偏移地址仅仅在该Unsafe函数中访问指定字段时使用。如下代码使用Unsafe类
获取变量value在AtomicLong对象中的内存偏移。
●int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址。
●int arrayIndexScale(Class arrayClass)方法:获取数组中一个元素占用的字节。
●boolean compareAndSwapLong(Object obj, long offiset, long expect, long update)方法:比较对象obj中偏移量为offset的变量的值是否与expect相等,相等则使用update值更新,然后返回true,否则返回false。
●public native long getLongvolatile(Object obj, long offset)方法:获取对象obj中偏移量为offset的变量对应volatile语义的值。
●void putLongvolatile(Object obj, long offset, long value) 方法:设置obj对象中offset偏移的类型为long的field 的值为value,支持volatile语义。
●void putOrderedLong(Object obj, long offset, long value)方法:设置obj对象中offset偏移地址对应的long型field的值为value。这是一个有延迟的putLongvolatile方法,并且不保证值修改对其他线程立刻可见。只有在变量使用volatile修饰并且预计会被意外修改时才使用该方法。
●long getAndSetLong(Object obj, long offset, long update)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量volatile语义的值为update。
1.Unsafe类的安全限制:只能是启动类加载器加载的类
Unsafe类:Unsafe 是一个底层工具类,本身不能直接通过 new 实例化(构造方法是私有的)。JVM 在启动时会自动初始化这个 theUnsafe 静态变量,作为 Unsafe 类的唯一实例,JDK 内部类通过 getUnsafe() 获取 theUnsafe,而普通应用类无法通过 getUnsafe() 获取,只能通过反射读取这个私有静态变量来突破权限限制
java
// Unsafe 类的核心定义(简化)
public final class Unsafe {
// 私有静态单例实例,外部无法直接访问
private static final Unsafe theUnsafe = new Unsafe();
// 私有构造方法,禁止外部实例化
private Unsafe() {}
// 对外提供的获取方法,但有权限校验
public static Unsafe getUnsafe() {
// 校验调用者的类加载器,只有启动类加载器加载的类才能调用
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader())) {
throw new SecurityException("Unsafe");
}
return theUnsafe;
}
}
java
package cn.tx.cas;
import sun.misc.Unsafe;
public class TestUnSafe {
//获取Unsafe的实例(2.2.1)
static final Unsafe unsafe = Unsafe.getUnsafe();
//记录变量state在类TestUnSafe中的偏移值(2.2.2)
static final long stateOffset;
//变量(2.2.3)
private volatile long state = 0;
static {
try {
//获取state变量在类TestUnSafe中的偏移值(2.2.4)
stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
} catch (Exception ex) {
System.out.println(ex.getLocalizedMessage());
throw new Error(ex);
}
}
public static void main(String[] args) {
//创建实例,并且设置state值为1(2.2.5)
TestUnSafe test = new TestUnSafe();
// (2.2.6)具体意思是,如果test对象中内存偏移量为stateOffset的state变量的值为0,则更新该值为1。
Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
System.out.println(sucess);
}
}
运行上面的代码,我们期望输出true,然而执行后会输出如下结果。

你在类加载阶段就执行了Unsafe.getUnsafe(),这段代码报错的核心原因是 Unsafe.getUnsafe() 存在严格的访问权限限制,JVM 不允许普通应用类直接调用这个方法。只有由 ** 引导类加载器(Bootstrap ClassLoader)** 加载的类(比如 JDK 核心类库),才能直接调用该方法。你的TestUnSafe类是由 ** 应用类加载器(App ClassLoader)** 加载的,所以调用Unsafe.getUnsafe()会触发SecurityException,导致静态初始化失败(ExceptionInInitializerError)。
2.解决Unsafe的安全限制:反射
java
package cn.tx.cas;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class TestUnSafe1 {
static final Unsafe unsafe;
static final long stateOffset;
private volatile long state = 0;
static {
try {
//使用反射获取Unsafe的成员变量theUnsafe的元信息
Field field = Unsafe.class.getDeclaredField("theUnsafe");
//突破私有限制
field.setAccessible(true);
//获取静态变量的值
unsafe = (Unsafe) field.get(null);
//获取state在TestUnSafe中的汇编语言偏移量
stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
} catch (Exception ex) {
System.out.println(ex.getLocalizedMessage());
throw new Error(ex);
}
}
public static void main(String[] args) {
//创建实例,并且设置state值为1(2.2.5)
TestUnSafe1 test = new TestUnSafe1();
// (2.2.6)
Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
System.out.println(sucess+"------>"+unsafe.getIntVolatile(test, stateOffset));
Boolean sucess1 = unsafe.compareAndSwapInt(test, stateOffset, 1, 10);
System.out.println(sucess1+"------>"+unsafe.getIntVolatile(test, stateOffset));
Boolean sucess2 = unsafe.compareAndSwapInt(test, stateOffset, 9, 20);
System.out.println(sucess2+"------>"+unsafe.getIntVolatile(test, stateOffset));
}
}
六、cas的优缺点
1.优点
一种非阻塞的轻量级的乐观锁,相比synchronized重量锁,synchronized会进行比较复杂的加锁,解锁和唤醒操作,性能比较低,而cas则不会有这方面的性能开销。
2.缺点
2.1ABA问题
所谓的 ABA 问题,就是一个值原来是 A,变成了 B,又变回了 A。这个时候使用 CAS 是检查不出变化的,但实际上却被更新了两次。
ABA 问题的解决思路是在变量前面追加上版本号或者时间戳。从 JDK 1.5 开始,JDK 的 atomic 包里提供了一个类AtomicStampedReference类来解决 ABA 问题。
这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果二者都相等,才使用 CAS 设置为新的值和标志。
java
//预期引用值,新的引用值,预期标记,新的标记
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
下面举个具体的例子:
java
package cn.tx.cas.aba;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABAAtomic1 {
private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);
public static void main(String[] args) {
Thread refT1 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
System.out.println("thread refT1:" + atomicStampedRef.getReference());
atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
System.out.println("thread refT1:" + atomicStampedRef.getReference());
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
System.out.println("before sleep : stamp = " + stamp); // stamp = 0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println("thread refT2:" + atomicStampedRef.getReference() + ",c3 is " + c3); //true
}
});
refT1.start();
refT2.start();
}
}

| 时间节点 | 线程 refT2(线程 2) | 线程 refT1(线程 1) | atomicStampedRef 状态(引用值,stamp) |
|---|---|---|---|
| 0 秒 | 启动,执行: 1. 获取初始 stamp=0,打印before sleep : stamp = 0 2. 调用sleep(2秒),进入休眠 |
启动,执行: 1. 调用sleep(1秒),进入休眠 |
(100, 0) |
| 1 秒 | 仍在休眠(剩余 1 秒) | 休眠结束,执行: 1. compareAndSet(100, 101, 0, 1): ✅ 预期引用 = 100、预期 stamp=0,匹配当前状态,修改为 (101, 1) 2. 打印thread refT1:101 3. compareAndSet(101, 100, 1, 2): ✅ 预期引用 = 101、预期 stamp=1,匹配当前状态,修改为 (100, 2) 4. 打印thread refT1:100 |
(100, 2) |
| 2 秒 | 休眠结束,执行: 1. 打印after sleep : stamp = 2(当前最新 stamp) 2. compareAndSet(100, 101, 0, 1): ❌ 预期引用 = 100(匹配),但预期 stamp=0≠当前 stamp=2,CAS 失败 3. 打印thread refT2:100,c3 is false |
执行完毕 | (100, 2) |
2.2长时间自旋
CAS 多与自旋结合。如果自旋 CAS 长时间不成功,会占用大量的 CPU 资源。
解决思路是让 JVM 支持处理器提供的pause 指令。
pause 指令能让自旋失败时 cpu 睡眠一小段时间再继续自旋,从而使得读操作的频率降低很多,为解决内存顺序冲突而导致的 CPU 流水线重排的代价也会小很多。
2.3多个共享变量的原子操作
CPU 的 cmpxchg 指令只能对一个内存地址执行 "比较 + 交换" 的原子操作。当对一个共享变量执行操作时,CAS 能够保证该变量的原子性。但是对于多个共享变量,CAS 就无法保证操作的原子性,这时通常有两种做法:
- 使用AtomicReference类保证对象之间的原子性,把多个变量放到一个对象里面进行 CAS 操作;
- 使用锁。锁内的临界区代码可以保证只有当前线程能操作。