我要求每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),那么我需要保持一些逻辑。
由于
答案 0 :(得分:7)
我可以在线程上保证睡眠时间吗?
抱歉,但没有。
无法在Java SE JVM中获得可靠,精确的延迟时序。您需要使用在实时操作系统上运行的Real time Java实现。
以下是普通操作系统上的Java SE无法执行此操作的几个原因。
在某些时候,Java SE JVM中的GC需要“停止世界”#34;。发生这种情况时,没有用户线程可以运行。如果你的计时器在“停止世界”中熄灭了。暂停,无法安排,直到暂停结束。
JVM中的线程调度实际上是由主机操作系统完成的。如果系统繁忙,主机操作系统可能会决定在您的应用程序需要时不安排JVM的线程。
java.util.Timer.scheduleAtFixedRate
方法可能与您在Java SE上的方法一样好。它应该解决长期漂移问题,但你无法摆脱抖动"。而且这种抖动可能很容易达到几百毫秒......甚至几秒钟。
如果系统繁忙且操作系统正在抢占或未安排线程,Spinlocks将无法提供帮助。 (并且用户代码中的自旋锁定是浪费的......)
答案 1 :(得分:1)
如果你真的有困难时间限制,你想使用real-time operating system。通用计算没有时间限制;如果你的操作系统以你的一个间隔进入虚拟内存,那么你可能会错过你的睡眠间隔。实时操作系统将做出权衡,你可能会做得更少,但这项工作可以更好地安排。
如果您需要在普通操作系统上执行此操作,则可以旋转锁定而不是睡眠。这实在是效率低下,但如果你真的有困难,那么这是接近它的最好方法。
答案 2 :(得分:1)
根据评论,主要目标不是同时以此精确间隔执行多个任务。相反,目标是尽可能精确地以此间隔执行单个任务。
不幸的是,ScheduledExecutorService
或涉及Thread#sleep
或LockSupport#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获取时间,并在计算中使用它。或者换句话说,我会得到准确的时间并在计算中使用它。