Java - 实现忙等待机制

3
在我的项目中,我目前使用CyclicBarrier来“同步”多个线程(每个线程都运行相同类型的Runnable)。在我的情况下,由于同步频率高,使用CyclicBarrier效率低下,但繁忙等待机制可能会更快。以下是我的一些进展(略去了一些部分):
public class MyRunnable implements Runnable {

    private static AtomicInteger counter = null; // initialized to the number
                                                 // of threads

    public void run() {

        // do work up to a "common point"

        synchronized (this) {

            // decrement the counter and - if necessary - reset it
            if (counter.decrementAndGet() == 0) {

                counter.set(numberOfThreads);

                // make all the busy waiting threads exit from the loop
                for (int i = 0; i < threads.length; i++)
                    threads[i].interrupt();
            }
        }

        // busy wait until all threads have reached the "common point"
        while (!Thread.interrupted()) {}
    }
}

很遗憾,这段代码的表现甚至不如CyclicBarrier这里有一个简短可编译示例。您有任何改进建议吗?


1
这似乎是适合于代码审查(http://codereview.stackexchange.com/)的问题,你尝试过去那里问吗? - Daniel Bingham
1
如果您正在使用AtomicInteger并在线程对象本身上进行同步,为什么需要同步块呢? - Asaf
@Asaf:既然你提到了,其实没有真正的理由。但是如果没有synchronized,它也会变得更慢。 - ryyst
1
请发布一个SSCCE:http://sscce.org/ - Esko Luontola
4个回答

3

如果您拥有的处理器数量大于运行的线程数,则在此处忙等待仅会“更快”地工作。如果您不断旋转Thread.interrupted并且只是消耗CPU时间,那么实际上会严重降低性能。

CyclicBarrier / CountDownLatch出了什么问题?那似乎是一个更好的解决方案。


线程在同步点之间执行的工作相对较少,因此CyclicBarrier的开销会减慢所有操作速度。(请参见此处的讨论) - ryyst

1
这个代码有一个并发 bug(如果一个线程在调用 counter.get() 之间很慢),但可以通过使用两个计数器并重复两次这段代码来解决,以便计数器交替。
if (counter.decrementAndGet() == 0) {
    counter.set(numberOfThreads);
} else {
    while (counter.get() < numberOfThreads) {}
}

请提供一个可以编译并演示性能问题的示例。否则所有答案都只是猜测。

点击这里查看。问题是由于工作量相对于执行的同步数量而言较小所致。 - ryyst
那段代码的问题在于它没有执行任何工作。JVM优化掉了计算部分,所以剩下的只有同步操作。首先要理解这一点:https://dev59.com/hHRB5IYBdhLWcg3wz6UK - Esko Luontola
尽管基准测试存在缺陷,但是我的代码使用两个计数器可以比基准测试快2-16倍(取决于线程数量;我在C2Q6600上测试了1-4个线程)。但是随着线程数量的增加,它的性能会变差。这是可以预料的,因为同步总会带来一些开销。 - Esko Luontola
这是一个实际工作的例子(调用Math.sqrt),并将其分布在许多线程上,以使其线性扩展:http://pastebin.com/433pwPkW - Esko Luontola

1

很难想象忙等待循环比非忙等待循环更快。首先,在您的代码中,您仍然使用了不少于使用CyclicBarrier时需要的同步(请参见下文)。其次,您刚刚重新实现了CyclicBarrier机制,Java开发人员花费了时间和精力来优化它以获得最佳性能。第三,CyclicBarrier使用{{link1:ReentrantLock}}进行同步,这显然比使用synchronized关键字更有效率和更快速。因此,总体而言,您的代码不太可能赢得比赛。

考虑以下参考代码:

public class MyRunnable implements Runnable {

    private static CyclicBarrier barrier = new CyclicBarrier(threads.length);

    public void run() {

        // do work up to a "common point"

        try{
          barrier.await();
        }catch(InterruptedException e){
          Thread.interrupt();
          //Something unlikely has happened. You might want to handle this.
        }   
    }
 }

在一次运行中,此代码将同步thread.length次,这不会比您使用忙等待的版本更多。因此它不可能比您的代码慢。
性能问题的真正原因是您的线程在“相遇”之前做了很少的工作,这可能意味着存在高线程上下文切换开销以及大量同步。
您能否重新考虑架构?您真的需要等待所有工作人员在公共点“相遇”吗?在他们“相遇”之前,您能否多做一些工作?您是否尝试将线程数设置为较小的数字(= CPU / 核心数)?
您能否分享一些关于代码目的的信息并提供更多细节?

顺便提一下,与synchronized相比,Reentrantlock在速度上并没有更快,甚至可能更慢。这在Java 6中是正确的。 - John Vint

0

等待/通知怎么样?

public class MyRunnable implements Runnable {

    private static AtomicInteger counter = null; // initialized to the number
                                                 // of threads

    public void run() {

        // do work up to a "common point"

        // need to synchronize for wait/notify.
        synchronized ( counter ) {
            // decrement the counter and - if necessary - reset it
            if (counter.decrementAndGet() == 0) {

                counter.set(numberOfThreads);

                // notify all the waiting threads
                counter.notifyAll();
            }else{
                // wait until all threads have reached the "common point"
                counter.wait();
            }
        }
    }
}

总的来说,如果你需要同步的频率非常高,以至于屏障的开销成为问题,那就值得怀疑:要么你正在做一些不值得多线程处理的工作,要么你同步的次数超出了必要的范围。

你的实现缺乏对计数器的同步,因此会抛出“IllegalMonitorStateException”异常。 - ryyst
除非您持有对象的监视器,否则无法调用notifyAll()wait() - Esko Luontola
@ryyst:已修复。我通常不直接使用wait()notify[All](),所以一开始不确定锁定应该如何工作。 - trutheality

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接