欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  Java

Java 竞态条件和临界段

程序员文章站 2022-04-14 17:27:31
...
一个竞态条件是一个特殊的条件,可能发生在一个临界部分的内部(critical section)。一个临界部分是一段正在被多线程执行的代码,以及线程执行的顺序对于临界部分并发执行的结果产生影响。

当多线程执行一个临界段的结果依赖线程执行的顺序可能是不同的,这个临界段包含一个竞态条件。这个竞态条件的词条源于这个线程正在竞速通过这个临界段的暗喻,并且这个竞争的结果影响着执行这个临界段的结果。

这个可能听起来有点复杂,以至于我将会在下面的部分详细阐述关于竞态条件和临界段。


临界段(Critical Sections)

在相同的应用内部运行不止一个线程不会被他自己引起问题。当多个线程访问相同的资源问题就会出现。例如相同的内存(变量,数组,或者对象),系统(数据库,web服务)或者文件。

事实上,如果一个或者多个线程写这些资源的时候问题会出现。让多个线程读取相同的资源是安全的,只要资源不会改变。

这里有一个例子,如果多个线程同事执行可能会失败:


 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. 增加value值到寄存器。

  3. 将寄存器中的值写回内存。

观察线程A和B混合执行会发生什么:

       this.count = 0;

   A:  Reads this.count into a register (0)
   B:  Reads this.count into a register (0)
   B:  Adds value 2 to register
   B:  Writes register value (2) back to memory. this.count now equals 2
   A:  Adds value 3 to register
   A:  Writes register value (3) back to memory. this.count now equals 3


这两个线程想添加2和3到counter中。因此这两个线程执行完之后的值应该是5。然而,因为这两个线程执行时交叉的,因此结果以不同而结束。

在上面提到的执行顺序的例子中,两个线程都从内存中读取到0这个值。然后他们添加他们各自的值,2和3到那个值中去,然后把这个结果写回到内存中。代替5,在this.count中留下的值将会是最后的那个线程写给他的那个值。在上面的例子中是线程A,但是他也可能是线程B。

在临界段中的竞态条件

在上面的那个例子中的add方法的代码中包含了一个临界段。当多个线程执行这个临界段的时候,竞态条件就会发生了。

更正式的讲,两个线程的这种情形竞争着相同的资源,资源被访问的顺序是重要的,它被称为竞态条件。一个代码部分导致竞态条件就会被称之为临界段。

预防竞态条件

为了防止竞态条件的发生,你必须确保被执行的临界段作为一个原子指令被执行。那就意味着一旦一个单独的线程在执行它,其他的线程就不能执行它直到第一个线程已经离开了这个临界段。

竞态条件可以通过在临界段中使用线程同步的方式去避免。线程同步可以使用一个Java代码的同步锁去获取。线程同步也可以使用其他的同步概念去获取,像锁或者像java.util.concurrent.atomic.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同步锁。伴随着这个实现,同时只能有一个线程可以执行这个求和。

然而,因为这两个sum变量是相互独立的,你可以把他们分离成两个分离的同步锁,像这样:

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


注意,两个线程可以同时执行这个add方法。一个线程获取到第一个同步锁,另外一个线程获取第二个同步锁。这种方式,线程之间将会等待的更少时间。

当然,这个例子是非常简单的。在真正的生活中,临界段分离的共享资源可能会更加复杂的,并且需要更多的执行顺序可能性的分析。


以上就是Java 竞态条件和临界段的内容,更多相关内容请关注PHP中文网(www.php.cn)!