慕课微课-掌握Java并发编程的“基石”,入门并发编程

6 阅读4分钟

在系统学习了多门 Java 并发相关的慕课与微课后,我深刻体会到:并发编程不是“会用线程”就足够,而是要理解“为什么这样设计” 。本文将结合课程精华与个人实践,通过几个典型场景和关键代码,分享我对 Java 并发三大基石——可见性、原子性、有序性——的理解与应用。


一、可见性问题:volatile 的正确打开方式

场景还原

早期我曾写过如下代码,期望通过一个 flag 控制线程退出:

java
编辑
public class VolatileDemo {
    private static boolean flag = false; // ❌ 没有 volatile

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (!flag) {
                // 空循环等待
            }
            System.out.println("线程退出");
        }).start();

        Thread.sleep(1000);
        flag = true; // 主线程修改 flag
    }
}

结果:程序永远无法退出!因为子线程读取的是 CPU 缓存中的 flag 副本,看不到主线程的修改。

正确做法:使用 volatile

java
编辑
private static volatile boolean flag = false; // ✅ 保证可见性

感悟volatile 不是万能锁,但它能确保变量的修改对所有线程立即可见,并禁止指令重排序。适用于“一个线程写,多个线程读”的状态标志场景。


二、原子性问题:synchronized vs AtomicInteger

场景:计数器并发累加

java
编辑
public class Counter {
    private int count = 0;

    public void increment() {
        count++; // ❌ 非原子操作(读-改-写)
    }

    public int getCount() {
        return count;
    }
}

多线程调用 increment() 后,count 值往往小于预期。

方案1:synchronized 加锁

java
编辑
public synchronized void increment() {
    count++;
}

✅ 安全,但性能开销大(重量级锁)。

方案2:AtomicInteger(无锁 CAS)

java
编辑
private AtomicInteger count = new AtomicInteger(0);

public void increment() {
    count.incrementAndGet(); // ✅ 基于 CAS 的原子操作
}

感悟

  • synchronized 保证原子性 + 可见性 + 有序性,但可能阻塞线程;
  • AtomicInteger 利用 CPU 的 CAS(Compare-And-Swap)指令实现无锁并发,性能更高,但仅适用于单一变量操作。

三、线程协作:CountDownLatch 实现任务同步

场景:主线程等待多个子任务完成

传统 join() 写法繁琐,而 CountDownLatch 更优雅:

java
编辑
public class TaskCoordinator {
    public static void main(String[] args) throws InterruptedException {
        int taskCount = 3;
        CountDownLatch latch = new CountDownLatch(taskCount);

        for (int i = 0; i < taskCount; i++) {
            new Thread(() -> {
                try {
                    // 模拟任务
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName() + " 完成");
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    latch.countDown(); // 任务完成,计数减1
                }
            }).start();
        }

        latch.await(); // 主线程阻塞,直到计数归零
        System.out.println("所有任务完成,继续后续流程");
    }
}

感悟:JUC 包中的 CountDownLatchCyclicBarrierSemaphore 等工具类,极大简化了线程协作逻辑,避免手写 wait/notify 的复杂与易错


四、线程池:拒绝“new Thread()”的诱惑

错误示范

java
编辑
// ❌ 每次请求都新建线程,资源耗尽风险极高
new Thread(() -> { /* 业务逻辑 */ }).start();

正确姿势:使用 ThreadPoolExecutor

java
编辑
ExecutorService executor = new ThreadPoolExecutor(
    2,                     // 核心线程数
    4,                     // 最大线程数
    60L, TimeUnit.SECONDS, // 空闲线程存活时间
    new LinkedBlockingQueue<>(10), // 有界队列
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用者线程执行
);

// 提交任务
executor.submit(() -> {
    System.out.println("任务执行中...");
});

感悟

  • 核心线程常驻,非核心线程可回收
  • 有界队列防止 OOM
  • 拒绝策略保障系统稳定性(如降级、报警、由调用方处理)。

五、死锁预防:tryLock + 超时机制

经典死锁场景(A 持有锁1等锁2,B 持有锁2等锁1)

安全方案:使用 ReentrantLock.tryLock(timeout)

java
编辑
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();

boolean acquired1 = false, acquired2 = false;
try {
    acquired1 = lock1.tryLock(1, TimeUnit.SECONDS);
    acquired2 = lock2.tryLock(1, TimeUnit.SECONDS);
    
    if (acquired1 && acquired2) {
        // 执行业务
    } else {
        // 获取锁失败,避免死锁
        System.out.println("获取锁超时,放弃操作");
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
} finally {
    if (acquired2) lock2.unlock();
    if (acquired1) lock1.unlock();
}

感悟死锁可防不可救。通过固定加锁顺序、设置超时、减少锁持有时间,可大幅降低风险。


结语:从“会用”到“懂理”

通过慕课微课的学习与上述实战,我逐渐明白:

  • 并发问题的本质是共享状态的协调
  • JDK 提供的工具(JUC)是经过千锤百炼的最佳实践
  • 真正的高手,不是不用锁,而是知道何时用、用哪种、如何用得安全

如今,面对高并发场景,我不再恐惧,而是能冷静分析:这是可见性问题?原子性问题?还是线程协作问题?然后选择最合适的工具解决。

吃透并发基石,入门其实很简单——只要你愿意从原理出发,动手验证,持续反思