图灵课堂-Java高级开发工程师

2 阅读5分钟

很多初学者认为 Java 高并发就是背背 API,用用 synchronized 或者 ReentrantLock。但在图灵课堂的学习让我明白了一个道理:不懂底层原理,并发代码写出来就是定时炸弹。  真正的实战高手,关注的是 CPU 缓存一致性协议、对象内存布局以及 JVM 的锁优化机制。

一、 并发的基石:volatile 与可见性

在多线程环境下,为什么一个线程修改了布尔变量,另一个线程却“看不见”?这就要从 JMM(Java 内存模型)  说起。

Java 内存模型规定了所有变量都存储在主内存,每条线程还有自己的工作内存。线程对变量的操作必须在工作内存中进行,然后再回写到主内存。

底层原理:
volatile 关键字之所以能保证可见性,是因为它在汇编层面插入了一个 Lock 前缀指令。这个指令做了两件事:

  1. 将当前处理器缓存行的数据立即回写到系统内存。
  2. 这个回写操作会使在其他 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 实现的同步器框架,ReentrantLockCountDownLatch 的底层都是它。

实战代码:模拟 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 的核心工作逻辑是这样的:

  1. 如果核心线程数未满,创建核心线程执行任务。
  2. 如果核心线程数已满,任务进入阻塞队列。
  3. 如果队列也满了,创建非核心线程(救急线程)执行任务。
  4. 如果非核心线程数达到最大值,执行拒绝策略。

实战代码:标准线程池配置

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 竞争,每一个知识点背后都是底层系统原理的映射。只有掌握了这些底层逻辑,我们在面对百万级并发流量时,才能做到胸有成竹,运筹帷幄。