Java中的竞争条件及示例

时间:2020-01-09 10:35:07  来源:igfitidea点击:

当两个或者多个线程尝试访问共享资源时,Java中的竞争条件可能会以Java之类的多线程语言出现。如果所有线程都只是读取没有问题的共享对象,但是由于竞争条件,修改或者写入值可能会导致错误的结果。

在多线程环境中,执行几个步骤后的线程可能会被另一个线程抢占。这可能会使共享数据处于不一致状态。

例如,增加一个简单的任务-counter ++;
这个增加计数器的简单任务实际上包括三个步骤

  • 读取计数器变量的值。
  • 将值增加1.
  • 存储计数器变量的值。

如果有两个线程共享此变量,则可能发生以下情况:

int counter = 0;
counter = counter + 1; // Thread 1
counter = counter + 1; // Thread 2 started before thread 1 could save the new 
                      //value of counter, so Thread 2 also got the initial value of counter as 0.
store counter value // Thread 1
store counter value // Thread 2

因此,由于线程交错,最终将计数器值设为1而不是正确的值2. 这就是竞争条件可以在多线程环境中对共享对象执行的操作。

由于比赛条件而导致的错误情况

由于存在竞争条件,执行线程可能会读取共享对象的陈旧值,这可能会导致以下任何情况。

  • 如果线程必须根据变量的值执行某些逻辑。因为线程可能最终读取了错误的值,所以它可能无法按照预期的方式运行。这种情况称为先检查后竞争情况。
  • 线程必须读取,修改和存储新值。再次由于竞争条件,线程可能最终读取和修改过时的值。这种情况称为读取-修改-写入竞争条件。

Java中竞争条件的示例

这是一个简单的示例,其中共享整数变量递增并显示值。创建十个线程,每个线程递增,然后显示变量的值。预期的行为是,每个线程应获得1-9之间的唯一值。

public class RaceConditionDemo {
  int counter = 0;
  public  void incrementCounter(){
    try {
      Thread.sleep(100);
    } catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    counter++;
  }
  public int getCounter(){
    return counter;
  }
  public static void main(String[] args) {
    RaceConditionDemo rc = new RaceConditionDemo();
    for(int i = 0; i < 10; i++){
      new Thread(new Runnable() {			
        @Override
        public void run() {
          rc.incrementCounter();
          System.out.println("value for " + Thread.currentThread().getName() + " - " + rc.getCounter());
        }
      }).start();
    }	
  }
}

输出:

value for Thread-0 - 1
value for Thread-2 - 2
value for Thread-1 - 3
value for Thread-4 - 4
value for Thread-5 - 6
value for Thread-3 - 6
value for Thread-6 - 6
value for Thread-9 - 8
value for Thread-8 - 9
value for Thread-7 – 8

在运行之一中,输出如上所述(请注意输出可能会有所不同)。如我们所见,线程5、3和6具有相同的值6,线程7和9也具有相同的值8.

避免Java中的竞争条件

现在,当我们知道什么是竞争条件并看到一个示例时,交织线程读取共享对象的相同值。这给我们带来了一个问题,即如何避免Java中的竞争条件。

显然,我们需要限制对关键部分的访问(修改共享资源的代码)。在Java中,这就是synced关键字的作用;同步对共享资源的访问。使用同步可确保原子操作作为单个操作执行而没有线程干扰。

在上面显示的示例中,同步方法调用应避免争用条件。

public class RaceConditionDemo {
  int counter = 0;
  public  void incrementCounter(){
    try {
      Thread.sleep(100);
    } catch (InterruptedException e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    counter++;
  }
  public int getCounter(){
    return counter;
  }
  public static void main(String[] args) {
    RaceConditionDemo rc = new RaceConditionDemo();
    for(int i = 0; i < 10; i++){
      new Thread(new Runnable() {			
        @Override
        public void run() {
          synchronized(rc){
            rc.incrementCounter();
            System.out.println("value for " + Thread.currentThread().getName() + " - " + rc.getCounter());
          }
        }
      }).start();
    }	
  }
}

输出:

value for Thread-0 - 1
value for Thread-8 - 2
value for Thread-7 - 3
value for Thread-9 - 4
value for Thread-6 - 5
value for Thread-4 - 6
value for Thread-5 - 7
value for Thread-3 - 8
value for Thread-2 - 9
value for Thread-1 – 10

如我们所见,每个线程都有一个唯一的值。