1、Atomic 原子类
1.1 基于 CAS 的实现
Atomic
原子类底层依赖 CAS (Compare-And-Swap) 算法,通过硬件提供的原子指令实现线程安全的更新。CAS 的关键操作包括以下三步:
- 比较:将内存中的值和预期值进行比较;
- 交换:如果两者相等,则用新值更新内存中的值;
- 返回结果:返回是否更新成功。
特点:
- 无需加锁,避免了线程阻塞,提高性能。
- 依赖硬件支持的
cmpxchg
指令,在多核 CPU 下性能较好。
CAS 的方法通常在 Java 中用 sun.misc.Unsafe
提供,原子类通过调用 Unsafe
的方法完成内存值的更新。
1.2 Unsafe 类
Atomic
类底层使用了 sun.misc.Unsafe
提供的内存操作方法,这是一种直接操作内存的工具,常见方法包括:
compareAndSwapInt
:更新int
类型的变量。compareAndSwapLong
:更新long
类型的变量。compareAndSwapObject
:更新引用类型的变量。
1.3 内存偏移量
为了找到变量在内存中的位置,Atomic
类利用 Unsafe
的 objectFieldOffset
方法获取变量的内存偏移量,通过偏移量直接定位变量位置。
示例(伪代码):
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) {
throw new Error(ex);
}
}
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
2、Synchronized
(具体细节可以看并发编程篇-09-线程安全-synchronized关键字的底层原理-基础回答_哔哩哔哩_bilibili)
对象的内存结构
处于不同状态的对象头
2.1 偏向锁
- 定义:偏向锁是为了优化无竞争场景设计的,当同一个线程多次加锁时,不需要再执行 CAS 操作。
- 过程:
- 第一次加锁时,使用CAS操作
Mark Word
,Mark Word
的偏向锁标志置为 1,记录当前线程 ID。 - 如果是同一个线程重复获取锁,只需检查
Mark Word
线程 ID,跳过 CAS。 - 如果其他线程尝试获取锁,偏向锁撤销并升级为轻量级锁。
- 第一次加锁时,使用CAS操作
- 特点:适用于无竞争的场景,能显著提高性能。
2.2 轻量级锁
- 定义:轻量级锁通过 CAS 操作和线程栈中的 Lock Record 来实现锁的快速获取和释放。
- 过程:
- 加锁时,线程在栈中创建 Lock Record,并通过 CAS 将对象头的
Mark Word
替换为指向 Lock Record 的指针。 - 如果 CAS 成功,则线程持有轻量级锁。
- 如果 CAS 失败,说明有其他线程竞争锁,轻量级锁升级为重量级锁。
- 加锁时,线程在栈中创建 Lock Record,并通过 CAS 将对象头的
- 特点:适用于低竞争的场景,避免了重量级锁的线程阻塞和唤醒开销。
2.3 重量级锁
- 定义:重量级锁使用 Monitor(监视器)来实现线程的挂起和唤醒,依赖操作系统的互斥锁实现。
- 过程:
当多个线程竞争同一个锁时,
Monitor
的作用流程如下:
线程尝试获取锁:
- 如果当前锁是空闲状态,线程会成为
Owner
(持有锁的线程)。- 如果锁已被其他线程持有,当前线程会被挂起,进入
EntryList
(阻塞队列),状态变为BLOCKED
。线程进入等待:
- 如果线程在持有锁的情况下调用
wait()
方法:
- 当前线程会释放锁。
- JVM 会将线程移动到
WaitSet
,状态变为WAITING
。- 等待其他线程调用
notify()
或notifyAll()
。唤醒和重新竞争:
- 当其他线程调用
notify()
或notifyAll()
时,WaitSet
中的线程会被唤醒,重新移动到EntryList
,进入BLOCKED
状态。- 从
EntryList
中选择一个线程,让它获取锁,成为新的Owner
。锁释放:
- 当前持有锁的线程执行完同步代码块后,释放锁。
- 如果
EntryList
中有线程等待,Monitor
会唤醒一个线程并赋予它锁的所有权。
- 特点:适用于高竞争场景,但上下文切换和线程挂起成本较高。
重量级锁-字节码层面
synchronized
会被编译为字节码指令,具体指令包括:
monitorenter
:进入 Monitor(加锁)。monitorexit
:退出 Monitor(解锁)。
示例代码
public void synchronizedMethod() {
synchronized (this) {
System.out.println("Inside synchronized block");
}
}
对应的字节码
0: aload_0 // 加载 `this` 引用
1: dup // 复制引用
2: monitorenter // 加锁
3: getstatic ... // 获取 `System.out`
6: ldc "Inside ..." // 加载字符串
8: invokevirtual ... // 调用 `println`
11: monitorexit // 解锁
12: goto 20
15: astore_1 // 异常处理
16: aload_0
17: monitorexit // 异常时释放锁
18: aload_1
19: athrow
20: return
可以看到:
monitorenter
和monitorexit
实现了加锁和解锁。- 异常处理时也确保
monitorexit
执行,避免死锁。
3、ReentrantLock
JAVA八股之源码解析----ReentrantLock原理源码解析-优快云博客