Java Puzzler:忙等待线程停止工作

时间:2013-04-23 21:30:34

标签: java multithreading thread-synchronization java-memory-model busy-waiting

这是某种Java Puzzler,我偶然发现并无法解释。也许有人可以吗?

以下程序在短时间内挂起。有时在2次输出后,有时在80次之后,但几乎总是在正确终止之前。如果第一次没有发生,你可能需要运行几次。

public class Main {
    public static void main(String[] args) {
        final WorkerThread[] threads = new WorkerThread[]{ new WorkerThread("Ping!"), new WorkerThread("Pong!") };
        threads[0].start();
        threads[1].start();

        Runnable work = new Runnable() {
            private int counter = 0;
            public void run() {
                System.out.println(counter + " : " + Thread.currentThread().getName());
                threads[counter++ % 2].setWork(this);
                if (counter == 100) {
                    System.exit(1);
                }
            }
        };

        work.run();
    }
}

class WorkerThread extends Thread {
    private Runnable workToDo;

    public WorkerThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        while (true){
            if (workToDo != null) {
                workToDo.run();
                workToDo = null;
            }
        }
    }

    public void setWork(Runnable newWork) {
        this.workToDo = newWork;
    }
}

现在,很明显忙碌的等待循环通常不是一个好主意。但这不是关于改进,而是关于理解正在发生的事情。

由于当WorkerThread.setWork()synchronizedWorkerThread.workToDo字段设置为volatile时,一切都按预期工作,我怀疑是内存问题。

但为什么会这样呢?调试没有用,一旦你开始单步执行,一切都按预期运行。

将不胜感激。

3 个答案:

答案 0 :(得分:2)

  1. 第一个问题是您从Runnable workToDo线程设置main,然后在没有同步的情况下在2个分叉线程中读取它。每当您修改多个主题中的字段时,都应将其标记为volatile或某人synchronized

    private volatile Runnable workToDo;
    
  2. 此外,由于多个线程正在执行counter++,因此这也需要synchronized。我建议使用AtomicInteger

    private AtomicInteger counter = new AtomicInteger(0);
    ...
    threads[counter.incrementAndGet() % 2].setWork(this);
    
  3. 但我认为真正的问题可能是竞争条件之一。两个线程都可以将workToDo设置为Runnable,然后让它们返回并将其设置回null,这样它们就会永远旋转。我不知道如何解决这个问题。

    1. threads[0] has it's `workToDo` set to the runnable.  It calls `run()`.
    2. at the same time threads[1] also calls `run()`.
    3. threads[0] sets the `workToDo` on itself and threads[1] to be the runnable.
    4. at the same time threads[1] does the same thing.
    5. threads[0] returns from the `run()` method and sets `workToDo` to be `null`.
    6. threads[1] returns from the `run()` method and sets `workToDo` to be `null`.
    7. They spin forever...
    
  4. 而且,正如你所提到的,旋转循环很疯狂,但我认为这是一个演示线程程序。

答案 1 :(得分:1)

问题出现在这些行之间:

workToDo.run();
workToDo = null;

假设发生以下事件序列:

- Original Runnable runs.  "Ping!".setWork() called
- Ping! thread realizes workToDo != null, calls run(), the stops between those two lines
  - "Pong!".setWork() called
- Pong! thread realizes workToDo != null, calls run()
  - "Ping!".setWork() called
- Ping! thread resumes, sets workToDo = null, ignorantly discarding the new value
- Both threads now have workToDo = null, and the counter is frozen at 2,...,80
  Program hangs

答案 2 :(得分:0)

我的2美分......

import java.util.concurrent.atomic.AtomicReference;

class WorkerThread extends Thread {
    private AtomicReference<Runnable> work;

    public WorkerThread(String name) {
        super(name);
        work = new AtomicReference<Runnable>();
    }

    @Override
    public void run() {
        while (true){
            Runnable workToDo = work.getAndSet(null);
            if ( workToDo != null ) {
                workToDo.run();
            }
        }
    }

    public void setWork(Runnable newWork) {
        this.work.set(newWork);
    }
}
相关问题