Il thread Java che esegue l'operazione resto in un ciclo blocca tutti gli altri thread


123

Il seguente frammento di codice esegue due thread, uno è un semplice timer che registra ogni secondo, il secondo è un ciclo infinito che esegue un'operazione resto:

public class TestBlockingThread {
    private static final Logger LOGGER = LoggerFactory.getLogger(TestBlockingThread.class);

    public static final void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            int i = 0;
            while (true) {
                i++;
                if (i != 0) {
                    boolean b = 1 % i == 0;
                }
            }
        };

        new Thread(new LogTimer()).start();
        Thread.sleep(2000);
        new Thread(task).start();
    }

    public static class LogTimer implements Runnable {
        @Override
        public void run() {
            while (true) {
                long start = System.currentTimeMillis();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // do nothing
                }
                LOGGER.info("timeElapsed={}", System.currentTimeMillis() - start);
            }
        }
    }
}

Questo dà il seguente risultato:

[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=13331
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1006
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1003
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004
[Thread-0] INFO  c.m.c.concurrent.TestBlockingThread - timeElapsed=1004

Non capisco perché l'attività infinita blocca tutti gli altri thread per 13,3 secondi. Ho provato a cambiare le priorità del thread e altre impostazioni, niente ha funzionato.

Se hai suggerimenti per risolvere questo problema (inclusa la modifica delle impostazioni di cambio di contesto del sistema operativo), fammelo sapere.


8
@Marthin Not GC. È JIT. In esecuzione con -XX:+PrintCompilationottengo quanto segue al termine del ritardo esteso: TestBlockingThread :: lambda $ 0 @ 2 (24 byte) COMPILE SKIPPED: banale ciclo infinito (riprova a livello diverso)
Andreas

4
Si riproduce sul mio sistema con l'unica modifica che ho sostituito la chiamata di registro con System.out.println. Sembra un problema dello scheduler perché se si introduce una sospensione di 1 ms all'interno del ciclo while (true) di Runnable, la pausa nell'altro thread scompare.
JJF

3
Non che lo raccomandi, ma se disabiliti JIT usando -Djava.compiler=NONE, non succederà.
Andreas

3
È presumibilmente possibile disabilitare JIT per un singolo metodo. Vedere Disabilitare Java JIT per un metodo / classe specifico?
Andreas

3
Non esiste una divisione intera in questo codice. Correggi il titolo e la domanda.
Marchese di Lorne

Risposte:


94

Dopo tutte le spiegazioni qui (grazie a Peter Lawrey ) abbiamo scoperto che la fonte principale di questa pausa è che il punto sicuro all'interno del ciclo viene raggiunto piuttosto raramente, quindi ci vuole molto tempo per fermare tutti i thread per la sostituzione del codice compilato con JIT.

Ma ho deciso di andare più a fondo e scoprire perché il punto di sicurezza viene raggiunto raramente. Ho trovato un po 'confuso il motivo per cui il salto all'indietro del whileloop non è "sicuro" in questo caso.

Quindi chiamo -XX:+PrintAssemblyin tutta la sua gloria per aiutare

-XX:+UnlockDiagnosticVMOptions \
-XX:+TraceClassLoading \
-XX:+DebugNonSafepoints \
-XX:+PrintCompilation \
-XX:+PrintGCDetails \
-XX:+PrintStubCode \
-XX:+PrintAssembly \
-XX:PrintAssemblyOptions=-Mintel

Dopo alcune indagini ho scoperto che dopo la terza ricompilazione del C2compilatore lambda ha gettato via completamente i sondaggi safepoint all'interno del ciclo.

AGGIORNARE

Durante la fase di profilazione la variabile inon è mai stata vista uguale a 0. Ecco perché ha C2ottimizzato speculativamente questo ramo, in modo che il ciclo fosse trasformato in qualcosa di simile

for (int i = OSR_value; i != 0; i++) {
    if (1 % i == 0) {
        uncommon_trap();
    }
}
uncommon_trap();

Nota che il ciclo infinito originariamente è stato rimodellato in un ciclo finito regolare con un contatore! A causa dell'ottimizzazione JIT per eliminare i sondaggi safepoint in cicli con conteggio finiti, non vi era nemmeno alcun sondaggio safepoint in questo ciclo.

Dopo un po 'di tempo, iavvolto di nuovo 0, e la trappola insolita è stata presa. Il metodo è stato deottimizzato e ha continuato l'esecuzione nell'interprete. Durante la ricompilazione con una nuova conoscenza ha C2riconosciuto il ciclo infinito e ha rinunciato alla compilazione. Il resto del metodo è proceduto nell'interprete con i safepoint appropriati.

C'è un ottimo post sul blog "Punti di sicurezza: significato, effetti collaterali e spese generali" di Nitsan Wakart che tratta i punti di sicurezza e questo particolare problema.

È noto che l'eliminazione di Safepoint in loop contati molto lunghi è un problema. Il bug JDK-5014723(grazie a Vladimir Ivanov ) risolve questo problema.

La soluzione alternativa è disponibile fino a quando il bug non viene finalmente risolto.

  1. Si può provare a utilizzare -XX:+UseCountedLoopSafepoints(che sarà causare pena complessiva delle prestazioni e può portare a blocco della JVM JDK-8161147 ). Dopo averlo usato il C2compilatore continua a mantenere i safepoint ai salti all'indietro e la pausa originale scompare completamente.
  2. È possibile disabilitare esplicitamente la compilazione del metodo problematico utilizzando
    -XX:CompileCommand='exclude,binary/class/Name,methodName'

  3. Oppure puoi riscrivere il codice aggiungendo manualmente safepoint. Ad esempio, la Thread.yield()chiamata alla fine del ciclo o anche il passaggio int ia long i(grazie, Nitsan Wakart ) risolverà anche la pausa.


7
Questa è la vera risposta alla domanda su come riparare .
Andreas

ATTENZIONE: non utilizzare -XX:+UseCountedLoopSafepointsin produzione, poiché potrebbe causare l' arresto anomalo di JVM . La soluzione migliore finora è dividere manualmente il ciclo lungo in quelli più brevi.
apangin

@apangin aah. fatto! grazie :) ecco perché c2rimuove i safepoint! ma un'altra cosa che non ho capito è cosa succederà dopo. per quanto posso vedere non ci sono punti di sicurezza rimasti dopo lo srotolamento del ciclo (?) e sembra che non ci sia modo di fare stw. quindi c'è una sorta di timeout e ha luogo la deottimizzazione?
vsminkov

2
Il mio commento precedente non era accurato. Ora è completamente chiaro cosa succede. Nella fase di profilazione inon è mai 0, quindi il ciclo viene trasformato speculativamente in qualcosa di simile ad for (int i = osr_value; i != 0; i++) { if (1 % i == 0) uncommon_trap(); } uncommon_trap();un ciclo regolare conteggio finito. Una volta che itorna a 0, viene presa la trappola non comune, il metodo viene deottimizzato e procede in interprete. Durante la ricompilazione con la nuova conoscenza JIT riconosce il ciclo infinito e rinuncia alla compilazione. Il resto del metodo viene eseguito nell'interprete con i safepoint appropriati.
apangin

1
Potresti semplicemente rendere ia lungo invece di un int, questo renderebbe il ciclo "non contato" e risolverebbe il problema.
Nitsan Wakart

64

In breve, il loop che hai non ha un punto sicuro al suo interno tranne quando i == 0viene raggiunto. Quando questo metodo viene compilato e attiva il codice da sostituire, è necessario portare tutti i thread a un punto sicuro, ma ciò richiede molto tempo, bloccando non solo il thread che esegue il codice ma tutti i thread nella JVM.

Ho aggiunto le seguenti opzioni della riga di comando.

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -XX:+PrintCompilation

Ho anche modificato il codice per utilizzare il punto mobile che sembra richiedere più tempo.

boolean b = 1.0 / i == 0;

E quello che vedo nell'output è

timeElapsed=100
Application time: 0.9560686 seconds
  41423  280 %     4       TestBlockingThread::lambda$main$0 @ -2 (27 bytes)   made not entrant
Total time for which application threads were stopped: 40.3971116 seconds, Stopping threads took: 40.3967755 seconds
Application time: 0.0000219 seconds
Total time for which application threads were stopped: 0.0005840 seconds, Stopping threads took: 0.0000383 seconds
  41424  281 %     3       TestBlockingThread::lambda$main$0 @ 2 (27 bytes)
timeElapsed=40473
  41425  282 %     4       TestBlockingThread::lambda$main$0 @ 2 (27 bytes)
  41426  281 %     3       TestBlockingThread::lambda$main$0 @ -2 (27 bytes)   made not entrant
timeElapsed=100

Nota: per sostituire il codice, i thread devono essere interrotti in un punto sicuro. Tuttavia sembra qui che un tale punto sicuro venga raggiunto molto raramente (forse solo quando si i == 0cambia l'attività in

Runnable task = () -> {
    for (int i = 1; i != 0 ; i++) {
        boolean b = 1.0 / i == 0;
    }
};

Vedo un ritardo simile.

timeElapsed=100
Application time: 0.9587419 seconds
  39044  280 %     4       TestBlockingThread::lambda$main$0 @ -2 (28 bytes)   made not entrant
Total time for which application threads were stopped: 38.0227039 seconds, Stopping threads took: 38.0225761 seconds
Application time: 0.0000087 seconds
Total time for which application threads were stopped: 0.0003102 seconds, Stopping threads took: 0.0000105 seconds
timeElapsed=38100
timeElapsed=100

Aggiungendo attentamente il codice al ciclo si ottiene un ritardo maggiore.

for (int i = 1; i != 0 ; i++) {
    boolean b = 1.0 / i / i == 0;
}

prende

 Total time for which application threads were stopped: 59.6034546 seconds, Stopping threads took: 59.6030773 seconds

Tuttavia, cambia il codice per utilizzare un metodo nativo che abbia sempre un punto sicuro (se non è un intrinseco)

for (int i = 1; i != 0 ; i++) {
    boolean b = Math.cos(1.0 / i) == 0;
}

stampe

Total time for which application threads were stopped: 0.0001444 seconds, Stopping threads took: 0.0000615 seconds

Nota: l'aggiunta if (Thread.currentThread().isInterrupted()) { ... }a un ciclo aggiunge un punto sicuro.

Nota: questo è accaduto su una macchina a 16 core, quindi non mancano le risorse della CPU.


1
Quindi è un bug di JVM, giusto? Dove "bug" significa grave problema di qualità dell'implementazione e non violazione delle specifiche.
usr

1
@vsminkov che è in grado di fermare il mondo per diversi minuti a causa della mancanza di safepoint sembra che dovrebbe essere trattato come un bug. Il runtime è responsabile di introdurre safepoint per evitare lunghe attese.
Voo

1
@Voo ma d'altra parte mantenere i punti di sicurezza in ogni salto indietro può costare molti cicli della CPU e causare un notevole degrado delle prestazioni dell'intera applicazione. ma sono d'accordo con te. in quel caso particolare sembra legittimo mantenere safepoint
vsminkov

9
@Voo beh ... Ricordo sempre questa immagine quando si tratta di ottimizzazioni delle prestazioni: D
vsminkov

1
.NET inserisce i safepoint qui (ma .NET ha un codice generato lentamente). Una possibile soluzione è tagliare il ciclo. Suddiviso in due cicli, fare in modo che il ciclo interno non verifichi i batch di 1024 elementi e il ciclo esterno guidi batch e safepoint. Taglia l'overhead concettualmente di 1024x, meno in pratica.
usr

26

Ho trovato la risposta del perché . Sono chiamati safepoint e sono meglio conosciuti come Stop-The-World che accade a causa di GC.

Consulta gli articoli: Registrazione delle pause stop-the-world in JVM

Eventi diversi possono causare la sospensione di tutti i thread dell'applicazione da parte della JVM. Tali pause sono chiamate pause Stop-The-World (STW). La causa più comune per l'attivazione di una pausa STW è la garbage collection (esempio in github), ma diverse azioni JIT (esempio), revoca del blocco parziale (esempio), alcune operazioni JVMTI e molte altre richiedono anche l'arresto dell'applicazione.

I punti in cui i thread dell'applicazione possono essere fermati in modo sicuro sono chiamati, sorpresa, safepoint . Questo termine è spesso usato anche per riferirsi a tutte le pause STW.

È più o meno comune che i log GC siano abilitati. Tuttavia, questo non acquisisce le informazioni su tutti i safepoint. Per ottenere tutto, utilizza queste opzioni JVM:

-XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime

Se ti stai interrogando sulla denominazione che si riferisce esplicitamente a GC, non allarmarti: l'attivazione di queste opzioni registra tutti i safepoint, non solo le pause di garbage collection. Se esegui un esempio seguente (sorgente in GitHub) con i flag specificati sopra.

Leggendo il Glossario dei termini di HotSpot , definisce questo:

safepoint

Un punto durante l'esecuzione del programma in cui tutte le radici GC sono note e tutti i contenuti degli oggetti heap sono coerenti. Da un punto di vista globale, tutti i thread devono bloccarsi in un punto sicuro prima che il GC possa essere eseguito. (Come caso speciale, i thread che eseguono codice JNI possono continuare a essere eseguiti, perché usano solo handle. Durante un safepoint devono bloccare invece di caricare il contenuto dell'handle.) Da un punto di vista locale, un safepoint è un punto distinto in un blocco di codice in cui il thread in esecuzione potrebbe bloccarsi per il GC. La maggior parte dei siti di chiamata si qualifica come safepoint.Ci sono forti invarianti che valgono in ogni punto di sicurezza, che possono essere ignorati nei punti di sicurezza. Sia il codice Java compilato che il codice C / C ++ possono essere ottimizzati tra i safepoint, ma meno tra i safepoint. Il compilatore JIT emette una mappa GC a ogni safepoint. Il codice C / C ++ nella VM utilizza convenzioni stilizzate basate su macro (ad esempio, TRAPS) per contrassegnare potenziali safepoint.

Funzionando con i flag sopra menzionati, ottengo questo output:

Application time: 0.9668750 seconds
Total time for which application threads were stopped: 0.0000747 seconds, Stopping threads took: 0.0000291 seconds
timeElapsed=1015
Application time: 1.0148568 seconds
Total time for which application threads were stopped: 0.0000556 seconds, Stopping threads took: 0.0000168 seconds
timeElapsed=1015
timeElapsed=1014
Application time: 2.0453971 seconds
Total time for which application threads were stopped: 10.7951187 seconds, Stopping threads took: 10.7950774 seconds
timeElapsed=11732
Application time: 1.0149263 seconds
Total time for which application threads were stopped: 0.0000644 seconds, Stopping threads took: 0.0000368 seconds
timeElapsed=1015

Notare il terzo evento STW:
Tempo totale interrotto: 10,7951187 secondi
Interruzione thread presi: 10,7950774 secondi

Lo stesso JIT non ha richiesto praticamente tempo, ma una volta che la JVM ha deciso di eseguire una compilazione JIT, è entrata in modalità STW, tuttavia poiché il codice da compilare (il ciclo infinito) non ha un sito di chiamata , non è mai stato raggiunto alcun punto di sicurezza.

L'STW termina quando JIT alla fine smette di aspettare e conclude che il codice è in un ciclo infinito.


"Safepoint - Un punto durante l'esecuzione del programma in cui tutte le radici GC sono note e tutti i contenuti degli oggetti heap sono coerenti" - Perché questo non dovrebbe essere vero in un ciclo che imposta / legge solo variabili di tipo valore locale?
BlueRaja - Danny Pflughoeft

@ BlueRaja-DannyPflughoeft Ho provato a rispondere a questa domanda nella mia risposta
vsminkov

5

Dopo aver seguito i thread dei commenti e alcuni test per conto mio, credo che la pausa sia causata dal compilatore JIT. Perché il compilatore JIT impiega così tanto tempo va oltre la mia capacità di eseguire il debug.

Tuttavia, poiché hai solo chiesto come prevenirlo, ho una soluzione:

Trascina il tuo ciclo infinito in un metodo in cui può essere escluso dal compilatore JIT

public class TestBlockingThread {
    private static final Logger LOGGER = Logger.getLogger(TestBlockingThread.class.getName());

    public static final void main(String[] args) throws InterruptedException     {
        Runnable task = () -> {
            infLoop();
        };
        new Thread(new LogTimer()).start();
        Thread.sleep(2000);
        new Thread(task).start();
    }

    private static void infLoop()
    {
        int i = 0;
        while (true) {
            i++;
            if (i != 0) {
                boolean b = 1 % i == 0;
            }
        }
    }

Esegui il tuo programma con questo argomento VM:

-XX: CompileCommand = exclude, PACKAGE.TestBlockingThread :: infLoop (sostituisci PACKAGE con le informazioni sul pacchetto)

Dovresti ricevere un messaggio come questo per indicare quando il metodo sarebbe stato compilato JIT:
### Escludendo compile: static blocking.TestBlockingThread :: infLoop
potresti notare che ho inserito la classe in un pacchetto chiamato blocking


1
Il compilatore non sta impiegando così tanto tempo, il problema è che il codice non sta raggiungendo un punto sicuro perché non ce n'è nessuno all'interno del ciclo tranne quandoi == 0
Peter Lawrey

@ PeterLawrey ma perché la fine del ciclo in whileloop non è un punto di sicurezza?
vsminkov

@vsminkov Sembra che ci sia un punto di sicurezza in if (i != 0) { ... } else { safepoint(); }ma questo è molto raro. vale a dire. se esci / interrompi il ciclo ottieni più o meno gli stessi tempi.
Peter Lawrey

@ PeterLawrey dopo un po 'di indagini ho scoperto che è pratica comune fare un punto di sicurezza al salto indietro del loop. Sono solo curioso di sapere quale sia la differenza in questo caso particolare. forse sono ingenuo ma non vedo motivo per cui il salto all'indietro non sia "sicuro"
vsminkov

@vsminkov Sospetto che il JIT veda che un safepoint è nel ciclo, quindi non ne aggiunge uno alla fine.
Peter Lawrey
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.