如何在线程上实现保证的睡眠时间

时间:2016-07-10 12:15:48

标签: java multithreading

我要求每50毫秒调用一个类方法。我没有使用Thread.sleep因为它非常重要,因为它尽可能精确地发生在milli,而sleep只能保证最短的时间。基本设置如下:

public class ClassA{
    public void setup(){
        ScheduledExecutorService se = Executors.newScheduledThreadPool(20);
        se.scheduleAtFixedRate(this::onCall, 2000, 50, TimeUnit.MILLISECONDS);
    }

    protected void onCall(Event event) {
        // do something
    }
}

现在这个大体上工作正常。我已将System.out.println(System.nanoTime)放在onCall中,以便按照我希望的那样检查其被调用。我发现在100次通话过程中会有1-5毫秒的漂移,这会一次又一次地纠正。

不幸的是,5毫秒漂移对我来说非常重要。 1毫米漂移是可以的,但是在5毫秒时,由于其他物体的状态,它会影响我在onCall中所做的计算。如果我可以让调度程序自动更正,如果它在一次通话中迟到5ms,则下一次将在45ms而不是50ms内发生,这几乎是可以的。

我的问题是:在Java中有没有更精确的方法来实现这一目标?我现在能想到的唯一解决方案是每隔1ms调用check方法并检查时间,看它是否在50ms标记处。但是,如果在偶然的情况下,错过了精确的50ms间隔(49,51),那么我需要保持一些逻辑。

由于

5 个答案:

答案 0 :(得分:7)

  

我可以在线程上保证睡眠时间吗?

抱歉,但没有。

无法在Java SE JVM中获得可靠,精确的延迟时序。您需要使用在实时操作系统上运行的Real time Java实现。

以下是普通操作系统上的Java SE无法执行此操作的几个原因。

  1. 在某些时候,Java SE JVM中的GC需要“停止世界”#34;。发生这种情况时,没有用户线程可以运行。如果你的计时器在“停止世界”中熄灭了。暂停,无法安排,直到暂停结束。

  2. JVM中的线程调度实际上是由主机操作系统完成的。如果系统繁忙,主机操作系统可能会决定在您的应用程序需要时不安排JVM的线程。

  3. java.util.Timer.scheduleAtFixedRate方法可能与您在Java SE上的方法一样好。它应该解决长期漂移问题,但你无法摆脱抖动"。而且这种抖动可能很容易达到几百毫秒......甚至几秒钟。

    如果系统繁忙且操作系统正在抢占或未安排线程,Spinlocks将无法提供帮助。 (并且用户代码中的自旋锁定是浪费的......)

答案 1 :(得分:1)

如果你真的有困难时间限制,你想使用real-time operating system。通用计算没有时间限制;如果你的操作系统以你的一个间隔进入虚拟内存,那么你可能会错过你的睡眠间隔。实时操作系统将做出权衡,你可能会做得更少,但这项工作可以更好地安排。

如果您需要在普通操作系统上执行此操作,则可以旋转锁定而不是睡眠。这实在是效率低下,但如果你真的有困难,那么这是接近它的最好方法。

答案 2 :(得分:1)

根据评论,主要目标不是同时以此精确间隔执行多个任务。相反,目标是尽可能精确地以此间隔执行单个任务。

不幸的是,ScheduledExecutorService或涉及Thread#sleepLockSupport#parkNanos的任何手动构造在这个意义上都不是非常精确。正如其他答案所指出的那样:可能总会有一些影响因素无法控制 - 即JVM实施细节,垃圾收集,JIT运行等。

然而,在这里实现高精度的相对简单的方法是忙等待。 (这已在现已删除的答案中提及)。但当然,这有几个警告。最重要的是,它将刻录一个CPU的处理资源。 (在单CPU系统上,这可能特别糟糕)。

但是为了表明它可能比其他等待方法更精确,这里是ScheduledExecutorService方法和忙碌等待的简单比较:

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class PreciseSchedulingTest
{
    public static void main(String[] args)
    {
        long periodMs = 50;
        PreciseSchedulingA a = new PreciseSchedulingA();
        a.setup(periodMs);

        PreciseSchedulingB b = new PreciseSchedulingB();
        b.setup(periodMs);
    }
}

class CallTracker implements Runnable
{
    String name;
    long expectedPeriodMs;
    long baseTimeNs;
    long callTimesNs[];
    int numCalls;
    int currentCall;

    CallTracker(String name, long expectedPeriodMs)
    {
        this.name = name;
        this.expectedPeriodMs = expectedPeriodMs;
        this.baseTimeNs = System.nanoTime();
        this.numCalls = 50;
        this.callTimesNs = new long[numCalls];
    }

    @Override
    public void run()
    {
        callTimesNs[currentCall] = System.nanoTime();
        currentCall++;
        if (currentCall == numCalls)
        {
            currentCall = 0;
            double maxErrorMs = 0;
            for (int i = 1; i < numCalls; i++)
            {
                long ns = callTimesNs[i] - callTimesNs[i - 1];
                double ms = ns * 1e-6;
                double errorMs = ms - expectedPeriodMs;
                if (Math.abs(errorMs) > Math.abs(maxErrorMs))
                {
                    maxErrorMs = errorMs;
                }
                //System.out.println(errorMs);
            }
            System.out.println(name + ", maxErrorMs : " + maxErrorMs);
        }
    }

}

class PreciseSchedulingA
{
    public void setup(long periodMs)
    {
        CallTracker callTracker = new CallTracker("A", periodMs);
        ScheduledExecutorService se = Executors.newScheduledThreadPool(20);
        se.scheduleAtFixedRate(callTracker, periodMs, 
            periodMs, TimeUnit.MILLISECONDS);
    }
}

class PreciseSchedulingB
{
    public void setup(long periodMs)
    {
        CallTracker callTracker = new CallTracker("B", periodMs);

        Thread thread = new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                while (true)
                {
                    long periodNs = periodMs * 1000 * 1000;
                    long endNs = System.nanoTime() + periodNs;
                    while (System.nanoTime() < endNs) 
                    {
                        // Busy waiting...
                    }
                    callTracker.run();
                }
            }
        });
        thread.setDaemon(true);
        thread.start();
    }
}

同样,这应该花一点时间,但MyMachine®的结果如下:

A, maxErrorMs : 1.7585339999999974
B, maxErrorMs : 0.06753599999999693
A, maxErrorMs : 1.7669149999999973
B, maxErrorMs : 0.007193999999998368
A, maxErrorMs : 1.7775299999999987
B, maxErrorMs : 0.012780999999996823

显示等待时间的错误在几微秒的范围内。

为了在实践中应用这种方法,需要更复杂的基础设施。例如。为了弥补过高的等待时间而需要的簿记。 (我认为它们不能太低)。此外,所有这些仍然不能保证精确定时执行。但至少可以考虑这个问题。

答案 3 :(得分:0)

是(假设您只想防止长期漂移并且不要担心每次延迟)。 java.util.Timer.scheduleAtFixedRate:

  

...在固定速率执行中,每次执行都是相对于初始执行的预定执行时间进行调度的。如果执行因任何原因(例如垃圾收集或其他后台活动)而延迟,则会快速连续执行两次或更多次执行,以便赶上。&#34;赶超。从长远来看,执行频率将恰好是指定周期的倒数(假设Object.wait(long)下的系统时钟是准确的)。 ...

基本上,做这样的事情:

new Timer().scheduleAtFixedRate(new TimerTask() {

        @Override
        public void run() {
            this.onCall();
        }
    }, 2000, 50);

答案 4 :(得分:0)

这很难 - 想想GC ......我要做的是用nanoTime获取时间,并在计算中使用它。或者换句话说,我会得到准确的时间并在计算中使用它。