一、synchronized:JVM内置锁的底层原理

1.1 同步代码块与monitor机制

synchronized可以修饰方法或代码块,其底层依赖JVM的monitor(监视器)实现。当我们对一个代码块加锁时:

synchronized (lock) {
    // 临界区代码
}

编译后会在字节码中插入`monitorenter`和`monitorexit`指令——前者在进入同步块时执行,后者在正常或异常退出时执行。即使临界区抛出异常,monitorexit也会被触发,从而释放锁。这是一个重要的面试点:抛异常不会导致锁一直占用,避免死锁。

1.2 锁存放在哪里?——对象头

Java中每个对象都有一个对象头(Header),其中Mark Word区域记录了锁的相关信息。对象头的大小与JVM字宽一致:

32位JVM:Mark Word占32位(4字节)
64位JVM:Mark Word占64位(8字节)

> 小知识:两个不同对象的hashcode有可能相同,但对象头中除了hashcode,还存储了分代年龄、锁标记等。

1.3 锁的升级(膨胀)过程 —— 必考!

synchronized在JDK 1.6之后做了重大优化,引入了锁升级机制,且升级不可逆(偏向锁→轻量级锁→重量级锁)。

锁状态 适用场景 原理简述 优缺点
偏向锁 只有一个线程反复获取锁 在Mark Word中记录线程ID,该线程再次进入时无需任何同步操作 优点:开销几乎为0。缺点:一旦有第二个线程竞争,立即膨胀,且需要执行偏向锁撤销(STW)
轻量级锁 少量线程交替持有锁 通过CAS尝试将对象头的Mark Word复制到线程栈的Lock Record中,成功则获取锁;失败则自旋等待 优点:线程不用阻塞,响应快。缺点:自旋会消耗CPU,不适合大量线程竞争 
重量级锁* 多线程激烈竞争 竞争失败的线程进入阻塞队列,由操作系统内核完成线程调度 优点:不浪费CPU。缺点:线程挂起和唤醒的切换成本高(用户态↔内核态)

为什么锁升级不可逆?因为一旦出现过竞争,JVM认为后续竞争的概率依然存在,反向降级会引入复杂度且收益很小。

1.4 锁的优点与缺点

优点:
  使用简单,无需手动释放(JVM自动完成)。
  JVM层面做了大量优化(锁消除、锁粗化、自适应自旋)。
  与JVM调度完美整合,不会出现像ReentrantLock那样的忘记unlock。
缺点:
  重量级锁下性能差于显式Lock。
  锁升级过程不可控,不能像ReentrantLock那样尝试加锁(tryLock)。
  偏向锁在某些场景下撤销会引发STW。

二、CAS:无锁并发的基石

 2.1 什么是CAS?

CAS(Compare And Swap)是一种原子操作,需要调用操作系统内核函数才能实现真正的原子性。它包含三个参数:内存地址V,期望值A,新值B。只有当V的值等于A时,才将V更新为B,整个过程不可被中断(CPU指令级别保证)。

// 伪代码示意
boolean compareAndSwap(V, A, B) {
    if (V == A) {
        V = B;
        return true;
    }
    return false;
}

2.2 代码演示:CAS实现线程安全计数器

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
    private AtomicInteger atomicI = new AtomicInteger(0);
    private int i = 0;

    public static void main(String[] args) {
        final Counter cas = new Counter();
        List<Thread> ts = new ArrayList<>(600);
        long start = System.currentTimeMillis();

        // 启动100个线程,每个对普通int和AtomicInteger各累加10000次
        for (int j = 0; j < 100; j++) {
            Thread t = new Thread(() -> {
                for (int k = 0; k < 10000; k++) {
                    cas.count();
                    cas.safeCount();
                }
            });
            ts.add(t);
        }

        for (Thread t : ts) t.start();
        for (Thread t : ts) {
            try { t.join(); } catch (InterruptedException e) { e.printStackTrace(); }
        }

        System.out.println("非线程安全结果: " + cas.i);          // 大概率小于1000000
        System.out.println("CAS安全结果: " + cas.atomicI.get()); // 总是1000000
        System.out.println("耗时(ms): " + (System.currentTimeMillis() - start));
    }

    /** 使用CAS实现线程安全计数器 */
    private void safeCount() {
        for (;;) {
            int cur = atomicI.get();
            boolean suc = atomicI.compareAndSet(cur, cur + 1);
            if (suc) break;
        }
    }

    /** 非线程安全计数器 */
    private void count() { i++; }
}

讲解:普通`i++`不是原子操作(读-改-写三步),多线程下结果丢失。而CAS通过`compareAndSet`在循环中不断尝试,直到成功。这种方式避免了互斥锁的阻塞唤醒,但激烈竞争时CPU空转严重——此时反而不如重量级锁。

2.3 CAS的三大原子问题及解决方案

(1)ABA问题

描述:线程1将值从A改为B再改回A;线程2看到值还是A,CAS成功,但中间状态已被改变过。
影响:某些场景(如栈顶指针、链表头)可能造成错误。
解决方案:使用版本号。`AtomicStampedReference`同时保存引用和版本戳,每次更新同时递增版本号。

AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int[] stamp = new int[1];
String old = ref.get(stamp);
// 版本号必须匹配才能更新
ref.compareAndSet(old, "B", stamp[0], stamp[0] + 1);

(2)循环时间长开销大

自旋CAS如果长时间不成功,会大量消耗CPU。

解决:设置自旋次数上限,或退化为互斥锁(JVM自适应自旋)。

(3)只能保证一个共享变量的原子操作

解决:将多个变量封装成一个对象,使用`AtomicReference`对该对象进行CAS;或使用锁。

 2.4 总线锁与缓存锁

处理器实现原子操作依赖两种锁机制:

总线锁:使用LOCK#信号,锁定整个总线,其他处理器无法访问内存。代价高。
缓存锁:锁定某个缓存行,只禁止其他处理器修改同一缓存行。性能更好。

以下两种情形不会被缓存锁定:
1. 操作的数据跨越多个缓存行(无法被单个缓存行覆盖)。
2. 处理器不支持缓存锁定(老式CPU自动降级为总线锁)。

三、内存可见性与指令重排序

3.1 为什么会看到“过期的”数据?

每个CPU核心都有自己的高速缓存。一个线程对变量的修改可能暂时停留在缓存中,没有刷新到主内存,导致另一个线程看不到修改。这就是内存可见性问题。

3.2 指令重排序

现代CPU采用流水线技术提高指令吞吐量,但会带来重排序问题。从源码到最终执行,可能经历:

源代码 → 编译器重排序 → 指令级并行重排序 → 内存系统重排序 → 最终指令序列

`final`关键字可以阻止部分重排序(构造函数返回前保证初始化完成),但范围有限。

 3.3 内存屏障

JMM通过插入内存屏障(LoadLoad、StoreStore等)来禁止特定类型的重排序。`volatile`和`synchronized`底层都会插入内存屏障,从而保证可见性和有序性。

四、线程间如何通信

方式 原理 java示例
共享内存 线程读写同一块内存(堆中的变量) volatile、synchronized、CAS
消息传递 线程间显式发送消息/事件 wait/notify、BlockingQueue、管道流

> 所有实例域、静态域、数组元素都存储在堆内存中,堆是线程共享的。JMM决定一个线程对共享变量的写入何时对其它线程可见。

五、总结与面试高频追问

1、synchronized抛异常会释放锁吗?
  → 会,monitorexit指令保证释放。
2、锁升级可以降级吗?
  → 不能,不可逆。
3、CAS一定比锁快吗?
  → 不一定。低竞争时快,高竞争时自旋空转,不如重量级锁。
4、如何解决ABA?
  → AtomicStampedReference 或 AtomicMarkableReference。
5、volatile能保证原子性吗?
  → 不能,只能保证可见性和有序性。

希望这篇博客能帮你彻底拿下synchronized和CAS的面试关。加油!<(~^ - ^~)>

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐