Java多线程:意外的结果

时间:2017-06-03 19:12:27

标签: java multithreading

我正在开发企业应用程序。我在多线程环境中运行应用程序时遇到了一些问题。我正在编写一个程序,其中有一个变量,其值以非常快的速率更新(递增)(例如10000次更新/ persecond)。循环运行一定的迭代,变量的值递增并存储在HashMap中。一旦循环终止并且值打印HashMap中的变量。我得到了变量的意外价值。

这是演示程序(请阅读评论以便更好地理解):

class test implements Runnable {

    static ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
    static AtomicInteger value_to_be_incremented_stored = new AtomicInteger(0); // variable whose value to be updated
    static AtomicInteger i = new AtomicInteger(0);  // this runs the loop

    @Override
    public void run() {

        for (i.set(0); i.get() < 100000; i.incrementAndGet()) {
            /*
                This loop should run 100000 times and when loop terminates according to me value of variable 
                "value_to_be_incremented_stored" should be 100000 as its value is incremented 
                100000 times the loop also runs 100000 times. 
            */
            System.out.println("Thread > " + Thread.currentThread() + "  " + value_to_be_incremented_stored.incrementAndGet());
            map.put("TC", value_to_be_incremented_stored.intValue());
        }

        System.out.println("Output by Thread  " + Thread.currentThread() + "     " + map.toString());
    }

    public static void main(String[] args) {

        test t1 = new test();
        Thread thread1 = new Thread(t1);
        thread1.setName("Thread 1");

        Thread thread2 = new Thread(t1);
        thread2.setName("Thread 2");

        Thread thread3 = new Thread(t1);
        thread3.setName("Thread 3");

        Thread thread4 = new Thread(t1);
        thread4.setName("Thread 4");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();

    }
}

输出(因人而异):

My output

问题: 我运行循环100000次(i.get() < 100000)然后变量value_to_be_incremented_stored的值变得超过100000。

3 个答案:

答案 0 :(得分:6)

我发现了三个缺陷。在比较循环计数器的点与增加循环计数器的位置之间的for循环中存在竞争条件。您应该在一个步骤中执行此操作以获得原子操作:

for ( ; i.incrementAndGet() < 100000;  ) {

另一种情况是,您的计数器增量与放置在地图之间也存在竞争条件。即使你将它们串联递增,任何线程都可以在内部具有不同的值(它在循环中的不同点),并且它可以在全局映射中放置先前的值。你需要这里的原子性来确保你增加的值是你在循环中放置的值。

synchronized( lock ) { 
    value_to_be_incremented_stored.incrementAndGet();
    map.put("TC", value_to_be_incremented_stored.intValue());
}

最后由于某种原因,<比较为我产生了99999的值。我不得不使用<=来修复它。

(正如我们在评论中所讨论的那样,在每个i.set(0)循环开始时设置for并不是出于相当明显的原因而工作。我想有四个缺陷。)

class ThreadTestX implements Runnable {

    static ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
    static AtomicInteger value_to_be_incremented_stored = new AtomicInteger(0); // variable whose value to be updated
    static AtomicInteger i = new AtomicInteger(0);  // this runs the loop
    static final Object lock = new Object();

    @Override
    public void run() {

        for ( ; i.incrementAndGet() <= 100000;  ) {
            /*
                This loop should run 100000 times and when loop terminates according to me value of variable 
                "value_to_be_incremented_stored" should be 100000 as its value is incremented 
                100000 times the loop also runs 100000 times. 
            */
            synchronized( lock ) {
                value_to_be_incremented_stored.incrementAndGet();
    //            System.out.println("Thread > " + Thread.currentThread() + 
    //                 "  " + value_to_be_incremented_stored.get());
                map.put("TC", value_to_be_incremented_stored.intValue());
            }
        }

        System.out.println("Output by Thread  " + Thread.currentThread() 
                + "     " + map.toString());
    }

    public static void main(String[] args) {

        ThreadTestX t1 = new ThreadTestX();
        Thread thread1 = new Thread(t1);
        thread1.setName("Thread 1");

        Thread thread2 = new Thread(t1);
        thread2.setName("Thread 2");

        Thread thread3 = new Thread(t1);
        thread3.setName("Thread 3");

        Thread thread4 = new Thread(t1);
        thread4.setName("Thread 4");

        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();

    }
}

输出:

run:
Output by Thread  Thread[Thread 4,5,main]     {TC=100000}
Output by Thread  Thread[Thread 3,5,main]     {TC=100000}
Output by Thread  Thread[Thread 1,5,main]     {TC=100000}
Output by Thread  Thread[Thread 2,5,main]     {TC=100000}
BUILD SUCCESSFUL (total time: 0 seconds)

事后补充:尽管标记正确,但我不确定我是否正确。这里的问题是你试图让三件事情保持同步:循环计数器i,要增加的值和地图。允许在同步块之外执行这些中的任何一个可以邀请它们处于意外状态。我认为以下内容可能更安全:

@Override
public void run() {

    for ( ;;  ) {
        synchronized( lock ) {
            if( i.incrementAndGet() <= 100000 ) {
                value_to_be_incremented_stored.incrementAndGet();
                map.put("TC", value_to_be_incremented_stored.intValue());
            }
            else
                break;
        }
    }
    System.out.println("Output by Thread  " + Thread.currentThread() 
            + "     " + map.toString());
}

这消除了将变量声明为AtomicInteger的需要,但我不知道如何确保他们的值不会因为该循环执行而改变(由于某些其他线程)

答案 1 :(得分:4)

您的“演示程序”同时存在两个缺陷。

缺陷#1是tsolakp指出的i.set( 0 )。你说你修复了这个但你仍然得到了超过100000的值。我也尝试了它,实际上,最终值仍然大于100000。

我修改了程序以便能够创建任意数量的线程,并尝试使用3,10和20个线程。我的最终数字分别为100003,100009和100019。看模式?

在最后一次迭代中,当i的值为99999时,表达式i.get() < 100000对于所有线程都为真,因此所有线程都会再次执行。 i.incrementAndGet()子句直观地位于i.get() < 1000000;旁边,但直到循环结束才会执行。

因此,在最后一次迭代之后,所有线程都有机会再次增加i

答案 2 :(得分:1)

每次新线程进入run方法时,它都会通过调用i通过for循环中的第一个语句将i.set(0)计数重置为零。

更新:修复重置问题后的下一步是逐步执行代码并查看线程的行为方式。 让我们说三个线程进入for循环,而第四个线程增加i。会发生什么事情value_to_be_incremented_stored只会增加3次而i只增加一次。