【06】竞态条件与临界区
【06】竞态条件与临界区
秋雨霏霏 发表于6个月前
【06】竞态条件与临界区
  • 发表于 6个月前
  • 阅读 19
  • 收藏 0
  • 点赞 0
  • 评论 0

标题:腾讯云 新注册用户域名抢购1元起>>>   

摘要: Jakob Jenkov 并发指南翻译:Race Conditions and Critical Sections

竞态条件 是一个可能在临界区中发生的特殊条件。 而临界区表示着,一段在并发执行和顺序执行时有着不同运行结果的代码片段。

临界区中多线程执行的结果可能会因不同的线程执行顺序,而导致不同的运行结果。 竞态条件就是源自线程同时竞争执行临界区,而竞争的结果也会影响着临界区的运行结果。

接下来再详细说说这两个概念。

临界区

在一个应用中运行多个线程,这本身不会有啥问题。 问题是出在,多个线程访问同一资源的时候。 例如:同一个内存区域(变量,数组,对象),系统资源(数据库,接口)以及文件等。

事实上,问题主要是线程对资源进行写操操作。 如果,多个线程都是执行资源读操作,那是不会有什么问题的。

下面这个例子就展示了一个临界区,当多个线程同时执行时,就会出问题:

public class Counter {

 protected long count = 0;

 public void add(long value){
     this.count = this.count + value;
 }
}

想象一下,如果有两个线程,A和B,在同一个实例上进行加法操作。 而我们无法知道操作是如何调度这两个线程的执行。 虽然只有一行代码,但在JVM中add()方法不会以原子的方式被执行。 而是会以下面的逻辑来处理:

  1. 从内存读取this.count的值到寄存器中
  2. 在寄存中执行加法指令操作
  3. 将计算结果从寄存器中写会内存

而当A,B两个线程交叉执行时,可能会得到如下的情况:

    this.count = 0;

A: 读取this.count到寄存器           (0)
B: 读取this.count到寄存器           (0)
B: 在寄存器中加上2                  
B: 将结果2写回内存。this.count=2    (2)
A: 在寄存器中加上3
A: 将结果3写回内存。this.count=3    (3)

两个线程分别想要对计数器累加2和3。所以,执行完成时预期值应该是5。 但是,两个线程的执行过程是交叉进行的,这样就会导致结果不同了。

如果按上面的顺序执行,最后A线程的结果会直接覆盖B线程的结果。

临界区中的竞态条件

上例中,add()方法就是一个临界区。当多个线程执行时,触发竞态条件。

就是说,两个线程争用同一个资源,此时,执行顺序就变得很敏感了,这也就称之为竞态条件。 导致竞态条件的代码块就称为临界区。

防止竞态条件

要想防止竞态条件的发生,就要确保临界区以原子方式执行。 这就意味着,一次只有一个线程在执行,在第一个线程离开临界区之前,不会有其他线程可以进入临界区。

为了避免竞态条件,可以对临界区使用一些同步机制。线程同步可以使用Java阻塞同步,还可以使用Lock以及原子变量来进行同步。

临界区吞吐量

在上例中,对整个临界区进行同步阻塞是可以解决问题的。一般来说,对于临界区是越小越好,这样可以让每一个线程执行更小的临界区。 为了减少对共享资源的争用,这样就可以增加总体上的吞吐量。

来看看这个例子:

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;
        }
    }
}

注意add()方法,对两个成员变量分别进行累加和。 为了防止累加和出现竞态条件,这里使用了一个Java 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()方法。 一个进入第一个同步块,另一个进入第二个。 两个同步块分别在两个不同的对象上进行同步,所以两个不同的线程可以互不干扰的进入两个代码块中。 这样在add()方法中,就可以减少线程等待的时间。

当然,这个例子太简单了。在实际工作中,将共享资源分解到多个临界区,其实是很复杂的工作。 需要分析更多情况下的执行顺序。

共有 人打赏支持
粉丝 115
博文 75
码字总数 138343
×
秋雨霏霏
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: