专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

Java中的Race condition和Critical section(译)

Java中的Race condition和Critical section(译)

race condition,即竞态,是一种可能发生于critical section中的特殊状态,critical section一般翻译为临界区,我个人认为临界区不是很直观,因此不做翻译,仍然用英文。这里的critical section实际上是一个可能会被多个线程并发执行的代码区段,并发线程的不同执行顺序会直接影响整个并发程序的执行结果。

由于并发线程的不同执行顺序直接影响这部分代码的执行结果,这时我们就说这部分代码为critical section,而critical section的代码被多个线程并发执行的时候,则处于被竞争执行的状态,即race condition

Critical section

事实上,同一段代码在多个线程并发执行时并不一定会引发问题,而是在多个线程同时竞争访问共享的资源时才有可能引发race condition。这里的资源包括内存资源(变量,数组或对象等),系统资源(数据库,web服务等)以及文件等。

实际上只有多个线程并发对共享的资源进行写操作时才有可能引发问题。多个线程读共享资源并不会引发什么问题。

下面的这段代码是一个critical section例子,多线程并发执行时会引发问题:

public class Counter {
  protected long count = 0;
  public void add(long value) {
    this.count = this.count + value;
  }
}

假设两个不同的线程A和B,通过一个相同的Counter对象,并发执行该对象的add方法。操作系统何时进行线程的切换是不确定的。add方法内部的这行代码编译成字节码在JVM内部执行时并不是一个原子操作,而是分解成了类似如下所示的几个字令来执行:

1、 从主内存将this.count读到CPU的寄存器内;
2、 将value值加到寄存器的值;
3、 将寄存器内的值写回到主内存。

将这个过程,对应到两个两个线程A、B并发执行,则可能以如下的顺序进行执行:

     this.count = 0;

A: 将this.count的值从主内存读取到寄存器(0)
B: 将this.count的值从主内存读取到寄存器(0)
B: 将value 2加到寄存器上
B: 将寄存器的值(2)写回到主内存。这是this.count的值为2
A: 将value 3加到寄存器上
A: 将寄存器的值(3)写回到主内存。这是this.count的值为3

本来这两个线程A,B是想将2和3加到count上,预期的结果应该是5才对。但是由于这两个线程是并发交叉执行的,导致线程A的计算结果3最终写回到主内存,同时也覆盖了现场B写会到主内存的2。执行结果不符合预期。当然,这里假象出来的这个执行流程只是一种可能发生的情况,也有可能执行结果是2或5。但是只要有这种竞态发生的可能性,这种代码段就是critical section,是我们需要极力杜绝的。

如何避免race condition

那么我们如何避免race condition的发生呢?答案是原子性

我们需要将有可能发生竞态的critical section包成一个原子操作,即如果一个线程正在执行这部分代码,那么其他线程只能等到该线程结束执行离开后才能开始执行。

具体来说,我们可以通过一些线程之间相互同步的手段来实现。比如:

1、 synchronized代码块;
2、 锁;
3、 原子性变量,如java.util.concurrent.atomic.AtomicInteger

critical section的吞吐量

对于逻辑简单的critical section,通过synchronized代码块来避免竞态没啥问题,但是对于逻辑复杂、代码量大的critical section来说,这种方式无疑会降低整个系统的吞吐量。这时我们可以尝试将其拆解成多个独立的、较小的critical section

举个例子:

public class TwoSums {
  private int sum1 = 0;
  private int sum2 = 0;
  public void add(int val1, int val2) {
    synchronized(this) {
      this.sum1 += val1;
      this.sum2 += val2;
    }
  }
}

这里我们为了避免竞态的放生,用synchronized将代码段包了下。这样在多线程并发执行时,这些线程只能挨个轮流执行该代码段。但是我们细想就会发现,这个代码段其实可以拆分成两个独立的、互不影响的子代码段:

public class TwoSums {
  private int sum1 = 0;
  private int sum2 = 0;

  private Integer sum1Lock = new Integer(1);
  private Integer sum2Lock = new Integer(2);

  public void add(int val1, int val2) {
    synchronized(this.sum1Lock) {
      this.sum1 += val1;
    }
    synchronized(this.sum2Lock) {
      this.sum2 += val2;
    }
  }
}

这样如果两个线程并发执行add方法,则可以再一个线程执行第一个子代码段的同时另一个线程执行第二个代码段,因此避免了更长时间的相互等待,提升了吞吐量。

当然,这个例子非常简单,仅为了说明原理,实际项目中可能需要更加认真的分析才知道如何进行拆分。

链接:Race Conditions and Critical Sections

文章永久链接:https://tech.souyunku.com/45462

未经允许不得转载:搜云库技术团队 » Java中的Race condition和Critical section(译)

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们