JavaRush /Java Blog /Random-IT /Non puoi rovinare Java con una discussione: Parte III - I...
Viacheslav
Livello 3

Non puoi rovinare Java con una discussione: Parte III - Interazione

Pubblicato nel gruppo Random-IT
Una breve panoramica delle funzionalità dell'interazione del thread. In precedenza, abbiamo esaminato come i thread si sincronizzano tra loro. Questa volta approfondiremo i problemi che possono sorgere quando i thread interagiscono e parleremo di come evitarli. Forniremo anche alcuni link utili per un approfondimento. Non puoi rovinare Java con un thread: Parte III - interazione - 1

introduzione

Quindi, sappiamo che ci sono thread in Java, di cui puoi leggere nella recensione " Thread Can't Spoil Java: Part I - Threads " e che i thread possono essere sincronizzati tra loro, di cui abbiamo parlato nella recensione " Il thread non può rovinare Java " Spoil: Parte II - Sincronizzazione ". È tempo di parlare di come i thread interagiscono tra loro. Come condividono le risorse comuni? Che problemi potrebbero esserci con questo?

Una situazione di stallo

Il problema peggiore è Deadlock. Quando due o più thread aspettano per sempre l'uno dall'altro, si parla di Deadlock. Prendiamo un esempio dal sito Oracle dalla descrizione del concetto di " Deadlock ":
public class Deadlock {
    static class Friend {
        private final String name;
        public Friend(String name) {
            this.name = name;
        }
        public String getName() {
            return this.name;
        }
        public synchronized void bow(Friend bower) {
            System.out.format("%s: %s has bowed to me!%n",
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s has bowed back to me!%n",
                    this.name, bower.getName());
        }
    }

    public static void main(String[] args) {
        final Friend alphonse = new Friend("Alphonse");
        final Friend gaston = new Friend("Gaston");
        new Thread(() -> alphonse.bow(gaston)).start();
        new Thread(() -> gaston.bow(alphonse)).start();
    }
}
Lo stallo qui potrebbe non apparire la prima volta, ma se l'esecuzione del programma è bloccata, è ora di eseguirlo jvisualvm: Non puoi rovinare Java con un thread: Parte III - interazione - 2se un plugin è installato in JVisualVM (tramite Strumenti -> Plugin), possiamo vedere dove si è verificato lo stallo:
"Thread-1" - Thread t@12
   java.lang.Thread.State: BLOCKED
    at Deadlock$Friend.bowBack(Deadlock.java:16)
    - waiting to lock &lt33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Il thread 1 è in attesa di un blocco dal thread 0. Perché succede questo? Thread-1avvia l'esecuzione ed esegue il metodo Friend#bow. È contrassegnato con la parola chiave synchronized, ovvero prendiamo il monitor con this. All'ingresso del metodo, abbiamo ricevuto un collegamento ad un altro file Friend. Ora, il thread Thread-1vuole eseguire un metodo su un altro Friend, ottenendo così un lock anche da lui. Ma se un altro thread (in questo caso Thread-0) è riuscito ad accedere al metodo bow, allora il lock è già occupato e Thread-1in attesa Thread-0, e viceversa. Il blocco è irrisolvibile, quindi è Dead, cioè morto. Sia una presa mortale (che non può essere rilasciata) sia un blocco morto da cui non si può scappare. Sul tema deadlock potete guardare il video: " Deadlock - Concurrency #1 - Advanced Java ".

Livelock

Se esiste un Deadlock, allora esiste un Livelock? Sì, c'è) Livelock è che i fili sembrano vivi esteriormente, ma allo stesso tempo non possono fare nulla, perché... la condizione in cui cercano di continuare il loro lavoro non può essere soddisfatta. In sostanza, Livelock è simile al deadlock, ma i thread non "si bloccano" sul sistema in attesa del monitor, ma fanno sempre qualcosa. Per esempio:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class App {
    public static final String ANSI_BLUE = "\u001B[34m";
    public static final String ANSI_PURPLE = "\u001B[35m";

    public static void log(String text) {
        String name = Thread.currentThread().getName(); //like Thread-1 or Thread-0
        String color = ANSI_BLUE;
        int val = Integer.valueOf(name.substring(name.lastIndexOf("-") + 1)) + 1;
        if (val != 0) {
            color = ANSI_PURPLE;
        }
        System.out.println(color + name + ": " + text + color);
        try {
            System.out.println(color + name + ": wait for " + val + " sec" + color);
            Thread.currentThread().sleep(val * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Lock first = new ReentrantLock();
        Lock second = new ReentrantLock();

        Runnable locker = () -> {
            boolean firstLocked = false;
            boolean secondLocked = false;
            try {
                while (!firstLocked || !secondLocked) {
                    firstLocked = first.tryLock(100, TimeUnit.MILLISECONDS);
                    log("First Locked: " + firstLocked);
                    secondLocked = second.tryLock(100, TimeUnit.MILLISECONDS);
                    log("Second Locked: " + secondLocked);
                }
                first.unlock();
                second.unlock();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        new Thread(locker).start();
        new Thread(locker).start();
    }
}
Il successo di questo codice dipende dall'ordine in cui lo scheduler dei thread Java avvia i thread. Se si avvia prima Thead-1, otterremo Livelock:
Thread-1: First Locked: true
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
Thread-0: Second Locked: true
Thread-0: wait for 1 sec
Thread-1: Second Locked: false
Thread-1: wait for 2 sec
Thread-0: First Locked: false
Thread-0: wait for 1 sec
...
Come si può vedere dall'esempio, entrambi i thread tentano alternativamente di catturare entrambi i lock, ma falliscono. Inoltre, non sono in una situazione di stallo, cioè visivamente va tutto bene con loro e stanno facendo il loro lavoro. Non puoi rovinare Java con un thread: Parte III - interazione - 3Secondo JVisualVM, vediamo i periodi di sospensione e il periodo di parcheggio (questo è quando un thread tenta di occupare un blocco, entra nello stato di parcheggio, come abbiamo discusso in precedenza parlando della sincronizzazione dei thread ). A proposito di livelock puoi vedere un esempio: " Java - Thread Livelock ".

Fame

Oltre al blocco (deadlock e livelock), c'è un altro problema quando si lavora con il multithreading: Starvation o "starvation". Questo fenomeno differisce dal blocco in quanto i thread non vengono bloccati, ma semplicemente non hanno risorse sufficienti per tutti. Pertanto, mentre alcuni thread occupano tutto il tempo di esecuzione, altri non possono essere eseguiti: Non puoi rovinare Java con un thread: Parte III - interazione - 4

https://www.logicbig.com/

Un super esempio può essere trovato qui: " Java - Thread Starvation and Fairness ". Questo esempio mostra come funzionano i thread in Starvation e come una piccola modifica da Thread.sleep a Thread.wait può distribuire il carico in modo uniforme. Non puoi rovinare Java con un thread: Parte III - interazione - 5

Condizione di gara

Quando si lavora con il multithreading, esiste una cosa come una "condizione di gara". Questo fenomeno sta nel fatto che i thread condividono tra loro una determinata risorsa e il codice è scritto in modo tale da non garantire il corretto funzionamento in questo caso. Diamo un'occhiata ad un esempio:
public class App {
    public static int value = 0;

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                int oldValue = value;
                int newValue = ++value;
                if (oldValue + 1 != newValue) {
                    throw new IllegalStateException(oldValue + " + 1 = " + newValue);
                }
            }
        };
        new Thread(task).start();
        new Thread(task).start();
        new Thread(task).start();
    }
}
Questo codice potrebbe non generare un errore la prima volta. E potrebbe assomigliare a questo:
Exception in thread "Thread-1" java.lang.IllegalStateException: 7899 + 1 = 7901
    at App.lambda$main$0(App.java:13)
    at java.lang.Thread.run(Thread.java:745)
Come puoi vedere, durante l'assegnazione, newValuequalcosa è andato storto e newValuece n'erano altri. Alcuni dei fili nella condizione di gara sono riusciti a cambiare valuetra queste due squadre. Come possiamo vedere, è apparsa una gara tra thread. Ora immagina quanto sia importante non commettere errori simili con le transazioni di denaro... Esempi e diagrammi possono essere visti anche qui: “ Codice per simulare la condizione di gara nel thread Java ”.

Volatile

Parlando dell'interazione dei thread, vale la pena notare in particolare la parola chiave volatile. Diamo un'occhiata ad un semplice esempio:
public class App {
    public static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        Runnable whileFlagFalse = () -> {
            while(!flag) {
            }
            System.out.println("Flag is now TRUE");
        };

        new Thread(whileFlagFalse).start();
        Thread.sleep(1000);
        flag = true;
    }
}
La cosa più interessante è che con un alto grado di probabilità non funzionerà. Il nuovo thread non vedrà il cambiamento flag. Per risolvere questo problema, flagè necessario specificare una parola chiave per il campo volatile. Come e perché? Tutte le azioni vengono eseguite dal processore. Ma i risultati del calcolo devono essere archiviati da qualche parte. A questo scopo sul processore c'è la memoria principale e una cache hardware. Queste cache del processore sono come un piccolo pezzo di memoria per accedere ai dati più velocemente rispetto all'accesso alla memoria principale. Ma tutto ha anche uno svantaggio: i dati nella cache potrebbero non essere aggiornati (come nell'esempio sopra, quando il valore del flag non è stato aggiornato). Quindi, la parola chiave volatiledice alla JVM che non vogliamo memorizzare nella cache la nostra variabile. Ciò ti consente di vedere il risultato effettivo in tutti i thread. Questa è una formulazione molto semplificata. Su questo argomento volatilesi consiglia vivamente di leggere la traduzione delle " JSR 133 (Java Memory Model) FAQ ". Ti consiglio inoltre di leggere di più sui materiali “ Java Memory Model ” e “ Java Volatile Keyword ”. Inoltre, è importante ricordare che volatilesi tratta di visibilità e non di atomicità dei cambiamenti. Se prendiamo il codice da "Race Condition", vedremo un suggerimento in IntelliJ Idea: Non puoi rovinare Java con un thread: Parte III - interazione - 6questa ispezione (Ispezione) è stata aggiunta a IntelliJ Idea come parte del problema IDEA-61117 , che era elencato nelle note di rilascio nel 2010.

Atomicita

Le operazioni atomiche sono operazioni che non possono essere divise. Ad esempio, l'operazione di assegnare un valore a una variabile è atomica. Sfortunatamente, l'incremento non è un'operazione atomica, perché un incremento richiede fino a tre operazioni: ottenere il vecchio valore, aggiungerne uno e salvare il valore. Perché l'atomicità è importante? Nell'esempio dell'incremento, se si verifica una condizione di competizione, in qualsiasi momento la risorsa condivisa (ovvero il valore condiviso) potrebbe cambiare improvvisamente. Inoltre, è importante che anche le strutture a 64 bit non siano atomiche, ad esempio longe double. Puoi leggere di più qui: " Garantire l'atomicità durante la lettura e la scrittura di valori a 64 bit ". Un esempio di problemi con l'atomicità può essere visto nel seguente esempio:
public class App {
    public static int value = 0;
    public static AtomicInteger atomic = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 0; i < 10000; i++) {
                value++;
                atomic.incrementAndGet();
            }
        };
        for (int i = 0; i < 3; i++) {
            new Thread(task).start();
        }
        Thread.sleep(300);
        System.out.println(value);
        System.out.println(atomic.get());
    }
}
Una classe speciale per lavorare con Atom Integerci mostrerà sempre 30000, ma valuecambierà di volta in volta. C'è una breve panoramica su questo argomento " Un'introduzione alle variabili atomiche in Java ". Atomic si basa sull'algoritmo Confronta e scambia. Potete saperne di più nell'articolo su Habré " Confronto tra algoritmi senza lock - CAS e FAA usando l'esempio di JDK 7 e 8 " o su Wikipedia nell'articolo su " Confronto con exchange ". Non puoi rovinare Java con un thread: Parte III - interazione - 8

http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html

Succede prima

C'è una cosa interessante e misteriosa: succede prima. Parlando di flussi, vale la pena leggerlo. La relazione Happens Before indica l'ordine in cui verranno visualizzate le azioni tra i thread. Ci sono molte interpretazioni e interpretazioni. Uno dei rapporti più recenti su questo argomento è questo rapporto:
Probabilmente è meglio che questo video non dica nulla al riguardo. Quindi lascerò semplicemente un link al video. Puoi leggere " Java - Comprendere le relazioni accade prima ".

Risultati

In questa recensione, abbiamo esaminato le funzionalità dell'interazione dei thread. Abbiamo discusso dei problemi che potrebbero sorgere e dei modi per rilevarli ed eliminarli. Elenco di materiali aggiuntivi sull'argomento: #Viacheslav
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION