竞态条件和临界区

本文翻译自http://tutorials.jenkov.com/java-concurrency/race-conditions-and-critical-sections.html,机翻加人工校正,仅供学习交流。

竞态条件和临界区

竞态条件是可能发生在临界区段内的并发问题。临界区是在多个线程执行的情况下,线程的执行顺序会对临界区访问的结果产生影响的一段代码。多个线程执行一个临界区的结果
多个线程执行一个临界段的结果可能不同,这取决于线程的顺序执行,临界区包含竞态条件。竞态条件这个术语就来源于线程在临界区快速运行,而且竞争的结果会影响执行临界区的结果。这听起来可能有点复杂,所以我将在下面的章节中详细阐述竞态条件和临界区。

竞态条件教程视频

如果你喜欢视频,我这里有本教程的视频版本:Java多线程中的竞争条件。
https://www.youtube.com/watch?v=RMR75VzYoos&list=PLL8woMHwr36EDxjUoCzboZjedsnhLP1j4&index=8

竞态条件的两种类型

当两个或多个线程读写同一个变量时,就会出现竞争条件,根据这两种模式之一:

  1. 读-修-改写
  2. 检查-运行
    读-修改-写模式意味着两个或多个线程首先读取给定的变量,然后修改它的值并将其写回变量。这会引起问题,新值必须以某种方式依赖于以前的值,可能出现的问题是,如果两个线程读取值(到CPU寄存器),然后修改值(在CPU寄存器中),然后把值写回。稍后将更详细地解释这种情况。
    检查-运行意味着两个或多个线程检查给定的条件,例如,如果一个Map包含一个给定的值,然后根据这些信息采取行动,例如从Map中获取值。如果两个线程同时检查Map的给定值,查看该值是否存在,然后两个线程都尝试取(删除)那个值,然而,只有一个线程可以实际操作该值。另一个线程将返回一个空值。如果使用Queue而不是Map,也可能发生这种情况。

读-修改-写临界区

如上所述,读-修改-写临界区可能会导致竞争条件,在本节中,我将进一步了解为什么会这样。如果被多个线程同时执行,一个读-修改-写临界段的Java代码可能会失败:

 public class Counter {

     protected long count = 0;

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

假设有两个线程,A和B在Counter类的同一个实例上执行add方法。没有办法知道操作系统何时在两个线程之间切换。add()方法中的代码不是由Java虚拟机作为单个原子指令执行的。相反,它是作为一组更小的指令执行的,类似于:

  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 
   A:  添加值3到寄存器
   A:  写寄存器的值3到内存,this.count等于3

这两个线程需要将值2和3添加到计数器。因此当两个线程执行完毕后值应该是5。但是两个线程交替执行,结果最终是不一致的。
在上述的执行顺序,所有的线程都是从内存中读取的值都是0。它们添加各自的值2和3到count中,然后将结果写回给内存。this.count最终值是最后一个线程写给内存的,而不是结果5。上面的例子中可能是A线程,也可能是B线程。

读-修改-写临界区的竞态条件

前面示例中的add()方法中的代码包含一个临界部分。当多个线程执行此临界段时,就会出现竞争条件。
更正式地说,两个线程争夺同一资源的情况,如果访问资源的顺序是重要的,称为竞态条件。导致竞态条件的代码段称为临界区。

check-then-act临界区

如上所述,check-then-act临界区也可能导致竞态条件。如果两个线程检查相同的条件,根据条件执行,条件的改变会导致竞态条件。如果两个线程同时检查该条件,然后一个线程继续并改变条件,这可能会导致其他线程在该条件下的行为不正确。为了说明check-then-act临界部分如何导致竞态条件,看看下面的例子:

public class CheckThenActExample {

    public void checkThenAct(Map<String, String> sharedMap) {
        if(sharedMap.containsKey("key")){
            String val = sharedMap.remove("key");
            if(val == null) {
                System.out.println("Value for 'key' was null");
            }
        } else {
            sharedMap.put("key", "value");
        }
    }
}

如果两个或多个线程在同一个CheckThenActExample对象上调用checkThenAct()方法,当两个或多个线程同时执行if-语句时,将sharedMap.containsKey(“key”)赋值为true,然后移到if-语句的主体代码块中。多个线程可能会尝试删除该键,为键“key”存储的值,但实际上只有一个人能做到。因为另一个线程已经删除了键值对,剩下的会返回一个空值。

防止竞态条件

为了防止竞态条件的发生,必须确保临界区是以原子性执行。这意味着一旦有一个线程在执行它,在第一个线程离开临界区之前,没有其他线程可以执行它。
通过在临界区进行适当的线程同步,可以避免竞争条件。线程同步可以使用一个同步的Java代码块来实现。线程同步也可以使用其他同步构造来实现,例如锁或原子变量java.util.concurrent. atomicinteger。

临界区吞吐量

对于较小的临界区,使整个临界区成为一个同步块可以工作。对于大的临界区,将临界区分成更小的临界区可能是有益的,允许多个线程执行每个较小的临界区。这可能会减少对共享资源的争用,增加总临界段的吞吐量。下面是一个非常简单的Java代码示例来说明我的意思:

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()方法是如何向两个不同的sum成员变量添加值的。为了防止竞争条件,求和是在Java同步块中执行的。通过这个实现,只有一个线程可以在同一时间执行求和。
由于两个和变量相互独立,你可以把它们的总和分成两个独立的同步块,就像这样:

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()方法所需的等待时间就会减少。
当然,这个例子非常简单。在现实生活中共享资源,临界区的分解可能要复杂得多,需要对执行顺序的可能性进行更多分析。

下一节:线程安全和共享资源
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值