源本科技 | 码上会

Java 中的同步机制

2026/01/29
60
0

学习目标

  • 理解 Java 中同步机制的作用与必要性

  • 掌握三种主要的同步实现方式:同步方法、同步代码块和静态同步

  • 区分进程同步与线程同步的应用场景

  • 了解 volatile 关键字的用途及其与 synchronized 的区别


为什么需要同步

在多线程环境中,多个线程可能同时访问共享资源(如变量、对象或方法)。若不加以控制,可能导致以下问题:

  • 数据不一致:多个线程同时修改共享数据,导致结果不可预测

  • 竞态条件:程序行为依赖于线程执行顺序,造成逻辑错误

  • 线程不安全:共享资源被并发修改,破坏程序状态

  • 数据完整性受损:最终数据与预期不符

同步机制通过限制同一时间只能有一个线程访问关键代码段,有效解决上述问题。


三种实现方式

1. 同步方法

使用 synchronized 修饰实例方法,确保同一对象的该方法在同一时刻只能被一个线程执行。

class Counter {
    private int c = 0;

    // 同步方法:确保原子性
    public synchronized void inc() {
        c++;
    }

    public synchronized int get() {
        return c;
    }
}

public class MainApp {
    public static void main(String[] args) throws InterruptedException {
        Counter cnt = new Counter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) cnt.inc();
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) cnt.inc();
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Counter: " + cnt.get()); // 输出:2000
    }
}

说明:由于 inc()get() 是同步方法,两个线程不会同时修改 c,最终结果正确为 2000。


2. 同步代码块

当只需保护部分代码时,可使用同步代码块,减少锁的粒度,提升性能。

class Counter {
    private int c = 0;

    public void inc() {
        synchronized (this) { // 仅对关键操作加锁
            c++;
        }
    }

    public int get() {
        return c;
    }
}

优势:相比整个方法同步,同步块只锁定必要部分,降低锁竞争开销。


3. 静态同步

用于同步静态方法。此时锁的对象是类的 Class 对象(如 Table.class),而非实例。

class Table {
    public static synchronized void printTable(int n) {
        for (int i = 1; i <= 3; i++) {
            System.out.println(n * i);
            try {
                Thread.sleep(10); // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

class PrinterThread1 extends Thread {
    public void run() {
        Table.printTable(1);
    }
}

class PrinterThread2 extends Thread {
    public void run() {
        Table.printTable(10);
    }
}

public class MainApp {
    public static void main(String[] args) {
        new PrinterThread1().start();
        new PrinterThread2().start();
    }
}

输出示例

1
2
3
10
20
30

关键点:即使没有共享对象实例,静态同步仍能保证同一时间只有一个线程执行该静态方法。


同步的类型

进程同步

协调多个线程对共享资源(如银行账户)的操作,确保数据一致性。

/**
 * 银行账户类,用于模拟银行存取款操作。
 * 初始余额为 1000 元人民币。
 */
public class BankAccount {
    private int balance = 1000; // 单位:元

    /**
     * 存款操作(线程安全)
     * @param amount 存入金额(单位:元)
     */
    public synchronized void deposit(int amount) {
        balance += amount;
        System.out.println("已存入 " + amount + " 元,当前余额:" + balance + " 元");
    }

    /**
     * 取款操作(线程安全)
     * @param amount 取出金额(单位:元)
     */
    public synchronized void withdraw(int amount) {
        if (balance >= amount) {
            balance -= amount;
            System.out.println("已取出 " + amount + " 元,当前余额:" + balance + " 元");
        } else {
            System.out.println("余额不足,无法取出 " + amount + " 元。当前余额:" + balance + " 元");
        }
    }

    /**
     * 获取当前账户余额
     * @return 当前余额(单位:元)
     */
    public int getBalance() {
        return balance;
    }
}

多个线程同时存取款时,同步方法防止余额计算错误。

模拟不使用线程同步在多线程环境下可能出现的问题

  • 步骤一:去掉 synchronizedBankAccount

/**
 * 非线程安全的银行账户类(用于演示并发问题)
 */
public class UnsafeBankAccount {
    private int balance = 1000; // 初始余额 1000 元

    public void deposit(int amount) {
        balance += amount;
        System.out.println("已存入 " + amount + " 元,当前余额:" + balance + " 元");
    }

    public void withdraw(int amount) {
        if (balance >= amount) {
            // 模拟处理延迟(更容易暴露竞态条件)
            try {
                // 加入 Thread.sleep(10) 是为了放大竞态条件,让多个线程更可能在“检查余额”和“扣款”之间发生交错
                Thread.sleep(10); // 关键!制造时间窗口
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
            balance -= amount;
            System.out.println("已取出 " + amount + " 元,当前余额:" + balance + " 元");
        } else {
            System.out.println("余额不足,无法取出 " + amount + " 元。当前余额:" + balance + " 元");
        }
    }

    public int getBalance() {
        return balance;
    }
}
  • 步骤二:编写多线程测试代码

public class BankConcurrencyTest {
    public static void main(String[] args) throws InterruptedException {
        UnsafeBankAccount account = new UnsafeBankAccount();

        // 创建两个线程,同时尝试取款 600 元(总需求 1200 > 1000,应至少有一个失败)
        Thread t1 = new Thread(() -> account.withdraw(600), "客户 A");
        Thread t2 = new Thread(() -> account.withdraw(600), "客户 B");

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("最终余额: " + account.getBalance() + " 元");
    }
}
  • 可能出现的异常结果

正常情况下,两个线程各取 600 元(共需 1200),但账户只有 1000 元,最多只能成功一次

但在非同步情况下,可能两次都成功,导致余额变成 -200 元!例如输出:

已取出 600 元,当前余额:400 元
已取出 600 元,当前余额:-200 元
最终余额: -200 元

或者:

余额不足,无法取出 600 元。当前余额:1000 元
已取出 600 元,当前余额:400 元
最终余额: 400 元

第一种情况就是典型的并发安全问题
两个线程都通过了 if (balance >= amount) 检查(此时 balance=1000),然后先后执行扣款,导致超支

  • 如果你把 withdraw 方法加上 synchronized,上述超支问题将永远不会发生,因为检查和扣款是原子操作


线程同步

控制线程执行顺序,包括:

  • 互斥:同一时间仅一个线程访问临界区

  • 协作:线程间通信(如 wait() / notify()

示例:票务系统

/**
 * 票务预订系统,模拟门票/车票预订。
 * 初始可售票数为 10 张。
 */
public class TicketBooking {
    private int availableTickets = 10; // 剩余可订票数

    /**
     * 预订票务(线程安全)
     * @param tickets 要预订的票数
     */
    public synchronized void bookTicket(int tickets) {
        if (availableTickets >= tickets) {
            availableTickets -= tickets;
            System.out.println("成功预订 " + tickets + " 张票,剩余票数:" + availableTickets + " 张");
        } else {
            System.out.println("票数不足,无法预订 " + tickets + " 张票。当前剩余:" + availableTickets + " 张");
        }
    }

    /**
     * 获取当前剩余可订票数
     * @return 剩余票数
     */
    public int getAvailableTickets() {
        return availableTickets;
    }
}

同步确保不会超卖票,即使多个用户同时下单。


volatile 关键字

volatile 保证变量的可见性,但不保证原子性

特点:

  • 仅适用于变量

  • 所有线程读取的都是主内存中的最新值(禁止缓存)

  • 不提供互斥,不能替代 synchronized 用于复合操作(如 count++

示例:控制线程运行状态

class Counter {
    private volatile boolean running = true;

    public void stop() {
        running = false; // 修改对其他线程立即可见
    }

    public void start() {
        new Thread(() -> {
            while (running) {
                System.out.println("Running...");
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
            System.out.println("Stopped.");
        }).start();
    }
}

public class MainApp {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        counter.start();
        Thread.sleep(600);
        counter.stop(); // 安全停止线程
    }
}

输出

Running...
Running...
Running...
Stopped.

running 不是 volatile,子线程可能永远看不到主线程对其的修改,导致无法停止。


volatile 与 synchronized

特性

synchronized

volatile

作用范围

方法或代码块

仅变量

功能

保证互斥 + 可见性

仅保证可见性

性能

较低(需获取 / 释放锁)

较高(无锁开销)

原子性

支持复合操作的原子性

不支持(如 i++ 仍非原子)

建议

  • 简单状态标志(如开关)用 volatile

  • 涉及多步操作或需互斥时,必须用 synchronized


重点总结

  • 同步是保障多线程环境下数据一致性的核心机制

  • synchronized 可用于方法、代码块或静态方法,分别锁定实例或类对象

  • 同步分为进程同步(保护共享资源)和线程同步(控制执行顺序)

  • volatile 提供轻量级可见性保障,但不能替代同步用于复合操作

  • 合理选择同步策略可兼顾线程安全与程序性能


思考题

  1. 为什么在 Counter 类中,即使 get() 方法只是读取值,也需要声明为 synchronized

  2. 如果将 volatile boolean running 改为普通 boolean,在某些 JVM 实现下程序可能出现什么问题?

  3. 在票务系统示例中,若去掉 synchronized,两个线程同时尝试预订 6 张票(共 10 张),可能出现什么异常结果?