关于 Atomic boolean 在并发访问完成时的奇怪行为的问题

And*_*nko 6 java concurrency

我得到了这样的代码:

public class ConcurrencyCheck {
    private volatile static AtomicBoolean top=new AtomicBoolean(false);
    private  int i=0;

    public class Toppler extends Thread{
        private final boolean bool;

        public Toppler(boolean myBool,String name) {
            super(name);
            bool=myBool;
        }

        @Override
        public void run() {
            while(!isInterrupted()){

                i++;
                synchronized (top) {
                    if (top.get() == bool) top.set(!top.get());
                    System.err.println(super.getName() + "  "+ bool +"->" + top + ". i is " + i);
                }

                try {
                    sleep(100);
                } catch (InterruptedException e) {
                    break;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ConcurrencyCheck cc = new ConcurrencyCheck();
        Thread t1= cc.new Toppler(true,"thread1");
        Thread t2= cc.new Toppler(false,"thread2");
        Thread t3= cc.new Toppler(true,"thread3");
        Thread t4= cc.new Toppler(false,"thread4");
        Thread t5= cc.new Toppler(true,"thread5");
        Thread t6= cc.new Toppler(false,"thread6");
        Thread t7= cc.new Toppler(true,"thread7");
        Thread t8= cc.new Toppler(false,"thread8");
        t1.start();
        ...
        t8.start();
        sleep(950);
        t1.interrupt();
        ...
        t8.interrupt();
    }
}
Run Code Online (Sandbox Code Playgroud)

它旨在检查 AtomicBoolean 的工作方式。Toppler 类是一个线程,它会定期推翻 Atomic 布尔值。推翻布尔值的代码块是同步的。正如我猜测的那样,每个输出行都必须推翻“top”变量的值,因此输出必须是“true->false false->true true->false...”。但出于某种原因,有时我可以看到这样的输出:

thread1  true->false. i is 1
thread8  false->true. i is 8
thread7  true->false. i is 8
thread4  false->true. i is 8
thread6  false->true. i is 8
thread3  true->false. i is 8
thread5  true->false. i is 8
thread2  false->true. i is 8
thread1  true->false. i is 9
thread8  false->true. i is 10
thread7  true->false. i is 11
thread4  false->true. i is 12
thread6  false->true. i is 13
thread3  true->false. i is 14
Run Code Online (Sandbox Code Playgroud)

问题是:为什么可能出现两个随后的 false->true 或 true->false 输出行?

Bas*_*que 2

太长了;博士

\n

你问:

\n
\n

问题是:为什么两个后续的 false->true 或 true->false 输出行是可能的?

\n
\n

两个原因:

\n
    \n
  • 您不能期望线程以任何特定顺序运行。JVM 和主机操作系统按照自己的想法安排线程在 CPU 核心上的执行时间,其方式是我们无法预测的。
  • \n
  • System.err.println我怀疑对之类的调用System.out.println并不总是按时间顺序出现在控制台上。
  • \n
\n

\xe2\x9e\xa5 如果您想要协调线程之间的活动,例如以特定模式交替它们的工作,则必须执行其他步骤。

\n

细节

\n

我不太清楚你的实验代码的目标。显然,每个任务都是用目标布尔值构建的。如果该任务找到当前标志变量top与我们的目标布尔值匹配,那么我们就会翻转top到相反的状态。

\n

至于线程按 1、8、7、4 等编号顺序交错,这是可以预料的。无法预测线程的执行顺序。接下来安排哪个线程在 CPU 核心上执行以及执行多长时间取决于 JVM 和主机操作系统,具体取决于它们的算法和当前运行时条件。永远不要期望一组线程按特定顺序运行。

\n

您的代码有几个问题。

\n

一是您正在增加一个没有并发保护的共享int变量。i您将该intvar 声明为类成员ConcurrencyCheck,然后i++在每个线程中调用。该代码递增i不是线程安全的,跨线程共享可变资源。这就是为什么你看到那里的价值观失败的原因。您应该使用 来AtomicInteger增加计数器的目的。

\n

关于synchronized (top) {,无需将该代码标记synchronized为您当前编写的代码。AtomicBoolean使用而不是仅仅使用的原因Boolean是为了自动线程安全,从而消除对synchronized. 然而,您试图同时做一些其他事情:增加计数器,并用当前值报告您的进度。因此,出于这个原因,要将多个其他操作以原子方式组合在一起,您确实需要 a synchronized,但您不需要简单地单独synchronized使用a 。AtomicBoolean

\n

关于if (top.get() == bool) top.set(!top.get());,您应该使用其中一种复杂的方法以AtomicBoolean原子方式执行获取和设置类型的操作。AtomicBoolean这就是使此类操作原子化的工作。具体来说,我相信您希望AtomicBoolean#compareAndExchange在找到预期值时设置一个新值,然后返回找到的值。

\n

您用于报告结果的消息字符串令人困惑。我会写一些更像这样的东西,其中你的boolvar 被重命名为expectedValueJavadoc for 的每个术语compareAndExchange。警告:我不是这方面的专家,因此请验证我的逻辑并验证我对该 Javadoc 的理解。

\n
boolean witnessValue = top.compareAndExchange( this.expectedValue , ! this.expectedValue );\nString msg = "Thread ID # " + Thread.currentThread().getId() + " expected: " + this.expectedValue + ", found: " + witnessValue + ", ended with top being: " + witnessValue + ". countAttempts: " + countAttempts + "." + " Now: " + Instant.now();\n
Run Code Online (Sandbox Code Playgroud)\n

关于private volatile static AtomicBoolean top,这里不需要标记AtomicBooleanvolatile。通过初始化声明的位置,您可以确保已经AtomicBoolean分配了一个对象。您永远不会用另一个对象替换该对象,因此任何线程都不可能出现 CPU 核心缓存具有不同值的可见性问题。您应该标记该字段,final以防止错误地换出另一个对象。

\n

另一个问题是,在现代 Java 中,我们很少需要Thread直接处理类。Java 5 中添加了执行器服务框架,以减轻我们处理线程的繁琐工作。将您的任务定义为Runnable( 或Callable),并将实例提交给ExecutorService您选择的实现(请参阅Executors类)。

\n

不要期望控制台输出按时间顺序排列。至于 上的输出顺序System.err,我想System.out它不是时间顺序排列的。我不知道这是由于在括号内生成的消息内容println与实际传递给println方法之间的线程被挂起,还是由于内部缓冲问题,但我可以告诉你,我经常看到一系列控制台上未按println时间顺序出现的呼叫。我建议 (a) 始终包含对或 的调用,以及 (b) 在线程安全的.Instant.nowSystem.nanoTimeList

\n

  • 使用“if (top.get() == bool) top.set(!top.get());”真的让“synchronized(top)”变得不必要了吗?我同意它可以更优雅地解决,例如使用“compareAndSet()”,但考虑到OP的代码,我想说,如果没有额外的同步,多个线程可以同时进入if语句的主体 - 或者我错过了什么? (2认同)