当前位置: 首页 > news >正文

Java线程同步:从多线程协作到银行账户安全

前言:当多线程成为双刃剑

在单核CPU时代,多线程曾是“伪并行”的代名词;如今,面对多核处理器与分布式系统的浪潮,真正的并行计算已成为Java高并发编程的基石。

然而,线程在共享资源时的不确定性,如同一场没有红绿灯的十字路口交通——竞态条件(Race Condition)、死锁(Deadlock)、内存可见性(Memory Visibility)问题频发。如何让多个线程安全有序地协同工作?这正是线程同步(Thread Synchronization)的核心使命。本文将带你穿透synchronized、Lock、CAS等技术的迷雾,构建线程安全的铜墙铁壁。

微信图片_20250525162736

一、线程同步简介

线程同步(Thread Synchronization)是一种协调多个线程对共享资源的访问的机制,确保在同一时刻只有一个线程能够操作特定的共享数据。其核心目标是防止因并发操作导致的数据不一致问题。

典型场景

  • 银行账户扣款:两个线程同时修改账户余额,可能导致最终结果错误。
  • 生产者-消费者模型:需要协调生产者与消费者之间的数据交换。
  • 任务协作:多个线程需要按特定顺序执行操作。

二、多线程带来的问题

在多线程程序中,常见的问题主要来自于线程对共享资源的访问。当多个线程并发操作共享资源时,可能会出现以下问题:

  1. 竞态条件(Race Condition) :当多个线程并发地对共享资源进行修改时,可能会出现执行顺序的不确定性,导致数据的状态不一致。例如,两个线程同时对一个共享变量进行自增操作,可能导致最终结果不如预期。
  2. 死锁(Deadlock) :多个线程在执行过程中相互等待对方释放资源,导致程序陷入无限等待的状态,无法继续执行。
  3. 活锁(Livelock) :与死锁类似,活锁发生在多个线程相互响应对方的动作,虽然程序没有完全停止,但也无法正常进行。
  4. 饥饿(Starvation) :某些线程由于系统资源分配不均,长时间得不到执行,导致程序不公平地分配CPU资源。

举例:

多线程可以充分利用多核 CPU 的计算能力,那多线程难道就没有一点缺点吗?有。

多线程很难掌握,稍不注意,就容易使程序崩溃。我们以在路上开车为例:

在一个单向行驶的道路上,每辆汽车都遵守交通规则,这时候整体通行是正常的。『单向车道』意味着『一个线程』,『多辆车』意味着『多个 job 任务』。

单线程顺利同行
单线程顺利同行

如果需要提升车辆的同行效率,一般的做法就是扩展车道,对应程序来说就是『加线程池』,增加线程数。这样在同一时间内,通行的车辆数远远大于单车道。

多线程顺利同行
多线程顺利同行

然而车道一旦多起来,『加塞』的场景就会越来越多,出现碰撞后也会影响整条马路的通行效率。这么一对比下来『多车道』就比『单车道』慢多了。

多线程故障
多线程故障

防止汽车频繁变道加塞可以在车道间增加『护栏』,那在程序的世界里该怎么做呢?

1. 竞态条件(Race Condition)

竞态条件是指多个线程对共享资源的访问顺序影响程序的最终结果。例如,以下代码模拟了两个线程同时修改一个银行账户的余额:

package org.example;public class UnsafeAccount {private int balance = 1000;public void withdraw(int amount) {// 人为添加延迟,放大竞态条件窗口if (balance >= amount) {try {Thread.sleep(100); // 模拟耗时操作,增加其他线程介入的机会} catch (InterruptedException e) {e.printStackTrace();}balance -= amount;System.out.println(Thread.currentThread().getName() + " 取款 " + amount + ",余额: " + balance);} else {System.out.println(Thread.currentThread().getName() + " 余额不足");}}public static void main(String[] args) throws InterruptedException {UnsafeAccount account = new UnsafeAccount();Thread t1 = new Thread(() -> account.withdraw(600), "线程1");Thread t2 = new Thread(() -> account.withdraw(600), "线程2");t1.start();t2.start();// 等待两个线程执行完毕t1.join();t2.join();System.out.println("最终余额: " + account.balance);}
}

image

image

最终余额可能为-200

问题根源balance >= amount​的条件判断与balance -= amount​的操作非原子性,中间插入其他线程操作导致数据错误。

竞态条件示例的逐步解析

场景复现
两个线程同时调用 withdraw(600)​ 方法,初始余额为1000元。以下是导致余额为-200的关键执行流程:

  1. 线程1(T1)启动

    • 读取 balance​ 值:1000
    • 检查条件 balance >= 600​ → 通过
    • 准备执行 balance -= 600​(但尚未完成)
  2. 线程2(T2)同时启动

    • 在T1修改 balance​ 前,T2也读取 balance​ 值:1000
    • 检查条件 balance >= 600​ → 通过
    • 准备执行 balance -= 600
  3. 操作交错执行

    • T1 完成操作:balance = 1000 - 600 = 400
    • T2 继续执行:由于它之前读取的 balance​ 是1000,仍会执行 balance = 1000 - 600 = 400
    • 但实际上此时 balance​ 已经是400,T2的操作覆盖了T1的结果,最终 balance = 400 - 600 = -200​(假设业务允许透支)

关键问题

  • 非原子操作balance -= amount​ 并非一步完成,实际包含三步:

    1. 读取当前 balance​ 值
    2. 计算新值(原值 - amount)
    3. 将新值写回 balance
  • 线程切换时机:若多个线程在步骤1和步骤3之间切换,会导致共享数据被覆盖。

可视化流程

时间线         | 线程1操作                | 线程2操作
---------------|--------------------------|--------------------------
t1             | 读取 balance=1000       |
t2             |                         | 读取 balance=1000
t3             | 计算 balance=1000-600=400 | 
t4             |                         | 计算 balance=1000-600=400
t5             | 写入 balance=400        |
t6             |                         | 写入 balance=400-600=-200

修复方案
通过同步机制(如 synchronized​)保证操作的原子性:

public class Account {private int balance = 1000;// 添加synchronized关键字public synchronized void withdraw(int amount) {if (balance >= amount) {balance -= amount; // 现在线程安全}}
}

修复后的执行流程

  • 线程1(T1) 先获得锁,完成全部操作(读→计算→写),释放锁。
  • 线程2(T2) 必须等待锁释放后,才能进入方法。此时 balance​ 已经是400,条件 balance >= 600​ 不成立,操作被拒绝。
  • 最终余额:400(符合预期)。

总结
竞态条件的本质是多个线程对共享资源的非原子操作的交错执行。解决方法是:

  1. 加锁(如 synchronized​、ReentrantLock​)确保操作的原子性。
  2. 使用原子变量(如 AtomicInteger​)替代基础类型。
  3. 设计无状态对象线程封闭(如 ThreadLocal​)避免共享。

2. 死锁

public class DeadlockExample {public static void main(String[] args) {// 定义两个锁对象Object lockA = new Object();Object lockB = new Object();// 线程1:先获取lockA,再请求lockBThread t1 = new Thread(() -> {synchronized (lockA) {System.out.println(Thread.currentThread().getName() + " 持有 lockA,等待 lockB...");try {Thread.sleep(100); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockB) {System.out.println(Thread.currentThread().getName() + " 持有 lockA 和 lockB");}}}, "Thread-1");// 线程2:先获取lockB,再请求lockAThread t2 = new Thread(() -> {synchronized (lockB) {System.out.println(Thread.currentThread().getName() + " 持有 lockB,等待 lockA...");try {Thread.sleep(100); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}synchronized (lockA) {System.out.println(Thread.currentThread().getName() + " 持有 lockB 和 lockA");}}}, "Thread-2");// 启动线程t1.start();t2.start();}
}

image

运行结果与死锁分析

  1. 线程执行顺序

    • 线程1 先获取 lockA​,然后休眠100ms,接着尝试获取 lockB​。
    • 线程2 先获取 lockB​,然后休眠100ms,接着尝试获取 lockA​。
  2. 死锁发生条件

    • 互斥lockA​ 和 lockB​ 不能同时被两个线程持有。
    • 不可抢占:线程必须主动释放锁。
    • 循环等待:线程1等待线程2持有的 lockB​,线程2等待线程1持有的 lockA​。
    • 持有并等待:两个线程都持有部分锁。
  3. 典型输出(死锁发生时)

    Thread-1 持有 lockA,等待 lockB...
    Thread-2 持有 lockB,等待 lockA...
    
    • 程序卡住,无后续输出,两个线程互相等待对方释放锁。

如何检测死锁?

  1. 使用 jstack命令

    • 在终端运行 jstack <进程ID>​,查看线程状态:

      Found one Java-level deadlock:
      =============================
      "Thread-2":waiting to lock monitor locked by "Thread-1",
      "Thread-1":waiting to lock monitor locked by "Thread-2",
      
  2. 代码中主动检测死锁(可选):

    ThreadMXBean tmBean = ManagementFactory.getThreadMXBean();
    long[] deadlockedThreads = tmBean.findDeadlockedThreads();
    if (deadlockedThreads != null && deadlockedThreads.length > 0) {System.out.println("死锁已发生!");
    }
    

关键点总结

问题原因解决方案
死锁线程1持有 lockA​ 等待 lockB​,线程2持有 lockB​ 等待 lockA避免交叉请求锁,或按固定顺序请求锁
互斥锁对象只能被一个线程持有使用 ReentrantLock​ 支持超时机制
不可抢占线程必须主动释放锁使用 tryLock()​ 显式尝试获取锁

改进方案(避免死锁)

// 固定顺序请求锁(例如:总是先获取 lockA,再获取 lockB)
Thread t3 = new Thread(() -> {synchronized (lockA) {try { Thread.sleep(100); } catch (InterruptedException e) {}synchronized (lockB) { /* ... */ }}
});
Thread t4 = new Thread(() -> {synchronized (lockA) {try { Thread.sleep(100); } catch (InterruptedException e) {}synchronized (lockB) { /* ... */ }}
});

通过统一锁请求顺序,避免循环等待,彻底消除死锁风险。

三、Java线程同步的实现方式

Java 提供了多种线程同步机制

1. synchronized​ 关键字

(1)方法级同步

  • 实例方法:锁定当前对象(this​)。

    public synchronized void withdraw(int amount) {if (balance >= amount) {balance -= amount;}
    }
    
  • 静态方法:锁定类的 Class​ 对象。

    public static synchronized void staticMethod() {// 类级同步
    }
    

(2)代码块级同步

  • 对象锁:指定任意对象作为锁。

    private Object lock = new Object();
    public void withdraw(int amount) {synchronized (lock) {if (balance >= amount) {balance -= amount;}}
    }
    
  • 类锁:锁定类的 Class​ 对象。

    public void method() {synchronized (YourClass.class) {// 类级同步}
    }
    

(3)synchronized​ 的特性

  • 隐式锁:由 JVM 自动管理加锁和释放。
  • 可重入性:同一个线程可以多次获取同一把锁。
  • 互斥性:同一时刻只有一个线程可以持有锁。

(4)示例:银行账户同步

之前示例未同步结果:

image

优化解决:

image

package com.example.thread;public class UnsafeAccount {private int balance = 1000;public synchronized void withdraw(int amount) {// 人为添加延迟,放大竞态条件窗口if (balance >= amount) {try {Thread.sleep(100); // 模拟耗时操作,增加其他线程介入的机会} catch (InterruptedException e) {e.printStackTrace();}balance -= amount;System.out.println(Thread.currentThread().getName() + " 取款 " + amount + ",余额: " + balance);} else {System.out.println(Thread.currentThread().getName() + " 余额不足");}}public static void main(String[] args) throws InterruptedException {UnsafeAccount account = new UnsafeAccount();Thread t1 = new Thread(() -> account.withdraw(600), "线程1");Thread t2 = new Thread(() -> account.withdraw(600), "线程2");t1.start();t2.start();// 等待两个线程执行完毕t1.join();t2.join();System.out.println("最终余额: " + account.balance);}
}

输出:最终余额为 400​,避免了线程竞争导致的负数问题。

这段代码中使用了 synchronized​ 关键字,这意味着在同一时间只有一个线程能够执行 withdraw​ 方法。

具体来说:

  • 当一个线程调用 withdraw​ 方法时,它会获得 Account​ 对象的锁,其他任何线程在这段时间内尝试调用这个 withdraw​ 方法都会被阻塞,直到第一个线程完成并释放锁。
  • 在你的代码示例中,如果 t1​ 线程正在执行 withdraw​ 方法,t2​ 线程必须等待,直到 t1​ 线程执行完毕并释放 Account​ 对象的锁,然后 t2​ 才能开始执行 withdraw​ 方法。

这样做的目的就是为了确保对 balance​ 的修改是线程安全的,避免了因并发访问导致的状态不一致问题。如果没有 synchronized​,两个线程可能会同时检查和修改 balance​,从而导致余额计算错误。

2. ReentrantLock​ 显式锁

(1)基本用法

import java.util.concurrent.locks.ReentrantLock;public class LockExample {private final ReentrantLock lock = new ReentrantLock();private int sharedResource;public void updateResource(int value) {lock.lock(); // 显式获取锁try {sharedResource = value;} finally {lock.unlock(); // 确保释放锁}}
}

(2)高级特性

  • 尝试加锁:非阻塞获取锁。

    if (lock.tryLock()) {try {// 临界区代码} finally {lock.unlock();}
    }
    
  • 超时加锁:在指定时间内尝试获取锁。

    if (lock.tryLock(1, TimeUnit.SECONDS)) {try {// 临界区代码} finally {lock.unlock();}
    }
    
  • 条件变量(Condition) :替代 wait/notify​,实现更细粒度的线程协作。

    ReentrantLock lock = new ReentrantLock();
    Condition notEmpty = lock.newCondition();public void waitForData() {lock.lock();try {while (data.isEmpty()) {notEmpty.await(); // 等待数据}} finally {lock.unlock();}
    }
    

(3)示例:生产者-消费者模型

public class ProducerConsumer {private final ReentrantLock lock = new ReentrantLock();private final Condition notFull = lock.newCondition();private final Condition notEmpty = lock.newCondition();private Queue<Integer> queue = new LinkedList<>();private static final int CAPACITY = 10;public void produce(int item) throws InterruptedException {lock.lock();try {while (queue.size() >= CAPACITY) {notFull.await(); // 等待队列不满}queue.add(item);notEmpty.signal(); // 通知消费者} finally {lock.unlock();}}public int consume() throws InterruptedException {lock.lock();try {while (queue.isEmpty()) {notEmpty.await(); // 等待队列非空}int item = queue.remove();notFull.signal(); // 通知生产者return item;} finally {lock.unlock();}}
}

3. volatile​ 关键字

(1)特性

  • 可见性:确保多个线程对变量的修改立即对其他线程可见。
  • 禁止指令重排序:防止编译器优化导致的执行顺序变化。
  • 不保证原子性:仅适用于单线程写、多线程读的场景。

(2)示例:状态标志位

public class FlagExample {private volatile boolean isRunning = true;public void stop() {isRunning = false;}public void runTask() {while (isRunning) {// 执行任务}}
}

4. 原子类(java.util.concurrent.atomic​)

(1)原理

基于 CAS(Compare-And-Swap) 实现无锁并发,通过硬件指令(如 CAS​)确保原子性。

(2)常用类

  • AtomicInteger​:原子整数操作。
  • AtomicReference​:原子对象引用操作。
  • AtomicLong​:原子长整型操作。

(3)示例:无锁计数器

import java.util.concurrent.atomic.AtomicInteger;public class Counter {private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet(); // 原子自增}public int getCount() {return count.get();}
}

5. wait​ / notify​ / notifyAll

(1)基本用法

  • wait() ​:释放锁并进入等待状态,直到其他线程调用 notify​ 或 notifyAll​。
  • notify() ​:随机唤醒一个等待的线程。
  • notifyAll() ​:唤醒所有等待的线程。

(2)示例:线程协作

public class WaitNotifyExample {public synchronized void waitMethod() {try {System.out.println("线程 " + Thread.currentThread().getName() + " 进入 wait");wait(); // 释放锁并等待System.out.println("线程 " + Thread.currentThread().getName() + " 被唤醒");} catch (InterruptedException e) {e.printStackTrace();}}public synchronized void notifyMethod() {notify(); // 唤醒一个线程System.out.println("线程 " + Thread.currentThread().getName() + " 调用 notify");}public static void main(String[] args) {WaitNotifyExample example = new WaitNotifyExample();Thread t1 = new Thread(() -> example.waitMethod(), "Thread-1");Thread t2 = new Thread(() -> {try {Thread.sleep(1000);example.notifyMethod();} catch (InterruptedException e) {e.printStackTrace();}}, "Thread-2");t1.start();t2.start();}
}

四、同步机制对比与选择

机制特性适用场景优点缺点
synchronized隐式锁,可重入,互斥访问简单同步需求使用简单,JVM优化无法控制锁粒度,性能较低
ReentrantLock显式锁,支持超时、尝试加锁、条件变量复杂并发场景灵活性高,性能优化需手动管理锁,易出错
volatile保证可见性,禁止指令重排序单写多读的状态标志性能高不保证原子性
原子类基于CAS的无锁操作高并发计数、更新操作无需加锁,性能高仅适用于简单数据类型
wait​/notify线程间协作,需在同步代码块中调用生产者-消费者、任务等待实现线程通信需谨慎处理条件检查,易出现虚假唤醒

五、最佳实践与注意事项

  1. 减少锁粒度:尽量缩小同步代码块范围,减少锁竞争。
  2. 避免死锁:按固定顺序获取锁,或使用超时机制。
  3. 使用高阶并发工具:优先考虑 java.util.concurrent​ 包中的线程安全类(如 ConcurrentHashMap​)。
  4. 警惕隐式锁泄漏:确保 finally​ 块中释放锁。
  5. 优先选择原子类:在适用场景下使用无锁操作提升性能。

六、总结

Java线程同步是多线程编程的核心技能,合理选择同步机制可以显著提升程序的并发性能和稳定性。以下是关键总结:

  • 简单场景:优先使用 synchronized​ 或原子类。
  • 复杂协作:选择 ReentrantLock​ 和 Condition​。
  • 状态标志:使用 volatile​ 确保可见性。
  • 避免过度同步:合理设计锁粒度,减少性能开销。

通过深入理解这些机制,开发者可以编写出高效、可靠的多线程程序,应对高并发场景的挑战。

求点关注-gif动图 138_爱给网_aigei_com

相关文章:

  • Linux核心技术:Linux文件系统与bootFS/rootFS
  • 进程通信-内存共享
  • 【目标检测】【医学图像目标检测】BGF-YOLO:脑肿瘤检测的多尺度注意力特征融合
  • Flink 常用算子详解与最佳实践
  • Python数据可视化实战:让数据从「数字堆」变成「故事书」
  • NestJS——重构日志、数据库、配置
  • Javase 基础加强 —— 08 IO流
  • 【Python 命名元祖】collections.namedtuple 学习指南
  • Java中关于数组的使用(下)
  • springboot中过滤器配置使用
  • 《爱的艺术》
  • python打卡训练营打卡记录day36
  • 电梯调度算法详解与Python实现
  • 简单数学板子和例题
  • 【短距离通信】【WiFi】WiFi7关键技术之4096-QAM、MRU
  • LLMs之Qwen:《Qwen3 Technical Report》翻译与解读
  • 用python实现中国象棋
  • PDF 编辑批量拆分合并OCR 识别
  • TCP 协议的相关特性
  • 识别速度快且精准的OCR工具
  • 做venn图网站/网页设计html代码大全
  • 做网站需要提供什么资料/短视频推广平台
  • 平湖网站设计/百度文章收录查询
  • 帮客户做网站的公司/系统优化软件排行榜
  • 专业网站建设咨/富阳seo关键词优化
  • web项目网站开发流程怎么写/提升关键词排名seo软件