如何通过一个多线程案例展示synchronized关键字的同步作用

wen java案例 49

本文目录导读:

如何通过一个多线程案例展示synchronized关键字的同步作用

  1. 目录导读
  2. 引言:为什么多线程需要同步控制?
  3. 案例背景:一个危险的银行账户取款程序
  4. 无锁版本演示:线程安全陷阱与数据错乱
  5. synchronized介入:三种经典加锁方式对比
  6. 核心原理:对象头、Monitor与锁升级过程
  7. 常见误区问答
  8. 何时该用synchronized

用真实案例讲透synchronized的同步锁机制

目录导读

  1. 引言:为什么多线程需要同步控制?
  2. 案例背景:一个危险的银行账户取款程序
  3. 无锁版本演示:线程安全陷阱与数据错乱
  4. synchronized介入:三种经典加锁方式对比
  5. 核心原理:对象头、Monitor与锁升级过程
  6. 常见误区问答:synchronized与性能、死锁
  7. 何时该用synchronized

引言:为什么多线程需要同步控制?

在多线程编程中,当多个线程同时访问共享资源(如变量、对象、文件)时,会产生“竞争条件”(Race Condition),例如两个线程同时给同一个银行账户取款,如果没有同步机制,最终余额可能比预期少扣或多扣。synchronized正是Java提供的最基础的线程同步关键字,它能保证同一时刻只有一个线程执行被修饰的代码块或方法。


案例背景:一个危险的银行账户取款程序

我们设计一个简单的银行账户类 BankAccount,包含余额 balance 和取款方法 withdraw(int amount),在主程序中创建两个线程,各执行100次取款1元的操作,期望最终余额减少200元,但如果不加同步,结果会怎样?


无锁版本演示:线程安全陷阱与数据错乱

核心代码(无synchronized):

class BankAccount {
    private int balance = 200;
    public void withdraw(int amount) {
        // 非原子操作:读-减-写
        if (balance >= amount) {
            // 模拟耗时操作
            Thread.sleep(1);
            balance -= amount;
        }
    }
}

运行结果:多次执行后,余额可能是198、199、200等错误值,从未正确减少到0。
原因balance >= amount 判断和 balance -= amount 之间被线程切换打断,导致多个线程都通过了检查,却只扣除了一次金额。

此案例清晰展示了临界区问题:多个线程并发执行“读取-修改-写入”操作时,必须互斥访问。


synchronized介入:三种经典加锁方式对比

同步代码块(锁当前对象)

public void withdraw(int amount) {
    synchronized(this) {
        if (balance >= amount) {
            Thread.sleep(1);
            balance -= amount;
        }
    }
}
  • 效果:只有持有this对象锁的线程才能进入代码块,其他线程阻塞等待。
  • 适用:只保护部分代码,减少锁范围。

同步方法

public synchronized void withdraw(int amount) {
    if (balance >= amount) {
        Thread.sleep(1);
        balance -= amount;
    }
}
  • 等价synchronized(this),锁住整个方法。
  • 注意:如果继承类重写此方法,需考虑锁对象改变。

锁静态方法(类锁)

public static synchronized void staticWithdraw() { ... }
  • 锁对象BankAccount.class,用于保护静态变量。

验证结果:添加任意一种synchronized后,无论执行多少次,余额始终准确递减到0,证明同步生效。


核心原理:对象头、Monitor与锁升级过程

每个Java对象都有一个对象头(Mark Word),其中存储锁状态信息:

  • 无锁:对象头标记为001。
  • 偏向锁:单线程反复获取同一锁时,将线程ID写入对象头,避免CAS开销。
  • 轻量级锁:多线程轻度竞争时,通过自旋(Spin)等待,不挂起线程。
  • 重量级锁:竞争激烈时,升级为Monitor(内部包含等待队列),线程进入阻塞。

synchronized自动完成锁升级,从偏向锁→轻量级锁→重量级锁,随着竞争加剧而加重代价,这也是为什么synchronized比早期版本性能好很多的原因。


常见误区问答

Q1:synchronized能保证可见性吗?
A:能,进入synchronized块时会刷新工作内存,退出时会强制将修改写入主内存,因此具备原子性可见性

Q2:synchronized会引发死锁吗?
A:会,例如线程A持有锁1,等待锁2;线程B持有锁2,等待锁1,解决方案:按固定顺序获取锁,或使用tryLock等工具类。

Q3:synchronized和Lock接口有什么区别?
A:synchronized是关键字,使用简单,自动释放锁;Lock是接口,提供更灵活的控制(如可中断、超时、公平性),性能上两者在JDK 6以后差距很小。

Q4:为什么我的synchronized代码块没生效?
A:最常见原因是锁的不是同一个对象,例如用字符串常量"lock"作为锁,多个new String("lock")是不同对象,应使用同一个静态对象或Class对象。


何时该用synchronized

  • 简单互斥场景:只需保护一个变量或一段代码,且不涉及超时、中断等高级需求。
  • 代码可读性优先:相比Lock代码更简洁,不易出错。
  • 与JVM优化配合:现代JVM对synchronized做了大量优化(锁消除、锁粗化、偏向锁),性能在大多数场景下足够。

本文通过银行取款案例,从现象到原理,从错误到正确,完整展示了synchronized如何解决多线程并发问题。 理解其本质,是掌握Java并发编程的关键一步。

抱歉,评论功能暂时关闭!