很多初学者认为 Java 高并发就是背背 API,用用 synchronized 或者 ReentrantLock。但在图灵课堂的学习让我明白了一个道理:不懂底层原理,并发代码写出来就是定时炸弹。 真正的实战高手,关注的是 CPU 缓存一致性协议、对象内存布局以及 JVM 的锁优化机制。
一、 并发的基石:volatile 与可见性
在多线程环境下,为什么一个线程修改了布尔变量,另一个线程却“看不见”?这就要从 JMM(Java 内存模型) 说起。
Java 内存模型规定了所有变量都存储在主内存,每条线程还有自己的工作内存。线程对变量的操作必须在工作内存中进行,然后再回写到主内存。
底层原理:
volatile 关键字之所以能保证可见性,是因为它在汇编层面插入了一个 Lock 前缀指令。这个指令做了两件事:
- 将当前处理器缓存行的数据立即回写到系统内存。
- 这个回写操作会使在其他 CPU 里缓存了该内存地址的数据无效(即总线嗅探机制,遵循 MESI 协议)。
实战代码:
java
复制
public class VolatileDemo {
// 不加 volatile,主线程可能永远不会感知到 stop 的变化,导致死循环
private volatile static boolean stop = false;
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
int i = 0;
while (!stop) {
i++;
}
System.out.println("Thread stopped, count: " + i);
});
worker.start();
Thread.sleep(1000);
stop = true; // 修改 volatile 变量,强制刷新主内存
System.out.println("Main thread set stop to true");
}
}
二、 锁的本质:Monitor(管程)与对象头
synchronized 是 Java 开发中最常用的锁,但你真的知道它锁的是什么吗?它锁的不是代码块,而是对象。
在 HotSpot 虚拟机中,对象在内存中的存储布局分为三块:对象头、实例数据和对齐填充。锁的状态就保存在对象头的 Mark Word 中。
底层原理:
当线程执行到 synchronized 代码块时,会尝试获取对象的 Monitor。Monitor 是基于操作系统的 Mutex Lock(互斥锁) 实现的,挂起线程和恢复线程都需要操作系统从“用户态”切换到“内核态”,这个切换成本非常高。因此,JDK 1.6 之后对 synchronized 做了大幅度优化,引入了偏向锁、轻量级锁和重量级锁。锁会随着竞争情况逐渐升级,但只能升级不能降级。
实战代码:
java
复制
public class SynchronizedDemo {
private final Object lock = new Object();
public void safeMethod() {
// synchronized 锁住的是 lock 对象的 Mark Word
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + " is holding the lock.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行完 monitorexit 指令后,锁释放
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
// 模拟并发竞争
for (int i = 0; i < 3; i++) {
new Thread(demo::safeMethod, "Thread-" + i).start();
}
}
}
三、 CAS 与 AQS:无锁到高效的跨越
既然 synchronized 涉及内核切换开销大,那有没有纯用户态的解决方案?答案就是 CAS(Compare And Swap) 。
底层原理:
CAS 是 CPU 的原子指令(如 cmpxchg)。它包含三个操作数:内存值 V、旧的预期值 A 和新值 B。仅当 V 和 A 相同时,CPU 才会将 V 更新为 B,否则什么都不做。这个过程是原子的,不需要加锁。
Java 的 AtomicInteger 等类就是基于 CAS 实现的。而 AQS(AbstractQueuedSynchronizer)则是基于 CAS 和 volatile 实现的同步器框架,ReentrantLock、CountDownLatch 的底层都是它。
实战代码:模拟 CAS 实现
java
复制
import java.util.concurrent.atomic.AtomicInteger;
public class CasDemo {
// 底层基于 Unsafe 类的 native 方法实现 CAS
private static AtomicInteger atomicInt = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
// getAndIncrement 内部调用了 unsafe.compareAndSwapInt
atomicInt.getAndIncrement();
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final result: " + atomicInt.get()); // 2000
}
}
四、 线程池:拒绝“一手”创建
在生产环境中,频繁创建和销毁线程是不可接受的。线程池不仅复用了线程,还通过队列解决了任务堆积问题,更重要的是它能根据 CPU 核心数动态调整并发度,榨干机器性能。
底层原理:
ThreadPoolExecutor 的核心工作逻辑是这样的:
- 如果核心线程数未满,创建核心线程执行任务。
- 如果核心线程数已满,任务进入阻塞队列。
- 如果队列也满了,创建非核心线程(救急线程)执行任务。
- 如果非核心线程数达到最大值,执行拒绝策略。
实战代码:标准线程池配置
java
复制
import java.util.concurrent.*;
public class ThreadPoolExecutorDemo {
public static void main(String[] args) {
// 手动创建线程池,规避 OOM 风险(不推荐使用 Executors 的快捷方法)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize: 核心线程数
4, // maximumPoolSize: 最大线程数
60L, // keepAliveTime: 非核心线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 工作队列,设置容量防止无限膨胀
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者运行
);
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("Task " + taskId + " is running by " + Thread.currentThread().getName());
try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); }
});
}
executor.shutdown();
}
}
总结
高并发编程不仅仅是写出“能跑”的代码,更是写出对 CPU 友好、对内存敏感的“优雅”代码。从 volatile 的可见性,到 synchronized 的对象头锁升级,再到 AQS 的无锁CAS 竞争,每一个知识点背后都是底层系统原理的映射。只有掌握了这些底层逻辑,我们在面对百万级并发流量时,才能做到胸有成竹,运筹帷幄。