一、为什么需要了解Java内存模型
想象你在厨房和室友一起做饭。如果两个人同时往锅里加盐,最后菜可能咸得没法吃。Java程序也一样——当多个线程同时操作同一个数据时,如果没有明确的规则,结果就会混乱不堪。Java内存模型(JMM)就是解决这个问题的"厨房操作手册",它规定了线程如何安全地"共享食材"(内存数据)。
二、内存模型的底层原理
可以把JMM想象成一个快递仓库:
- 主内存相当于中央仓库,存放所有原始数据
- 工作内存是每个线程的私人储物柜,存放需要处理的临时数据
关键规则:
- 线程不能直接修改主内存,必须先把数据"快递"到自己的工作内存
- 修改完成后,需要特殊指令才能把数据"寄回"主内存
// 技术栈:Java 11
public class MemoryVisibilityDemo {
private static /*volatile*/ boolean ready = false; // 试试去掉volatile会发生什么
private static int number;
public static void main(String[] args) {
new Thread(() -> {
while(!ready); // 循环等待
System.out.println(number); // 可能看到0或42
}).start();
number = 42;
ready = true; // 没有volatile时,这个修改可能对其他线程不可见
}
}
三、并发编程的三大难题
1. 可见性问题
就像两个快递员同时更新库存表,但彼此看不到对方的修改。解决方案:
// 正确姿势:使用volatile
private volatile boolean flag = true;
// 或者使用synchronized
private synchronized void updateFlag() {
flag = !flag;
}
2. 原子性问题
类似于银行转账时ATM突然故障,导致钱扣了但对方没收到。示例:
// 错误示范:非原子操作
private static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
counter++; // 这不是原子操作!
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(counter); // 通常小于20000
}
// 修复方案1:使用AtomicInteger
private static AtomicInteger atomicCounter = new AtomicInteger(0);
// 修复方案2:使用synchronized
private synchronized static void safeIncrement() {
counter++;
}
3. 有序性问题
编译器优化可能会像不靠谱的厨师,擅自调整做菜步骤。解决方案:
// 使用happens-before规则
class InstructionReorder {
int x = 0;
boolean initialized = false;
void writer() {
x = 42; // 1
initialized = true; // 2
}
void reader() {
if (initialized) { // 3
System.out.println(x); // 可能看到0
}
}
}
四、实战中的正确姿势
1. volatile的适用场景
适合做状态标志位,但要注意:
- 不能保证复合操作的原子性
- 会禁止指令重排序
// 典型用法:优雅终止线程
class Worker implements Runnable {
private volatile boolean running = true;
public void stop() { running = false; }
@Override
public void run() {
while(running) {
// 执行任务...
}
}
}
2. 锁的进阶用法
展示ReentrantLock比synchronized更灵活的特性:
import java.util.concurrent.locks.ReentrantLock;
class TicketSystem {
private final ReentrantLock lock = new ReentrantLock();
private int tickets = 100;
void sellTicket() {
lock.lock(); // 可在这里加tryLock()实现超时控制
try {
if (tickets > 0) {
System.out.println(Thread.currentThread().getName()
+ "售出第" + tickets-- + "张票");
}
} finally {
lock.unlock(); // 必须放在finally块
}
}
}
3. 线程安全容器
对比不同集合类的表现:
// 危险操作:普通HashMap
Map<String, Integer> unsafeMap = new HashMap<>();
// 安全方案1:ConcurrentHashMap
ConcurrentHashMap<String, Integer> safeMap = new ConcurrentHashMap<>();
// 安全方案2:Collections工具类
Map<String, Integer> synchronizedMap =
Collections.synchronizedMap(new HashMap<>());
五、避坑指南
- 不要过度同步:同步块太大反而会降低性能
- 注意锁的顺序:多个锁要按照固定顺序获取,避免死锁
- 警惕隐式锁:比如String.intern()的全局锁
- 优先使用并发工具类:如CountDownLatch、CyclicBarrier等
// 死锁示例:两个线程互相等待
public class DeadlockDemo {
static Object lockA = new Object();
static Object lockB = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lockA) {
try { Thread.sleep(100); }
catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println("Thread1 got both locks");
}
}
}).start();
new Thread(() -> {
synchronized (lockB) {
synchronized (lockA) {
System.out.println("Thread2 got both locks");
}
}
}).start();
}
}
六、总结与最佳实践
- 简单原则:能用volatile就不上锁
- 工具优先:优先使用java.util.concurrent包
- 避免重复造轮子:不要自己实现复杂同步机制
- 测试验证:多线程问题可能在高并发下才暴露
最后记住:理解内存模型不是为了让代码更复杂,而是为了写出更简单可靠的并发程序。就像交通规则——看似限制自由,实则是安全通行的保障。
评论