【问题标题】:Question about strange behavior of Atomic boolean while concurrent access is done关于并发访问完成时原子布尔值的奇怪行为的问题
【发布时间】:2021-03-30 05:55:39
【问题描述】:

我得到了这样的代码:

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();
    }
}

它旨在检查 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

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

【问题讨论】:

  • 您对“top”和“toppler”这两个词的使用令人困惑。你什么意思?如果该值与任务构造函数的给定布尔值匹配,您的目标是每个任务翻转布尔值吗?
  • 因为线程乱序运行时它没有翻转。 if 条件没有通过,但打印输出令人困惑,假设它会通过。
  • 是的。 Toppler 是一个反布尔值的类,如果它等于在构造 Toppler 类时设置的 'bool' 变量。
  • “我错过了括号” - 这是许多静态代码检查器总是需要使用括号的原因之一。你可以很容易地产生这种情况,所以我建议为你自己(和你的团队)制定一个规则,这样一个块上缺少的括号就会在你的脑海中引发一个危险信号。
  • “我总是在 if 块中使用括号。” - 这是一个好的开始,但我建议您将始终使用它们来防止与其他构造(例如循环)发生相同情况的规则。单表达式 lambda 可能是一个例外,因为无论如何编译器都会强制您为超过 1 个语句添加大括号。

标签: java concurrency


【解决方案1】:

tl;博士

你问:

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

两个原因:

  • 您不能期望线程以任何特定的顺序运行。 JVM 和主机操作系统会随心所欲地为 CPU 内核上的执行时间安排线程,这对我们来说是不可预测的。
  • 我怀疑对System.err.println 的调用(例如System.out.println总是按时间顺序出现在控制台上。

➥ 如果您想协调线程之间的活动,例如以特定模式交替工作,您必须采取额外的步骤。

详情

您的实验代码的目标对我来说不是很清楚。显然,每个任务都是用一个目标布尔值构造的。如果该任务发现当前标志变量top 与我们的目标布尔值匹配,那么我们将top 翻转到相反的位置。

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

您的代码有几个问题。

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

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

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

您用于报告结果的消息字符串令人困惑。我会写一些更像这样的东西,根据compareAndExchange 的Javadoc 术语,您的bool var 被重命名为expectedValue。警告:我不是这里的专家,所以请验证我的逻辑并验证我对那个 Javadoc 的理解。

boolean witnessValue = top.compareAndExchange( this.expectedValue , ! this.expectedValue );
String msg = "Thread ID # " + Thread.currentThread().getId() + " expected: " + this.expectedValue + ", found: " + witnessValue + ", ended with top being: " + witnessValue + ". countAttempts: " + countAttempts + "." + " Now: " + Instant.now();

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

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

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

【讨论】:

  • 使用if (top.get() == bool) top.set(!top.get());真的不需要synchronized(top)吗?我同意它可以更优雅地解决,例如compareAndSet() 但考虑到 OP 的代码,我想说如果没有额外的同步,多个线程可能会同时进入 if 语句的主体 - 还是我错过了什么?
  • @Kayaman 是的,i 共享的,不是吗?在ConcurrencyCheck 上作为成员字段存在,但在各个线程中以i++ 递增。
  • 这没有回答原始问题。为什么即使 if 语句和 println 在同步块中,字符串也会交错?可能这与流缓冲有关,但 System.err 会在 println 上自动刷新,所以我不理解观察到的行为。
  • @ciamej 我相信 8 8 8 8 系列的输出是由于以非线程安全的方式递增。至于1、8、7、4等编号顺序的线程交错,是可以预料的。无法预测线程的执行顺序。接下来调度哪个线程在 CPU 内核上执行,以及执行多长时间,取决于 JVM 和主机操作系统,具体取决于它们的算法和当前运行时条件。永远不要期望一组线程按顺序运行。
  • if(top.compareAndSet(bool, !bool)) System.err.println(super.getName() + " "+ bool +"->" + !bool + ". i is " + i); 更有意义,因为compareAndSet 返回是否发生翻转。然而,重要的是要记住,由于现在翻转和打印语句不受同步保护,即使原始错误不再存在,它们也可能被乱序打印。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-08-28
  • 2021-08-27
  • 2012-06-21
  • 1970-01-01
  • 1970-01-01
  • 2014-06-17
  • 1970-01-01
相关资源
最近更新 更多