`System.currentTimeMillis()`は複数のプロセスにわたって正しいですか?

Nov 23 2020

マスタープロセスがログに書き込む状況があります。

次に、独自のログに書き込む複数のワーカープロセスを生成します。(私は労働者にマスターを介してログを記録してほしかったが、何らかの理由でこのアイデアに抵抗があった。)

私が知りたいのは、複数のファイルで終わるタイムスタンプが互いに一貫していることを信頼できますか?つまり、ログファイルを1つのファイルにマージして瞬時に並べ替えた場合、イベントの順序は正しいですか?考えられるすべてのオペレーティングシステムにわたって?

私がこれを求めている理由は、マスターがワーカーにエラーがあると報告してから2秒後に、ワーカープロセスがエラーをログに記録したように見えるという奇妙な状況があるためです。まるでマスターが未来を見通すことができたようです。(マスターもタイムロードだと思いますが、ええと...)

回答

13 BasilBourque Nov 23 2020 at 07:53

の呼び出しSystem.currentTimeMillisとその最新の代替品Instant.nowはどちらも、ホストOSと基盤となるコンピュータークロックハードウェアによって報告された現在の瞬間をキャプチャします。Javadocとソースコードは、「利用可能な最良のシステムクロックに基づく」クロックを約束します。

だから、いや、未来に飛び込むべきではない。これらのメソッドのいずれかを呼び出すたびに、現在の瞬間をキャプチャします。

しかし、あなたは未来に飛び込むような錯覚を見るかもしれません。これは、次の理由で発生する可能性があります。

  • スレッドスケジューリング
  • 時計のリセット
  • 偽の時計

スレッドスケジューリング

この幻想は、現在の瞬間がキャプチャされたに何が起こるかによって発生する可能性があります。現在の瞬間をキャプチャしてから一瞬、そのスレッドの実行が一時停止する場合があります。その後、他のスレッドが後の瞬間をキャプチャし、その瞬間を報告し続ける場合があります。最終的に、その最初のスレッドが再開し、以前にキャプチャされた瞬間を報告しますが、その瞬間の報告が後でどのように行われるかに注意してください。

このサンプルコードを見てください。

package work.basil.example;

import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TellTime
{
    public static void main ( String[] args )
    {
        TellTime app = new TellTime();
        app.demo();
    }

    private void demo ( )
    {
        ExecutorService executorService = Executors.newCachedThreadPool();

        int countThreads = 15;
        List < Callable < Object > > tasks = new ArrayList <>( countThreads );
        for ( int i = 0 ; i < countThreads ; i++ )
        {
            Runnable tellTimeRunnable = ( ) -> System.out.println( Instant.now() );
            tasks.add( Executors.callable( tellTimeRunnable ) );
        }
        try
        {
            List < Future < Object > > list = executorService.invokeAll( tasks );
        }
        catch ( InterruptedException e )
        {
            e.printStackTrace();
        }
    }
}

そのコードを初めて実行したとき、出力の最後の2行でそのようなジャンプを見つけました。4行目は、3行目より少し前を示しています。5行目はさらに早い瞬間を示しています。

2020-11-23T01:07:34.305318Z
2020-11-23T01:07:34.305569Z
2020-11-23T01:07:34.305770Z
2020-11-23T01:07:34.305746Z
2020-11-23T01:07:34.305434Z

ここでの私の場合、呼び出しSystem.out.printlnは実行が遅れたため、以前のいくつかの瞬間が後で報告されました。同様に、あなたの場合、キャプチャされた瞬間をログに記録するという行為にはさまざまな遅延が含まれていたため、以前の瞬間が後でログに記録されたのではないかと思います。

時計のリセット

スティーブン・Cが指摘以下のコメントでは、コンピュータは、多くの場合に設定されているタイムサーバーからの情報に基づいて、ハードウェアクロックを自動調整します。多くのコンピューターのハードウェアクロックは、想像するよりも正確ではありません。したがって、ホストコンピュータのクロックは、時間追跡のドリフトを修正するために、より早いまたはより遅い時刻にリセットされる可能性があります。

一部のコンピューターは、ハードウェアクロックをバックアップするバッテリー/コンデンサーの故障または消耗状態で起動すると、クロックを1970-01-01 00:00Zなどのエポック基準点にリセットすることに注意してください。そのエポック参照瞬間は、コンピューターがタイムサーバーにチェックインする機会を得るまでの現在の瞬間として報告される場合があります。

または、人間がコンピュータの時計の現在の日付と時刻を手動で調整することもできます。:-(

コードは、このクロック調整のいずれかの側で現在の瞬間をキャプチャする場合があります。これで、後のイベントが以前に発生したように見える場合があります。

偽の時計

java.time、などの呼び出し、Instant.now現在割り当てられているアクセスClockの実装。「現在割り当てられている」とは、java.timeでデフォルトClockオブジェクトをオーバーライドできるという事実を指します。通常、これはテスト目的のみです。さまざまなClockオブジェクトが、固定モーメント、シフトモーメントを報告したり、ケイデンスを変更して報告したりできます。

したがってClock、テストコードで代替Clockオブジェクトが指定されている場合、代替が意図的に異なる時刻を通知する可能性があることに注意してください。デフォルトでは、メソッド呼び出しが実行された時点で常に現在の瞬間を取得します。

結論

ここには大きな意味があります。時間追跡は完全に信頼できるわけではありません。現在の瞬間が誤ってキャプチャされている可能性があり、キャプチャされた瞬間のレポートが順不同である可能性があります。

したがって、デバッグまたは調査するときは、常にこの考えを心の奥に隠しておいてください。タイムスタンプとその順序は、完全な真実を伝えていない可能性があります。最終的には、いつ何が起こったのかを100%確実に知ることはできません。