我正在学习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 ~
我对这个输出感到困惑,因为左边的尺寸是预期的,但尺寸不合适。我没想到会在右侧显示重复值。我该如何解决?有人帮忙吗?
答案 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方法。这很可能是问题所在:
t1
为ArrayBlockingQueue
提供了一个元素。 i = 0
p2
立即从队列中获取元素。 i = 0
t2
甚至在ArrayBlockingQueue
有机会致电t1
之前为i++
提供了一个元素。 i=0
。p2
立即从队列中获取元素。 i = 0
。 从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)请注意,此代码不会根据消息编号确保正确的插入顺序。它只能确保没有重复!如果需要正确的广告订单,则必须使用完全不同的方法,而不必使用AtomicInteger
和ArrayBlockingQueue
。而是使用非线程安全的队列并使用好的旧synchronized
来增加和插入原子:
while (true) {
synchronized(queue) {
if (i++ >= 500) break;
queue.put(... + i + ...);
}
}
以上代码效率不高。几乎整个代码都是同步的。没有真正的并行执行,并发也没用。
您可以以类似的方式更改j
。
答案 1 :(得分:0)
使变量volatile不能保护它免受竞争条件的影响,它只是表明变量绑定到的对象可能会发生变异以防止编译器优化,因此在您的情况下不需要它,因为I&amp; j在同一个线程(循环线程)中被修改。要克服竞争条件,您需要通过同步使i++
和j++
动作原子化,例如将它们定义为原子整数并使用incrementAndGet()
。