在并发编程中我们常说的“竞态”是什么?

本文深入解析了多线程编程中的竞态条件现象,通过实例代码展示了竞态条件的发生及原因,同时提供了两种解决策略:加锁和使用原子操作。

1 何谓“竞态”

之前在学习一篇文章的时候,就看到“竞态”,但是不知道什么意思,文章中也没有对“竞态”做更多的解释,后来经过一番的探索,终于弄的差不多明白了,今天写点总结。
首先,我们要明白“竞态”是什么。先说我的结论吧,“竞态”就是在多线程的编程中,你在同一段代码里输入了相同的条件,但是会输出不确定的结果的情况。我不知道这个解释是不是够清楚,我们接着往下看,下面我们用一段代码来解释一下啊。
出现竞态条件的代码:

public class MineRaceConditionDemo {
    private int sharedValue = 0;
    private final static int MAX = 1000;
    private int raceCondition() {
        if (sharedValue < MAX) {
            sharedValue++;
        } else {
            sharedValue = 0;
        }
        return sharedValue;
    }

    public static void main(String[] args) {
        MineRaceConditionDemo m = new MineRaceConditionDemo();
        ExecutorService es = new ThreadPoolExecutor(10,
                10,
                5,
                TimeUnit.MINUTES,
                new ArrayBlockingQueue<Runnable>(1000),
                new ThreadPoolExecutor.CallerRunsPolicy());
        ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();

        for (int i = 0; i < 1000; i++) {
            es.execute(() -> {
                try {
                //  这是精髓所在啊,如果没有这个,那么要跑好几次才会出现竞态条件。
                //  这个用来模拟程序中别的代码的处理时间。
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int num = m.raceCondition();
                if (map.get(num) != null) {
                    System.out.println("the repeat num: " + num);
                    System.out.println("happen.");
                } else {
                    map.put(num, 0);
                }
            });
        }
        es.shutdown();
    }
}

以上的代码是我自己的设计的一段会出现竞态条件的代码,比较简陋,但是可以说明问题了,你只要运行上面这段代码,每次的输出的结果级大概率都是不同的,但是也有以外,比如你的电脑的性能很强,这段代码也会出现执行正确的情况,也就是啥也不输出。
比如有的时候输出这个:

the repeat num: 78
happen.
the repeat num: 229
happen.
the repeat num: 267
happen.
the repeat num: 267
happen.
the repeat num: 498
happen.

有点时候输出这个:

the repeat num: 25
happen.
the repeat num: 157
happen.

当然,以上的是我的输出的,你们的输出肯定也是不同的。
对于上面这些,同一段代码,对于同样的输出,但是程序的输出有的时候是正确,有的时候是错误的,这种情况,我们称之为“竞态”。最要命的就是,代码每次输出不是每次都错误,而是你不知道他什么时候会正确,什么时候会错误。
当然,如果以上的代码执行的情况就是,啥都不输出,所有的值都是唯一的。

2 “竞态”为什么会发生?

“竞态”的发生主要是因为多个线程都对一个共享变量(比如上面的 sharedValue 就属于共享变量)有读取-修改的操作。在某个线程读取共享变量之后,进行相关操作的时候,别的线程把这个变量给改了,从而导致结果出现了错误。

什么样的代码模式会发生“竞态”

这部分知识主要是来自《Java多线程编程实战指南 核心篇》。
这里书中提到,会发生竞态条件就是两个模式:read-modify-write(读-改-写)和 check-than-act(检测而后行动)。
当然,这里面的都有一个相同的操作过程:某个有读取这个“共享变量”的操作,然后别的线程有个修改这个变量的操作。这里有个重点,在多个线程中,起码有一个线程有更新操作;如果所有的线程都是读操作,那么就不存在什么竞态条件。
总体来说,就是要thread1#load - thread2#update。 这种的模式,起码是是要有两个线程的,而且其中某个线程肯定是要有更新“共享变量”操作的,另一个线程不管是读取变量还是更新变量都会出现错误(要么读取脏数据、要么丢失更新结果)。

3 如何消除“竞态”?

单以上面的操作来说,一般来说有两种解法方式,

3.1 加锁

加上synchronized关键字,保证每次只能有一个线程获取共享变量的使用权。

private synchronized int raceCondition() {
    if (sharedValue < MAX) {
        sharedValue++;
    } else {
        sharedValue = 0;
    }
    return sharedValue;
}

3.2 利用原子操作

利用java的工具包里的 AtomicInteger,代替int,利用原子操作消除“竞态”。

private AtomicInteger sharedValue = new AtomicInteger(0);
private final static int MAX = 1000;
private int raceCondition() {
    if (sharedValue.get() < MAX) {
        return sharedValue.getAndIncrement();
    } else {
        sharedValue.set(0);
        return sharedValue.get();
    }
}

以上两种方法的要义就是保证每个线程在操作“共享变量”的都是原子操作。

<think>我们正在讨论Java并发编程中的临界区(Critical Section)和条件(Race Condition)这两个核心概念。根据引用内容,我们可以这样定义和区分它们: ### 1. 条件(Race Condition) **定义**:条件是指多个线程对共享数据进行访问和操作时,最终执行结果依赖于线程执行的时序。当多个线程在没有适当同步的情况下并发修改共享数据时,由于执行顺序的不确定性,可能导致程序出现不可预期的行为。 **关键特征**: - 涉及多个线程对同一共享资源的修改操作 - 执行结果依赖于线程调度的随机性 - 通导致数据不一致或程序错误 **引用支持**: > 在Java并发编程中,条件(Race Condition)... 条件(Race Condition)[^1] **示例**(基于引用[5]的计数器类): ```java public class Counter { protected long count = 0; // 共享变量 // 非原子操作:读取-修改-写入 public void setCount(long count) { this.count = this.count + count; // 条件发生点 } } ``` 当两个线程同时调用`setCount()`方法时: 1. 线程A读取`count=100` 2. 线程B读取`count=100` 3. 线程A计算`100+50=150`写入 4. 线程B计算`100+30=130`写入 → **覆盖了线程A的更新** ### 2. 临界区(Critical Section) **定义**:临界区是指访问共享资源的代码段,该代码段需要以互斥方式执行。即同一时刻只允许一个线程执行临界区内的代码,以防止条件的发生。 **关键特征**: - 包含对共享资源的读写操作 - 需要通过同步机制保护(如`synchronized`) - 确保操作的原子性和可见性 **引用支持**: > 临界区保护:保护某些关键代码段,确保同一时刻只有一个线程可以执行[^4] **示例**(使用`synchronized`保护临界区): ```java public class SafeCounter { private long count = 0; // 通过同步方法保护临界区 public synchronized void add(long value) { this.count += value; // 受保护的临界区 } } ``` ### 3. 区别与联系 | 维度 | 条件 (Race Condition) | 临界区 (Critical Section) | |--------------|-----------------------------------|-----------------------------------| | **本质** | 程序错误(时序依赖的异行为) | 代码区域(需要保护的共享资源访问段)| | **成因** | 缺乏同步的并发修改 | 未定义(是解决方案的实施对象) | | **解决方式** | 通过保护临界区来消除 | 使用同步机制实现互斥访问 | | **关系** | 临界区未保护会导致条件 | 临界区是预防条件的实施载体 | **关系图解**: ```mermaid flowchart LR A[共享资源] --> B(多个线程并发访问) B --> C{未保护临界区} C -->|是| D[条件] C -->|否| E[数据一致性] D --> F[程序错误] ``` ### 4. 预防条件的实践 根据引用[4],Java中主要通过以下方式保护临界区: #### 4.1 synchronized机制 ```java // 同步方法(锁对象为this) public synchronized void transfer(int amount) { balance += amount; // 受保护的临界区 } // 同步代码块(显式指定锁对象) public void transfer(int amount) { synchronized(lockObject) { // 使用专用锁对象 balance += amount; } } ``` **优势**: - 保证操作的原子性(atomicity)和可见性(visibility) - 支持可重入(reentrancy) #### 4.2 volatile的应用限制 虽然`volatile`能保证可见性,但**无法解决条件**: ```java public class VolatileCounter { private volatile int count = 0; public void increment() { count++; // 仍存在条件(非原子操作) } } ``` 原因:`count++`包含三个独立操作(读取→修改→写入),`volatile`仅保证读取时获取最新值,但无法阻止多线程同时执行修改。 ### 5. 典型场景分析 **银行转账案例**(引用[4]扩展): ```java class BankAccount { private double balance; // 必须保护的操作:检查与更新必须原子执行 public synchronized void transfer(BankAccount dest, double amount) { if (this.balance >= amount) { this.balance -= amount; // 临界区起点 dest.balance += amount; // 临界区终点 } } } ``` 若不使用同步: 1. 线程A检查余额充足(如1000>500) 2. 线程B同时检查余额充足(1000>700) 3. 线程A扣减500 → 余额500 4. 线程B扣减700 → 余额-200(**透支错误**) ### 总结 - **条件**:并发编程中的错误现象,源于未受控的共享资源访问时序 - **临界区**:程序中需要互斥执行的代码片段,是解决条件的核心区域 - **防护核心**:通过`synchronized`、`Lock`等机制实现临界区互斥访问 正确识别临界区并施加同步保护,是消除条件、保证线程安全的关键[^1][^4]。 --- §§相关问题§§ 1. 除了`synchronized`,Java中还有哪些机制可以保护临界区? 2. 如何检测代码中潜在的条件? 3. 使用`volatile`关键字在什么情况下可以保证线程安全? 4. 多线程环境下,对不可变对象的访问是否存在条件?为什么? 5. 什么是原子操作?Java中有哪些原子操作可以避免条件?</think>### Java并发编程中临界区与条件的定义及区别 #### 一、条件(Race Condition) **定义**: 当多个线程**无序访问共享资源**时,程序的执行结果**依赖于线程执行的时序**,导致结果不可预测的现象[^1][^4]。 **核心特征**: ```mermaid flowchart LR A[线程A] -->|同时访问| C[共享资源] B[线程B] -->|同时访问| C C --> D{结果不确定} ``` **典型场景**: ```java // 引用[5]的计数器类存在条件 public class Counter { protected long count = 0; // 共享资源 public void setCount(long count) { // 非原子操作:读取-修改-写入 this.count = this.count + count; } } ``` 当两个线程同时执行`setCount(5)`: 1. 线程A读取`count=0` 2. 线程B读取`count=0` 3. 线程A写入`0+5=5` 4. 线程B写入`0+5=5` → **结果应为10,实际为5** #### 二、临界区(Critical Section) **定义**: 程序中**必须互斥执行**的代码段,即同一时刻只允许一个线程访问的共享资源操作区域[^1][^4]。 **核心特征**: ```mermaid flowchart LR T1[线程1] -->|申请进入| L[临界区] L -->|锁定| T1 T2[线程2] -->|等待| L T1 -->|释放| L L -->|允许进入| T2 ``` **实现方式**(引用[4]示例): ```java public class BankAccount { private int balance; // 共享资源 // synchronized保护的临界区 public synchronized void deposit(int amount) { balance += amount; // 临界区代码 } } ``` #### 三、核心区别与联系 | 维度 | 条件 (Race Condition) | 临界区 (Critical Section) | |--------------|-----------------------------------|-----------------------------------| | **本质** | 程序错误(异行为) | 解决方案(代码区域) | | **成因** | 缺乏同步的并发访问 | 未定义(是解决方案的组成部分) | | **解决方式** | 通过保护临界区来消除 | 使用同步机制实现互斥访问 | | **关系** | 临界区未保护会导致条件 | 保护临界区可预防条件 | **关系图解**: ```mermaid flowchart TD A[共享资源] --> B(无保护并发访问) B --> C[条件] A --> D{临界区保护} D -->|synchronized/Lock| E[有序访问] E --> F[结果正确] ``` #### 四、典型场景分析 1. **银行转账案例**(引用[4]扩展): ```java class BankAccount { private int balance; // 临界区:必须保证原子性 public void transfer(BankAccount dest, int amount) { synchronized(this) { // 保护临界区起点 if (this.balance >= amount) { this.balance -= amount; dest.balance += amount; // 仍存在风险! } } // 保护临界区终点 } } ``` **问题**: 当转账涉及两个账户时,需使用**全局锁**或`Lock.tryLock()`避免死锁[^4]。 2. **双重检查锁定**(单例模式): ```java public class Singleton { private static volatile Singleton instance; // volatile解决可见性问题 public static Singleton getInstance() { if (instance == null) { // 条件发生点 synchronized(Singleton.class) { // 临界区保护 if (instance == null) { instance = new Singleton(); // 非原子操作 } } } return instance; } } ``` #### 五、最佳实践 1. **缩小临界区范围**: ```java // 优化前:整个方法加锁 public synchronized void process() { readFile(); // IO操作(无需同步) updateCount(); // 需要同步的操作 } // 优化后:精确锁定临界区 public void process() { readFile(); // 非临界区 synchronized(this) { updateCount(); // 临界区 } } ``` 2. **使用并发集合**: ```java // 代替手动同步的HashMap ConcurrentMap<String, Integer> map = new ConcurrentHashMap<>(); map.compute("key", (k, v) -> (v == null) ? 1 : v + 1); // 原子操作 ``` 3. **原子变量类**: ```java AtomicInteger count = new AtomicInteger(0); count.incrementAndGet(); // 无锁原子操作 ``` ### 总结 - **条件**是并发编程中的**错误现象**,表现为结果不确定性 - **临界区**是解决条件的**技术手段**,通过互斥访问保证原子性 - 使用`synchronized`、`Lock`或原子类保护临界区可消除条件 - 临界区范围应尽量缩小以提升性能[^1][^4] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值