Czy metoda `System.currentTimeMillis ()` jest poprawna w przypadku wielu procesów?

Nov 23 2020

Mamy sytuację, w której proces główny zapisuje do dziennika.

Następnie tworzy wiele procesów roboczych, które zapisują w swoich własnych dziennikach. (Chciałem, aby pracownicy logowali się do mastera, ale z jakiegoś powodu był sprzeciw wobec tego pomysłu).

Chcę wiedzieć, czy mogę ufać, że sygnatury czasowe, które kończą się w wielu plikach, są ze sobą spójne? tj. jeśli połączę pliki dziennika w jeden plik, sortując je na bieżąco, czy kolejność zdarzeń będzie prawdziwa? We wszystkich możliwych systemach operacyjnych?

Pytam o to, że mam dziwną sytuację, w której wygląda na to, że proces roboczy zarejestrował błąd dwie sekundy po tym, jak master zgłosił, że pracownik wystąpił błąd. To tak, jakby mistrz mógł patrzeć w przyszłość. (Myślę, że mistrz jest również panem czasu, ale uh ...)

Odpowiedzi

13 BasilBourque Nov 23 2020 at 07:53

Wywołanie System.currentTimeMillisi jego nowoczesny zamiennik Instant.nowprzechwytują bieżący moment zgłoszony przez system operacyjny hosta i podstawowy sprzęt zegara komputera. Javadoc i kod źródłowy zapewniają zegar „oparty na najlepszym dostępnym zegarze systemowym”.

Więc nie, nie powinno być skoków w przyszłość . Za każdym razem, gdy wywołasz jedną z tych metod, uchwycisz bieżący moment.

Możesz jednak zobaczyć iluzję skoku w przyszłość . Może się tak zdarzyć z następujących powodów:

  • planowanie wątków
  • resetowanie zegara
  • fałszywy zegar

Planowanie wątków

Ta iluzja może wystąpić z powodu tego, co dzieje się po uchwyceniu obecnego momentu. Ułamek sekundy po przechwyceniu bieżącego momentu, wykonanie tego wątku może zostać wstrzymane. Inny wątek może wtedy uchwycić późniejszą chwilę, kontynuuj raportowanie tej chwili. W końcu ten pierwszy wątek zostaje wznowiony i zgłasza wcześniej przechwycony moment - ale zwróć uwagę, jak raportowanie tego momentu ma miejsce później.

Weź ten przykładowy kod.

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();
        }
    }
}

Gdy pierwszy raz uruchomiłem ten kod, znalazłem taki skok w ostatnich dwóch wierszach danych wyjściowych. Czwarta linia pokazuje chwilę wcześniej niż trzecia. Piąta linia pokazuje chwilę wcześniej.

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

W moim przypadku tutaj wezwania do System.out.printlnuległy opóźnieniu, więc niektóre wcześniejsze momenty zostały zgłoszone później. Podejrzewam również, że w twoim przypadku rejestracja przechwyconych momentów wiązała się z różnymi opóźnieniami, tak że niektóre wcześniejsze momenty zostały zarejestrowane później.

Resetowanie zegara

Jak podkreśla Stephen C w komentarzach poniżej , komputery są często skonfigurowane do automatycznego dostosowywania zegara sprzętowego w oparciu o informacje z serwera czasu. Zegary sprzętowe w wielu komputerach są mniej dokładne, niż mogłoby się wydawać. Zatem zegar komputera-hosta może być zresetowany na wcześniejszą lub późniejszą porę dnia, aby skorygować dryf śledzenia czasu.

Należy pamiętać, że niektóre komputery resetują swój zegar do punktu odniesienia epoki, takiego jak 1970-01-01 00: 00Z, gdy są uruchamiane z wadliwą lub wyczerpaną baterią / kondensatorem podtrzymującym zegar sprzętowy. Ten moment odniesienia epoki może być zgłaszany jako moment bieżący, dopóki komputer nie będzie miał szansy sprawdzić się na serwerze czasu.

Albo ktoś mógłby ręcznie ustawić aktualną datę i godzinę zegara komputera. :-(

Twój kod może uchwycić bieżący moment po obu stronach tej regulacji zegara. Teraz może się wydawać, że późniejsze zdarzenie miało miejsce wcześniej.

Fałszywy zegar

W java.time wywołania takie jak Instant.nowdostęp do aktualnie przypisanej Clockimplementacji. Przez „aktualnie przypisany” mam na myśli fakt, że w java.timeClock można przesłonić obiekt domyślny . Zwykle służy to wyłącznie celom testowym. Różne Clockobiekty mogą zgłaszać stały moment , przesunięty moment lub mogą zgłaszać ze zmienioną kadencją .

Pamiętaj więc, że alternatywa Clockmoże celowo wskazywać inny czas, jeśli twój kod testowy określił alternatywny Clockobiekt. Domyślnie jednak zawsze uzyskujesz bieżący moment w czasie wykonywania wywołania metody.

Wniosek

Ma to poważną konsekwencję: śledzeniu czasu nie można całkowicie ufać . Bieżący moment może być nieprawidłowo uchwycony , a raportowanie przechwyconych momentów może być nieczynne.

Dlatego podczas debugowania lub badania zawsze miej tę myśl z tyłu głowy: znaczniki czasu i ich kolejność mogą nie mówić całej prawdy. Ostatecznie nie możesz wiedzieć ze 100% pewnością, co się stało i kiedy.