深入理解Java并发编程:从办公室场景说起

深入理解Java并发编程:从办公室场景说起

引言

作为Java程序员,你一定听说过"可见性"、"原子性"、"有序性"这些概念。但这些抽象的概念往往让人难以理解。今天,让我们通过一个简单的办公室场景,来重新认识这些概念。在开始之前,我们先了解下为什么需要关注这些问题:

  • 现代CPU架构中的多级缓存
  • 编译器优化带来的指令重排
  • 多线程并发访问共享资源的竞争

一、Java内存模型:一个现代化办公室

想象一下,我们的办公室就是一个小型的公司。在这个比喻中:

  • 公告板(主内存):放在大厅中央,存储公司的所有重要信息
  • 员工的记事本(工作内存):每个人随身携带,记录需要处理的信息
  • 各位员工(线程):来来往往处理各种事务,需要相互协调

1.1 工作内存 vs 线程栈:不同的抽象层面

想象一下小李正在处理工资发放的场景。他的办公桌上(线程栈)有一个计算器和记事本(局部变量),同时他还随身携带着一个记录公司账户余额的便携本(工作内存)。他需要查看公告板(主内存)上的公司账户信息,记在自己的便携本上,然后结合桌上的计算器进行计算。

让我们通过这个工资发放的完整示例来理解这两个概念:

public class Employee {
    // 公司账户余额-主内存
    private static int companyMoney = 100000;
    // 部门预算-主内存
    private static int departmentBudget = 50000;
    
    public void processPayment() {
        // 局部变量-线程栈
        int salary = 5000;
        int bonus = 2000;
        // 共享变量-涉及工作内存
        companyMoney -= (salary + bonus);
        departmentBudget -= salary;
        
        // 记录日志-线程栈
        String logMessage = "Processed payment: " + (salary + bonus);
    }
}

线程栈的特点:

  1. 线程私有,不存在并发问题
  2. 生命周期跟随方法调用
  3. 存储基本数据类型和对象引用
  4. 自动管理内存,方法结束自动释放

工作内存的特点:

  1. 是主内存和线程的中间层
  2. 存储共享变量的副本
  3. 需要和主内存保持同步
  4. 通过内存屏障保证可见性

1.2 Java内存模型的交互规则

内存交互过程的完整示例:

class DocumentSystem {
    // 主内存变量
    private volatile int documentVersion = 1;
    private String documentContent = "";
    private boolean isModified = false;
    
    public void updateDocument(String newContent) {
        // 1. read & load:从主内存读取到工作内存
        int currentVersion = documentVersion;
        String currentContent = documentContent;
        
        // 2. use & assign:在工作内存中操作
        currentContent = newContent;
        currentVersion++;
        isModified = true;
        
        // 3. store & write:写回主内存
        documentContent = currentContent;
        documentVersion = currentVersion;
    }
}

八种原子操作的详细说明:

  1. read(读取):

    • 从主内存读取变量的值
    • 类似查看公告板上的信息
  2. load(载入):

    • 将读取的值写入工作内存
    • 类似将公告板内容抄写到记事本
  3. use(使用):

    • 从工作内存读取变量值
    • 类似查看记事本中的信息
  4. assign(赋值):

    • 在工作内存中写入新值
    • 类似在记事本上修改信息
  5. store(存储):

    • 将工作内存的值传输到主内存
    • 类似准备更新公告板
  6. write(写入):

    • 将值写入主内存
    • 类似实际更新公告板
  7. lock(锁定):

    • 将主内存变量加锁
    • 类似预订会议室
  8. unlock(解锁):

    • 将主内存变量解锁
    • 类似释放会议室

二、并发编程的三大困扰

2.1 可见性:错过的会议通知

生动的办公室场景:

想象这样一个场景:行政小张刚刚在公告板上贴了一个紧急会议通知,包括会议室信息和时间。但小李当时正在自己的工位上忙着处理文件,只是瞟了一眼自己随身携带的记事本(工作内存),发现没有会议通知就继续工作了。即使小张已经更新了公告板,但小李的记事本里还是旧的信息,导致他错过了这个重要会议。

这个场景完美地诠释了可见性问题:一个线程对共享变量的修改对其他线程不可见。让我们看看代码层面是如何体现的:

问题场景的深入分析:

public class MeetingNotice {
    private boolean meetingScheduled = false;
    private String meetingRoom;
    private Date meetingTime;
    
    // 行政小张:发布会议通知
    public void scheduleMeeting() {
        prepareMeeting();
        meetingRoom = "Room 301";             // ①
        meetingTime = new Date();             // ②
        meetingScheduled = true;              // ③
    }
    
    // 小李:等待并查看会议通知
    public void waitForMeeting() {
        while (!meetingScheduled) {           // ④
            // 不断查看记事本
        }
        // 可能看到不完整的会议信息
        System.out.println("Meeting in " + meetingRoom + " at " + meetingTime);
    }
}

问题分析:

  1. 指令重排可能导致③提前执行
  2. 工作内存的值没有及时刷新到主内存
  3. 其他线程看到了无效的缓存值

多种解决方案比较:

  1. volatile方案:
private volatile boolean meetingScheduled = false;
private volatile String meetingRoom;
private volatile Date meetingTime;
  1. synchronized方案:
public synchronized void scheduleMeeting() {
    prepareMeeting();
    meetingRoom = "Room 301";
    meetingTime = new Date();
    meetingScheduled = true;
}
  1. Lock方案:
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public void scheduleMeeting() {
    lock.writeLock().lock();
    try {
        prepareMeeting();
        meetingRoom = "Room 301";
        meetingTime = new Date();
        meetingScheduled = true;
    } finally {
        lock.writeLock().unlock();
    }
}

2.2 原子性:混乱的会议室预订

生动的办公室场景:

想象一下这个尴尬的场景:小李和小王同时看到会议室是空的,都想预订下午3点的时段。小李正在登记预订信息时,被同事叫走处理了一个紧急问题。这时小王看到登记本还是空的,就直接把自己的名字写了上去。等小李回来继续登记时,发现会议室已经被小王预订了!

这就是典型的原子性问题:一个操作被打断,导致最终结果出现混乱。在会议室预订系统中,"查看状态-登记预订"这个完整操作必须是原子的,不能被打断。让我们看看代码中是如何体现的:

复杂场景示例:

public class MeetingRoom {
    private static class RoomStatus {
        boolean isBooked;
        String bookedBy;
        Date bookingTime;
    }
    
    private RoomStatus status = new RoomStatus();
    
    public void book(String employee) {
        // 以下操作需要保证原子性
        if (!status.isBooked) {                    // 检查状态
            validateEmployee(employee);             // 验证身份
            status.isBooked = true;                // 更新预订状态
            status.bookedBy = employee;            // 记录预订人
            status.bookingTime = new Date();       // 记录时间
        }
    }
}

各种解决方案的优缺点:

  1. synchronized方案:

    • 优点:实现简单,保证可见性
    • 缺点:性能开销较大
  2. CAS方案:

public class MeetingRoom {
    private AtomicReference<RoomStatus> status = 
        new AtomicReference<>(new RoomStatus());
    
    public void book(String employee) {
        RoomStatus current, newStatus;
        do {
            current = status.get();
            if (current.isBooked) {
                return;
            }
            newStatus = new RoomStatus();
            newStatus.isBooked = true;
            newStatus.bookedBy = employee;
            newStatus.bookingTime = new Date();
        } while (!status.compareAndSet(current, newStatus));
    }
}
  1. 显式锁方案:
private final Lock roomLock = new ReentrantLock();
public void book(String employee) {
    roomLock.lock();
    try {
        if (!status.isBooked) {
            // 预订逻辑
        }
    } finally {
        roomLock.unlock();
    }
}

2.3 有序性:混乱的入职流程

生动的办公室场景:

设想新员工小张入职的场景。人力资源部需要为他准备工位、配置电脑账号并安排入职培训。正常流程是:先准备好工位和电脑,然后创建账号,最后进行培训。但由于多个部门同时处理,可能出现这样的情况:IT部门先创建了账号,但工位还没准备好;或者培训已经开始了,但电脑账号还没建好。这种流程顺序的混乱就会导致新员工入职体验很差。

这个场景完美地映射了程序中的有序性问题:看似有严格顺序的操作,可能被重排导致出现意料之外的情况。让我们看看代码层面的表现:

问题场景的完整示例:

public class Onboarding {
    private boolean workstationReady;
    private boolean accountCreated;
    private boolean trainingCompleted;
    
    public void prepare() {
        prepareWorkstation();         // ①
        workstationReady = true;      // ②
        setupAccount();               // ③
        accountCreated = true;        // ④
        conductTraining();            // ⑤
        trainingCompleted = true;     // ⑥
    }
    
    public void checkStatus() {
        // 由于指令重排,可能出现意外的状态组合
        if (accountCreated && !workstationReady) {
            // 不合理的状态
        }
    }
}

重排序类型:

  1. 编译器重排序
  2. 处理器重排序
  3. 内存系统重排序

解决方案:

  1. volatile保证单个变量的读写顺序
  2. synchronized保证代码块的顺序性
  3. happens-before规则建立操作间的顺序关系

三、深入理解happens-before

让我们继续用办公室场景来理解happens-before:想象公司里有一个严格的工作流程规定,比如交接班时,上一班必须完成交接记录,下一班才能开始工作。这就像程序中的happens-before关系,它确保了操作的先后顺序。

happens-before关系的完整规则集:

  1. 程序顺序规则
void process() {
    int a = 1;              // ①
    int b = 2;              // ②
    System.out.println(a);  // ③
    System.out.println(b);  // ④
}
// ①happens-before②, ②happens-before③, ③happens-before④
  1. volatile变量规则
class SharedData {
    private volatile boolean flag;
    private int[] data = new int[10];
    
    public void write() {
        for (int i = 0; i < data.length; i++) {
            data[i] = i;                    // ①
        }
        flag = true;                        // ②
    }
    
    public void read() {
        if (flag) {                         // ③
            int sum = 0;
            for (int i = 0; i < data.length; i++) {
                sum += data[i];             // ④
            }
        }
    }
}
// ①happens-before②, ②happens-before③, 所以①happens-before④
  1. 传递性规则
class TransitiveExample {
    private volatile int a = 0;
    private volatile int b = 0;
    
    void step1() { a = 1; }         // ①
    void step2() { b = a; }         // ②
    void step3() { print(b); }      // ③
}
// ①happens-before②, ②happens-before③
// 所以①happens-before③

四、实践建议

1. 优先使用高层同步工具

想象一下,与其让每个员工自己制定规则来协调工作,不如使用公司已有的规章制度和流程。同样地,在并发编程中,我们应该优先使用JDK提供的成熟工具类,而不是自己实现底层的同步机制。

推荐使用的并发工具类:

  1. AtomicXXX类:
public class Counter {
    private AtomicInteger count = new AtomicInteger(0);
    private AtomicReference<State> state = 
        new AtomicReference<>(State.IDLE);
    
    public void increment() {
        count.incrementAndGet();
        // 相比synchronized更高效
    }
}
  1. 并发集合:
// 线程安全的集合类
private ConcurrentHashMap<String, User> userCache = 
    new ConcurrentHashMap<>();
private CopyOnWriteArrayList<Event> eventLog = 
    new CopyOnWriteArrayList<>();

2. 正确使用synchronized

就像在办公室中,我们不会为了一个简单的复印任务就把整个办公室锁起来,而是只锁复印室。同样,在使用synchronized时,我们也需要选择合适的同步范围,既要确保安全,又要避免过度同步影响性能。

最佳实践:

  1. 选择合适的同步范围:
public class BankAccount {
    private double balance;
    private final Lock balanceLock = new ReentrantLock();
    
    public void transfer(BankAccount to, double amount) {
        // 使用细粒度的锁
        balanceLock.lock();
        try {
            if (balance >= amount) {
                balance -= amount;
                to.deposit(amount);
            }
        } finally {
            balanceLock.unlock();
        }
    }
}
  1. 避免嵌套锁:
// 不推荐
synchronized void outer() {
    synchronized void inner() {
        // 可能导致死锁
    }
}

3. volatile的适用场景

想象公司里有一个"紧急状态"的标志:当发生紧急情况时,只需要将标志设置为true,所有员工看到标志就立即停止当前工作开始处理紧急情况。这种简单的状态标志非常适合使用volatile来实现。

适用场景:

  1. 状态标志:
public class Service {
    private volatile boolean running = true;
    
    public void run() {
        while (running) {
            // 服务逻辑
        }
    }
    
    public void stop() {
        running = false;
    }
}
  1. 双重检查锁定:
public class Singleton {
    private volatile static Singleton instance;
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

总结

通过办公室的日常场景,我们深入理解了Java并发编程中的核心概念:

  1. 可见性:及时同步信息
  2. 原子性:操作的完整性
  3. 有序性:正确的执行顺序

实践建议:

  1. 理解内存模型的基本原理
  2. 合理使用synchronized和volatile
  3. 优先使用高层并发工具
  4. 保持代码的简单性和可维护性

参考资料

  1. 《Java并发编程实战》- Brian Goetz
  2. 《深入理解Java虚拟机》- 周志明
  3. JSR-133:
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,001评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,210评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,874评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,001评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,022评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,005评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,929评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,742评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,193评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,427评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,583评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,305评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,911评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,564评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,731评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,581评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,478评论 2 352

推荐阅读更多精彩内容