Java线程同步案例如何实现

wen java案例 26

Java线程同步案例如何实现:从基础到实战的完整指南

目录导读

  1. 为什么需要线程同步?
  2. Java同步机制核心概念
  3. 使用synchronized实现银行转账
  4. ReentrantLock实现库存扣减
  5. CountDownLatch模拟并发赛跑
  6. CyclicBarrier实现分批汇总
  7. 常见问题与最佳实践
  8. 总结与问答

为什么需要线程同步?

在多线程环境中,线程同步是保证数据一致性和程序正确性的关键机制,例如两个线程同时对一个余额为100的账户执行取款80的操作,如果不同步,结果可能变成60(正确应为20),Java提供多种同步方案来解决这类问题。

Java线程同步案例如何实现

Q: 不进行同步会有什么后果?
A: 会出现竞态条件(Race Condition),导致数据污染、程序逻辑混乱,甚至引发死锁或系统崩溃。


Java同步机制核心概念

Java线程同步主要通过以下方式实现:

  • synchronized关键字:内置锁,自动释放,适用于简单场景
  • Lock接口实现类(如ReentrantLock):更灵活,支持超时、中断锁
  • 并发工具类(如Semaphore、CountDownLatch、CyclicBarrier)
  • volatile变量:保证可见性,但不保证原子性
  • 原子类(如AtomicInteger):实现无锁同步

核心原则:保证互斥访问(同一时刻只有一个线程能执行临界区代码)和可见性(修改后的值对其他线程立即可见)。


案例一:使用synchronized实现银行转账

public class BankAccount {
    private int balance = 1000;
    // 同步方法:保证取款过程原子性
    public synchronized boolean withdraw(int amount) {
        if (balance >= amount) {
            try { Thread.sleep(10); } catch (InterruptedException e) {} // 模拟延迟
            balance -= amount;
            return true;
        }
        return false;
    }
    public int getBalance() { return balance; }
    public static void main(String[] args) throws InterruptedException {
        BankAccount account = new BankAccount();
        Thread t1 = new Thread(() -> account.withdraw(800));
        Thread t2 = new Thread(() -> account.withdraw(500));
        t1.start(); t2.start();
        t1.join(); t2.join();
        System.out.println("最终余额: " + account.getBalance()); // 正确结果应为200
    }
}

Q: synchronized修饰方法时锁住的是什么?
A: 如果方法是实例方法,锁是当前对象(this);如果是静态方法,锁是类的Class对象。


案例二:ReentrantLock实现库存扣减

当synchronized无法满足需求时(如需要尝试获取锁、中断等待),可使用ReentrantLock:

import java.util.concurrent.locks.ReentrantLock;
public class InventoryService {
    private int stock = 100;
    private final ReentrantLock lock = new ReentrantLock();
    public boolean reduceStock(int quantity) {
        lock.lock();
        try {
            if (stock >= quantity) {
                stock -= quantity;
                return true;
            }
            return false;
        } finally {
            lock.unlock(); // 必须在finally中释放
        }
    }
    public static void main(String[] args) {
        InventoryService inventory = new InventoryService();
        Runnable task = () -> {
            int myQuantity = (int)(Math.random() * 20) + 1;
            if (inventory.reduceStock(myQuantity)) {
                System.out.println(Thread.currentThread().getName() + " 扣减成功,数量=" + myQuantity);
            } else {
                System.out.println(Thread.currentThread().getName() + " 库存不足!");
            }
        };
        for (int i = 0; i < 10; i++) new Thread(task).start();
    }
}

Q: 使用Lock比synchronized好在哪?
A: Lock可以尝试获取锁(tryLock)、设置超时时间、响应中断,还支持公平锁和读写锁分离,性能在高并发下更优。


案例三:CountDownLatch模拟并发赛跑

用于等待多个线程全部到达某个点后再统一执行:

import java.util.concurrent.CountDownLatch;
public class Race {
    private static final int RUNNER_NUM = 5;
    private static CountDownLatch startLatch = new CountDownLatch(1);
    private static CountDownLatch finishLatch = new CountDownLatch(RUNNER_NUM);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= RUNNER_NUM; i++) {
            new Thread(() -> {
                try {
                    startLatch.await(); // 所有线程等待发令枪
                    System.out.println(Thread.currentThread().getName() + " 起跑!");
                    Thread.sleep((long)(Math.random() * 1000));
                    System.out.println(Thread.currentThread().getName() + " 到达终点!");
                    finishLatch.countDown();
                } catch (InterruptedException e) { e.printStackTrace(); }
            }, "运动员" + i).start();
        }
        System.out.println("裁判员:预备——");
        Thread.sleep(500);
        startLatch.countDown(); // 发令枪响
        finishLatch.await(); // 等待所有运动员完成
        System.out.println("所有运动员都完成比赛!");
    }
}

Q: CountDownLatch与CyclicBarrier的区别?
A: CountDownLatch是计数一次,不可重用;CyclicBarrier可循环使用,且支持在到达屏障时执行额外动作。


案例四:CyclicBarrier实现分批汇总

模拟多线程分阶段计算并汇总结果:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class DataSummary {
    private static final int THREAD_COUNT = 3;
    private static CyclicBarrier barrier = new CyclicBarrier(THREAD_COUNT, () -> 
        System.out.println("---所有线程完成本轮汇总,开始下一轮---"));
    public static void main(String[] args) {
        for (int i = 0; i < THREAD_COUNT; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + " 处理数据1...");
                    barrier.await();
                    System.out.println(Thread.currentThread().getName() + " 处理数据2...");
                    barrier.await();
                    System.out.println(Thread.currentThread().getName() + " 完成最终汇总!");
                } catch (InterruptedException | BrokenBarrierException e) {
                    e.printStackTrace();
                }
            }, "工作线程-" + i).start();
        }
    }
}

Q: 如果某个线程在barrier.await()时抛异常会怎样?
A: 该线程会进入BrokenBarrierException状态,其他线程也会收到此异常,整个屏障将不可用。


常见问题与最佳实践

死锁预防:确保多个锁的获取顺序一致,或使用tryLock超时机制。
性能调优

  • 减小同步块范围(只锁关键代码)
  • 优先使用偏向锁(JVM自动优化)
  • 高并发场景用Lock替代synchronized

工具使用建议

  • 读多写少:使用ReadWriteLock
  • 限制并发数:使用Semaphore
  • 无锁编程:使用Atomic类或CAS操作

Q: volatile能替代synchronized吗?
A: 不能,volatile只保证可见性,不保证原子性,例如对volatile变量的i++操作仍是线程不安全的。


总结与问答

本文通过银行转账、库存扣减、并发赛跑、数据汇总四个实战案例,全面展示了Java线程同步的synchronized、Lock、CountDownLatch、CyclicBarrier等核心实现方式,选择合适的同步机制需要综合考虑业务场景、并发量和性能需求。

问答精选:

Q1: 如果使用synchronized修饰整个方法,但方法内只有少量代码需要同步怎么办?
A: 应该改为在方法内部使用synchronized代码块,只锁住需要保护的代码片段,提高并发性能。

Q2: 多个线程访问同一个对象的同步和非同步方法,会相互阻塞吗?
A: 不会,同步方法需要获取对象锁,非同步方法不需要,它们可以同时执行。

Q3: 如何解决ABA问题?
A: 使用带版本号的原子操作类(如AtomicStampedReference),通过对比版本号来避免ABA问题。

Q4: 在高并发环境下,哪种同步方式性能最高?
A: 无锁同步(如AtomicInteger)性能高于锁,但只能处理单一变量;公平锁性能低于非公平锁,但可防止线程饥饿。

通过以上案例和问答,相信您已掌握Java线程同步的核心实现技巧,在实际项目中,请务必结合压测工具验证同步逻辑的正确性和性能表现。

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