在系统学习了多门 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 包中的
CountDownLatch、CyclicBarrier、Semaphore等工具类,极大简化了线程协作逻辑,避免手写 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)是经过千锤百炼的最佳实践;
- 真正的高手,不是不用锁,而是知道何时用、用哪种、如何用得安全。
如今,面对高并发场景,我不再恐惧,而是能冷静分析:这是可见性问题?原子性问题?还是线程协作问题?然后选择最合适的工具解决。
吃透并发基石,入门其实很简单——只要你愿意从原理出发,动手验证,持续反思。