Java并发编程(Java Concurrency)(8)- 竞争与临界区(Race Conditions and Critical Sections)

原文链接:http://tutorials.jenkov.com/java-concurrency/race-conditions-and-critical-sections.html

摘要:这是翻译自一个大概30个小节的关于Java并发编程的入门级教程,原作者Jakob Jenkov,译者Zhenning Lang,转载请注明出处,thanks and have a good time here~~~(希望自己不要留坑)

“竞争”是可能发生在“临界区”内的一种特使情况。临界区的含义是:如果多个线程访问一个代码段,同时这些线程的执行顺序将影响总得执行效果,那么这个代码端就叫做临界区。

反言之,如果临界区的执行结果受到多线程执行顺序的影响,那么就说存在竞争。竞争比喻了不同的线程互相争抢临界区的代码,并且争抢的结果也将影响临界区的运行结果。

如果上面过于抽象难懂,下面的例子将帮助理解竞争与临界区的含义。

1 临界区

当多个线程执行其内部代码的时候,其本身不会引起任何问题;引起问题的原因是对共享资源的使用。例如共用的内存(变量、数组和对象等)、系统资源(数据库、网络服务等)或者是文件。

跟进一步,事实上只有写共享资源操作才会引发问题。只要内容不变,让多个线程读取相同的资源是安全的。

下面是一个临界区的 Java 代码示例,例子中如果多个线程同时运行这段代码就会发生失败:

public class Counter {

    protected long count = 0;

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

现在假设两个线程 A 和 B 在对相同的 Counter 类的实例进行 add 操作,没有办法知道操作系统何时在两个线程间切换。Java 虚拟机无法像对待单一“原子”一样处理 add() 函数中的代码(就是没办法一下子执行完代码中的全部内容,而是按步骤执行的),事实上 add() 中的代码好像被一系列更小的指令执行:

  1. 将 this.count 从内存中读到寄存器中
  2. 将 value 与寄存器中的数据相加并存入寄存器
  3. 将寄存器数据写入内存

那么现在考虑如下的线程 A 和 B 的执行情况:

this.count = 0;
B: 将 this.count 读入一个寄存器 (0) (
A: 将 this.count 读入一个寄存器 (0)
B: 将寄存器中的数据 +2 (2)
B: 将寄存器中的数据 (2) 写回内存,此时 this.count 等于 2
A: 将寄存器中的数据 +3 (3)
A: 将寄存器中的数据 (3) 写回内存,此时 this.count 等于 3

这两个线程的本来目的是将 2 和 3 加到 counter 上,所以期待的结果应该是 5。然而实际运行中线程发生了交错,导致结果与预期不同。上例中,两个线程都将 0 从内存中读出并且加上了 2 和 3,然后将其写回内存。所以最后的结果取决于谁最后将结果写回内存(2 和 3 都有可能)。

2 临界区中的竞争

前面例子的 add() 方法中存在着临界区,所以当多线程执行临界区代码的时候,竞争发生了。

对于临界区和竞争更正规的定义是:如果两个线程争夺共享资源,并且资源被获取的时机(顺序)将对结果产生影响,这种情况叫做竞争;引发竞争的代码段被称作邻接区

3 阻止竞争的发生

为了阻止竞争的发生,临界区的代码必须以“原子”的模式被执行 —— 即一旦一个线程开始执行临界区的代码,直到其执行完临界区,其他的线程就无法执行临界区。

通过对临界区代码设置合理的线程同步(thread synchronization)机制,竞争就可以被阻止。而 Java 中的线程同步的一种实现方式是同步代码块(a synchronized block of Java code)。其他的实现途径还有锁(locks)、原子变量(atomic variables)(例如 java.util.concurrent.atomic.AtomicInteger)。

4 临界区的吞吐量

相比于定义分散的小的临界区,将所有代码定义成一个大的临界区也可以让代码正常运作。但将大的临界区拆封成小的临界区将带来更多的好处,因为这样的话不同的线程可以同时执行这些小的临界区代码,从而减少了其相互之间对资源的争夺,增加了整个临界区吞吐量。

为了解释这一点,下面是一个非常简单的示例:

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() 函数尝试着将 val1 和 val2 加到 sum1 和 sum2 上。同时为了防止竞争的发生,这段代码被放在了 Java 同步代码块中,这样同一时间只能有一个线程执行 add() 中的具体加操作。

然而,由于例子中的 sum1 和 sum2 变量是相互独立,完全可以分开在两个同步块中执行二者的加操作,如下:

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() 方法中的代码。一个线程执行 sum1 的加操作,而另一个线程执行 sum2 的加操作。由于两个同步块被两个不同的对象同步(this.sum1Lock 和 this.sum2Lock),这样一来执行 add() 方法的多个线程相互等待的时间就被减少了。

当然上例是非常简单的,而实际中对于临界区的拆分可能更加复杂,并且需要仔细考量线程执行顺序以及其结果的可能。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值