使用易失性以避免争用条件



我正在学习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  ~

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

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

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

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

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

因此,您正在使用报价方法。让我们看一下 offer 方法的 java 文档:

公开布尔报价(E e)

在的尾部插入指定的元素 此队列(如果可以立即执行此操作而不会超过 队列的容量,成功时返回 true,如果返回 false,则返回 false。 队列已满。这种方法通常比方法add(E)更可取, 它只能通过抛出异常来插入元素。

呼叫报价似乎不是阻止呼叫。这意味着多个线程可以同时调用 offer 方法。这是最有可能的问题:

  1. t1ArrayBlockingQueue提供了一个元素。i = 0
  2. p2立即从队列中获取元素。i = 0
  3. t2甚至在t1有机会打电话给i++之前就为ArrayBlockingQueue提供了一个元素。i=0.
  4. 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) 请注意,此代码不能确保根据消息编号正确的广告顺序。它只确保没有重复项!如果需要正确的广告顺序,则必须使用完全不同的方法,而无需AtomicIntegerArrayBlockingQueue。相反,请使用非线程安全队列并使用良好的旧synchronized使增量和插入原子化:

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

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

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

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

最新更新