使用volatile来避免竞争条件

时间:2015-04-24 05:42:15

标签: java multithreading

我正在学习java.util.concurrent。我写了一些像这样的代码:

import java.util.concurrent.ArrayBlockingQueue;

public class JUCArrayBlockingQueue {

private static ArrayBlockingQueue<String> abq = new ArrayBlockingQueue<String>(1);
private static volatile int i = 0;
private static volatile int j = 0;

public static void main(String[] args) {
    new Pop("t1").start();
    new Pop("t2").start();
    new Push("p1").start();
    new Push("p2").start();
}

static class Pop extends Thread {
    public Pop(String name) {
        super(name);
    }

    @Override
    public void run() {

        String str;
        try {

            while (++j < 500) {
                str = abq.take();
                System.out.println(j + ">>"
                        + Thread.currentThread().getName() + " take: "
                        + str);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

static class Push extends Thread {
    public Push(String name) {
        super(name);
    }

    @Override
    public void run() {

        while (i < 500) {
            if (abq.offer(Thread.currentThread().getName() + " t" + i
                    + "  ~")) {
                i++;
            }
            ;
        }
    }
}
}

结果是这样的:

488>>t1 take: p2 t486  ~
489>>t2 take: p2 t487  ~
490>>t2 take: p1 t488  ~
491>>t1 take: p1 t489  ~
492>>t1 take: p2 t490  ~
493>>t1 take: p1 t490  ~
494>>t2 take: p1 t492  ~
495>>t2 take: p1 t493  ~
496>>t1 take: p1 t494  ~
497>>t2 take: p1 t495  ~
498>>t1 take: p1 t496  ~
499>>t2 take: p1 t497  ~
500>>t1 take: p2 t498  ~

我对这个输出感到困惑,因为左边的尺寸是预期的,但尺寸不合适。我没想到会在右侧显示重复值。我该如何解决?有人帮忙吗?

2 个答案:

答案 0 :(得分:3)

让我们看一下打印输出右侧的代码片段:

if (abq.offer(Thread.currentThread().getName() + " t" + i
                    + "  ~")) {
       i++;
 }

让我们放大if语句中的条件:

abq.offer(Thread.currentThread().getName() + " t" + i
                        + "  ~")

所以你正在使用offer方法。我们来看看提供方法的java doc:

  

公共布尔报价(E e)

     

将指定元素插入尾部   如果可以立即这样做而不超过此队列   队列的容量,成功时返回true,如果是,则返回false   队列已满。该方法通常优于方法add(E),   只能通过抛出异常而无法插入元素。

致电优惠似乎不是阻止电话。这意味着多个线程可以同时调用offer方法。这很可能是问题所在:

  1. t1ArrayBlockingQueue提供了一个元素。 i = 0
  2. p2立即从队列中获取元素。 i = 0
  3. t2甚至在ArrayBlockingQueue有机会致电t1之前为i++提供了一个元素。 i=0
  4. p2立即从队列中获取元素。 i = 0
  5. 从2)和4)可以看出,即使在调用i之前,两个线程也有可能读取i++的值,因此看到i的相同值

    那么你如何解决这个问题呢?正如其他人所建议的那样,您可以使用AtomicInteger,如下所述:

    volatile关键字将确保当读取 or 对多个线程共享的变量(内存位置)进行写入时,保证每个线程都会看到最新的变量的值。请注意强调 or 。当您说i++j++时,您正在一起阅读 and 。这不是原子操作,volatile关键字不会阻止线程看到不一致的值。

    更改

    private static volatile int i = 0; 
    

    致:

    private static final AtomicInteger i = new AtomicInteger();
    

    AtomicInteger提供原子增量操作incrementAndGet()(= ++i)和getAndIncrement()(= i++),可以在这种情况下使用:

    int nr=0; // local holder for message number
    while ((nr = i.getAndIncrement()) < 500) {
        abq.put(Thread.currentThread().getName() + " t" + nr + "  ~");
    }
    

    (1)注意局部变量nr的用法!它确保将增量操作的结果用作put(...)中的消息号。如果我们直接使用i而不是nr,其他线程可能会在此期间增加i,我们会有不同的消息编号或重复的消息编号!

    (2)另请注意,offer(...)已替换为put(...)。由于您的队列是有界的(仅限 1 元素),插入操作应该在容量不足时阻止。 (offer(...)会在这种情况下立即返回false。)

    (3)请注意,此代码不会根据消息编号确保正确的插入顺序。它只能确保没有重复!如果需要正确的广告订单,则必须使用完全不同的方法,而不必使用AtomicIntegerArrayBlockingQueue。而是使用非线程安全的队列并使用好的旧synchronized来增加和插入原子:

    while (true) {
        synchronized(queue) {
            if (i++ >= 500) break;
            queue.put(... + i + ...);
        }
    }
    

    以上代码效率不高。几乎整个代码都是同步的。没有真正的并行执行,并发也没用。

    您可以以类似的方式更改j

答案 1 :(得分:0)

使变量volatile不能保护它免受竞争条件的影响,它只是表明变量绑定到的对象可能会发生变异以防止编译器优化,因此在您的情况下不需要它,因为I&amp; j在同一个线程(循环线程)中被修改。要克服竞争条件,您需要通过同步使i++j++动作原子化,例如将它们定义为原子整数并使用incrementAndGet()