用于模拟Java线程中的竞争条件的代码

时间:2014-08-06 09:27:44

标签: java multithreading

我是Java多线程的新手。我正在学习种族条件的概念。

基于Oracle文档

http://docs.oracle.com/javase/tutorial/essential/concurrency/interfere.html

我创建了一个示例代码,如下面的

public class CounterTest {

    public static void main(String[] args) {

        Thread thread1 = new Thread(new CounterIncThread());
        thread1.setName("add thread");
        thread1.start();

        Thread thread2 = new Thread(new CounterDecThread());
        thread2.setName("sub thread");
        thread2.start();

        Thread thread3 = new Thread(new CounterIncThread());
        thread3.setName("add thread2");
        thread3.start();
    }

}


class CounterIncThread implements Runnable {
    public void run() {
        SynchronizedCounter counter = new SynchronizedCounter();
        counter.increment();
        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        String threadName =
                Thread.currentThread().getName();
        System.out.println(threadName+ ": "+counter.value());
    }
}

class CounterDecThread implements Runnable {
    public void run() {
        SynchronizedCounter counter = new SynchronizedCounter();
        counter.decrement();
        String threadName =
                Thread.currentThread().getName();
        System.out.println(threadName+ ": "+counter.value());
    }
}

class SynchronizedCounter {
    private int c = 0;

    public  void increment() {
        c++;
    }

    public   void decrement() {
        c--;
    }

    public  int value() {
        return c;
    }

}

该代码未显示任何比赛条件。能帮助我,如何使用上面的代码刺激竞争条件?

由于

4 个答案:

答案 0 :(得分:7)

为了在两个线程之间进行竞争,这两个线程之间必须存在共享状态,并且与该状态的交互(读取和写入)必须在mutualy独占块(aka同步化)之外发生。读取,递增然后写回同步块之外的易失性字段就是一个很好的例子。

例如,请考虑此blog上记录的这种情况。

线程A和B都可以在发生任何修改之前读取计数器。然后他们都增加,然后他们都写。结果将是18,而不是19.因为它已经是19,我们将需要线程B来读取线程A写入计数器后的计数器。哪个,有时可能发生。这就是为什么它被称为种族。

enter image description here

为了可靠地实现这种竞争,请更改上面的测试代码,在线程外部创建计数器,然后通过其构造函数将其传递给它们。

你遇到的第二个问题是重叠操作的窗口是非常好的,并且考虑到启动一个线程,相比之下,很多头部比较大,那么这三个线程在右边重叠的可能性时间很短。因此,为了增加他们的几率,你应该在一个紧密的循环中重复运行。

以下代码演示了上述两个概念。所做的更改是:

  1. 重命名类以使其使用更清晰
  2. 在两个线程之间共享MyCounter的状态
  3. 每个线程内的紧密循环,调用增量1,000,000次
  4. 主线程现在阻塞使用join()等待两个线程完成,这将取代之前的Thread.sleep
  5. MyCounter中的计数器值c现在是不稳定的;这告诉JVM总是向共享内存寻找值,而不是通过在遇到之间将它保持在寄存器内来进行优化。为了让比赛更糟糕,请关注挥发性,看看会发生什么:)
  6. 主循环然后通过打印出计数器的值来完成,该值应为2,000,000。但这不会是由于在不稳定的柜台上进行的比赛。
  7. public class CounterTest {    
        public static void main(String[] args) throws InterruptedException {   
            MyCounter counter = new MyCounter();
    
            Thread thread1 = new Thread(new CounterIncRunnable(counter));
            thread1.setName("add thread");
            thread1.start();
    
            Thread thread2 = new Thread(new CounterIncRunnable(counter));
            thread2.setName("add thread2");
            thread2.start();
    
            thread1.join();
            thread2.join();
    
            System.out.println(counter.value());
        }    
    }
    
    
    class CounterIncRunnable implements Runnable {
        private MyCounter counter;
    
        public CounterIncRunnable(MyCounter counter) {
            this.counter = counter;
        }
    
        public void run() {
            for ( int i=0; i<1000000; i++ ) {
                counter.increment();
            }
        }
    }
    
    
    class MyCounter {
        private volatile int c = 0;
    
        public  void increment() {
            c++;
        }
    
        public   void decrement() {
            c--;
        }
    
        public  int value() {
            return c;
        }    
    }
    

    最后,只是为了好玩;将synchronized同步添加到MyCounter的increment方法,然后重新运行。竞争条件将消失,现在程序将正确打印2000000.这是因为每次增加调用现在只允许一个线程一次进入共享方法。因此,序列化对共享变量c的每次访问,并结束比赛。

答案 1 :(得分:6)

最简单的竞争条件是两个线程使用此模式更新某些共享数据

  read a value
  think for a bit, giving another thread a chance to get in
  increment the value and write it back

所以现在如果你有两个线程在运行,每个线程递增一个初始值为43的计数器,我们期望这个

  A reads value 43
  A thinks
  A increments and writes 44
  B reads value 44
  B thinks
  B increments and writes 45

但这可能会发生,因为&#34;思考窗口&#34;

  A reads value 43
  A thinks
  B reads value (it's still) 43
  B thinks
  B increments 43 to 44 and writes
  A increments 43 to 44 and write
  // the value is now 44, and we expected it to be 45

比赛的关键想法是你会出现意外的不良影响,例如在库存应用程序中,两个线程各自减少库存量,就像上面的例子中我们失去了#34;其中一个减量。

现在您的代码有两个问题:

1)。没有共享值,所以我们没有机会看到任何此类争用

2)。您在一行代码中递增一个整数,因此两个线程碰撞的可能性很小。在模拟比赛时,如上所示,最好将读写分开,然后创建一个机会窗口&#34;通过睡觉模拟思考时间。在多处理器环境中,线程可能真正并行运行,即使单行代码也可能会出现竞争,因为JVM内部会进行读写操作,甚至可能会保留值的缓存。

答案 2 :(得分:1)

您正在对每个线程中的不同对象进行操作,因此没有竞争条件。首先,您需要共享SynchronizedCounter(顺便说一下,这是一个令人困惑的名称)。在每个可运行的中添加counter成员。

CounterIncThread(SynchronizedCounter counter)
{
   this->counter = counter;
}

CounterDecThread(SynchronizedCounter counter)
{
   this->counter = counter;
}

...
SynchronizedCounter counter = new SynchronizedCounter();
Thread thread1 = new Thread(new CounterIncThread(counter));
Thread thread2 = new Thread(new CounterDecThread(counter));
Thread thread3 = new Thread(new CounterIncThread(counter));

另外。您在runnable中只执行一个操作。这可能不足以显示竞争条件。所以循环了很长时间。

for(int i = 0; i < 100000; i++) <-- 100000 is just on the top of my head
{
    counter.increment(); 
}

如果比赛发生,该值不会是操作的总和,在我的情况下我希望它是100000 * 2

更明确地说,运行几次。您可能会获得不同的值

答案 3 :(得分:0)

如果正确显示打印&#34;&#34;,是因为没有太多线程。尝试创建100个线程,您将看到打印未显示排序。如果SynchronizedCounter中的c是静态的,则可以看到竞争条件,因为线程正在读取相同的变量。