理解 Java 中同步机制的作用与必要性
掌握三种主要的同步实现方式:同步方法、同步代码块和静态同步
区分进程同步与线程同步的应用场景
了解 volatile 关键字的用途及其与 synchronized 的区别
在多线程环境中,多个线程可能同时访问共享资源(如变量、对象或方法)。若不加以控制,可能导致以下问题:
数据不一致:多个线程同时修改共享数据,导致结果不可预测
竞态条件:程序行为依赖于线程执行顺序,造成逻辑错误
线程不安全:共享资源被并发修改,破坏程序状态
数据完整性受损:最终数据与预期不符
同步机制通过限制同一时间只能有一个线程访问关键代码段,有效解决上述问题。
使用 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。
当只需保护部分代码时,可使用同步代码块,减少锁的粒度,提升性能。
class Counter {
private int c = 0;
public void inc() {
synchronized (this) { // 仅对关键操作加锁
c++;
}
}
public int get() {
return c;
}
}优势:相比整个方法同步,同步块只锁定必要部分,降低锁竞争开销。
用于同步静态方法。此时锁的对象是类的 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;
}
}多个线程同时存取款时,同步方法防止余额计算错误。
模拟不使用线程同步在多线程环境下可能出现的问题
步骤一:去掉 synchronized 的 BankAccount 类
/**
* 非线程安全的银行账户类(用于演示并发问题)
*/
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 保证变量的可见性,但不保证原子性。
特点:
仅适用于变量
所有线程读取的都是主内存中的最新值(禁止缓存)
不提供互斥,不能替代 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 提供轻量级可见性保障,但不能替代同步用于复合操作
合理选择同步策略可兼顾线程安全与程序性能
为什么在 Counter 类中,即使 get() 方法只是读取值,也需要声明为 synchronized?
如果将 volatile boolean running 改为普通 boolean,在某些 JVM 实现下程序可能出现什么问题?
在票务系统示例中,若去掉 synchronized,两个线程同时尝试预订 6 张票(共 10 张),可能出现什么异常结果?