CodeGym /Blog Java /Aleatoriu /Mai bine împreună: Java și clasa Thread. Partea a III-a –...
John Squirrels
Nivel
San Francisco

Mai bine împreună: Java și clasa Thread. Partea a III-a – Interacțiune

Publicat în grup
O scurtă prezentare generală a detaliilor despre modul în care interacționează firele. Anterior, ne-am uitat la modul în care firele sunt sincronizate între ele. De data aceasta ne vom scufunda în problemele care pot apărea pe măsură ce firele de discuție interacționează și vom vorbi despre cum să le evităm. De asemenea, vom oferi câteva link-uri utile pentru un studiu mai aprofundat. Mai bine împreună: Java și clasa Thread.  Partea a III-a — Interacțiune - 1

Introducere

Deci, știm că Java are fire. Puteți citi despre asta în recenzia intitulată Better together: Java and the Thread class. Partea I — Fire de execuție . Și am explorat faptul că firele de execuție se pot sincroniza unele cu altele în recenzia intitulată Better together: Java and the Thread class. Partea a II-a — Sincronizare . Este timpul să vorbim despre cum interacționează firele între ele. Cum împărtășesc resursele comune? Ce probleme ar putea apărea aici? Mai bine împreună: Java și clasa Thread.  Partea a III-a — Interacțiune - 2

Impas

Cea mai înfricoșătoare problemă dintre toate este blocajul. Deadlock este atunci când două sau mai multe fire îl așteaptă veșnic pe celălalt. Vom lua un exemplu de pe pagina web Oracle care descrie blocajul :

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 bowed to me!%n",
                    this.name, bower.getName());
            bower.bowBack(this);
        }
        public synchronized void bowBack(Friend bower) {
            System.out.format("%s: %s 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();
    }
}
Este posibil să nu apară blocarea aici prima dată, dar dacă programul dvs. se blochează, atunci este timpul să rulați jvisualvm: Mai bine împreună: Java și clasa Thread.  Partea a III-a — Interacțiune - 3Cu un plugin JVisualVM instalat (prin Instrumente -> Plugins), putem vedea unde a apărut blocajul:

"Thread-1" - Thread t@12
   java.lang.Thread.State: BLOCKED
	at Deadlock$Friend.bowBack(Deadlock.java:16)
	- waiting to lock <33a78231> (a Deadlock$Friend) owned by "Thread-0" t@11
Firul 1 așteaptă blocarea de la firul 0. De ce se întâmplă asta? Thread-1începe să ruleze și execută Friend#bowmetoda. Este marcat cu synchronizedcuvântul cheie, ceea ce înseamnă că achiziționăm monitorul pentru this(obiectul curent). Intrarea metodei a fost o referire la celălalt Friendobiect. Acum, Thread-1vrea să execute metoda pe celălalt Friendși trebuie să-și obțină blocarea pentru a face acest lucru. Dar dacă celălalt fir (în acest caz Thread-0) a reușit să intre în bow()metodă, atunci blocarea a fost deja achiziționată și Thread-1așteaptăThread-0, si invers. Acesta este un impas de nerezolvat, iar noi îl numim impas. Ca o strângere de moarte care nu poate fi eliberată, blocajul este o blocare reciprocă care nu poate fi întreruptă. Pentru o altă explicație a blocajului, puteți urmări acest videoclip: Deadlock and Livelock Explained .

Livelock

Dacă există blocaj, există și blocaj? Da, există :) Livelock se întâmplă atunci când firele par să fie vii în exterior, dar nu pot face nimic, deoarece nu pot fi îndeplinite condițiile necesare pentru a-și continua munca. Practic, livelock este similar cu deadlock, dar firele nu se „atârnă” în așteptarea unui monitor. În schimb, ei fac mereu ceva. De exemplu:

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();
    }
}
Succesul acestui cod depinde de ordinea în care programatorul de fire Java pornește firele. Dacă Thead-1începe mai întâi, atunci obținem 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
...
După cum puteți vedea din exemplu, ambele fire încearcă să obțină ambele blocări pe rând, dar nu reușesc. Dar, ei nu sunt în impas. În exterior, totul este bine și își fac treaba. Mai bine împreună: Java și clasa Thread.  Partea a III-a — Interacțiune - 4Potrivit JVisualVM, vedem perioade de somn și o perioadă de parcare (acesta este momentul în care un fir încearcă să obțină o blocare - intră în starea de parcare, așa cum am discutat mai devreme când am vorbit despre sincronizarea firului ) . Puteți vedea un exemplu de livelock aici: Java - Thread Livelock .

Foame

Pe lângă blocaj și blocaj, există o altă problemă care se poate întâmpla în timpul multithreadingului: înfometarea. Acest fenomen diferă de formele anterioare de blocare prin faptul că firele de execuție nu sunt blocate - pur și simplu nu au resurse suficiente. Drept urmare, în timp ce unele fire de execuție iau tot timpul de execuție, altele nu pot rula: Mai bine împreună: Java și clasa Thread.  Partea a III-a — Interacțiune - 5

https://www.logicbig.com/

Puteți vedea un super exemplu aici: Java - Thread Starvation and Fairness . Acest exemplu arată ce se întâmplă cu firele în timpul înfometării și cum o mică schimbare de la Thread.sleep()a Thread.wait()vă permite să distribuiți sarcina uniform. Mai bine împreună: Java și clasa Thread.  Partea a III-a — Interacțiune - 6

Condiții de cursă

În multithreading, există o „condiție de cursă”. Acest fenomen se întâmplă atunci când firele de discuție partajează o resursă, dar codul este scris într-un mod care nu asigură partajarea corectă. Aruncă o privire la un exemplu:

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();
    }
}
Este posibil ca acest cod să nu genereze o eroare prima dată. Când se întâmplă, poate arăta astfel:

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)
După cum puteți vedea, ceva a mers prost în timp ce newValuei s-a atribuit o valoare. newValueeste prea mare. Din cauza condiției de cursă, unul dintre fire a reușit să schimbe variabilele valuedintre cele două instrucțiuni. Se pare că există o cursă între fire. Acum gândiți-vă cât de important este să nu faceți greșeli similare cu tranzacțiile monetare... Exemple și diagrame pot fi văzute și aici: Cod pentru a simula condiția de cursă în firul Java .

Volatil

Vorbind despre interacțiunea firelor, volatilecuvântul cheie merită menționat. Să ne uităm la un exemplu simplu:

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;
    }
}
Cel mai interesant este că acest lucru este foarte probabil să nu funcționeze. Noul thread nu va vedea schimbarea în flagdomeniu. Pentru a remedia acest lucru pentru flagcâmp, trebuie să folosim volatilecuvântul cheie. Cum și de ce? Procesorul efectuează toate acțiunile. Dar rezultatele calculelor trebuie stocate undeva. Pentru aceasta, există memorie principală și există memoria cache a procesorului. Cache-urile unui procesor sunt ca o mică bucată de memorie folosită pentru a accesa datele mai rapid decât atunci când accesați memoria principală. Dar totul are un dezavantaj: este posibil ca datele din cache să nu fie actualizate (ca în exemplul de mai sus, când valoarea câmpului steag nu a fost actualizată). Asa cavolatilecuvântul cheie îi spune JVM-ului că nu dorim să ne memorăm variabila în cache. Acest lucru permite ca rezultatul actualizat să fie văzut pe toate firele. Aceasta este o explicație foarte simplificată. În ceea ce privește volatilecuvântul cheie, vă recomand să citiți acest articol . Pentru mai multe informații, vă sfătuiesc, de asemenea, să citiți Java Memory Model și Java Volatile Keyword . În plus, este important să ne amintim că volatileeste vorba despre vizibilitate, și nu despre atomicitatea modificărilor. Privind codul din secțiunea „Condiții de cursă”, vom vedea un sfat explicativ în IntelliJ IDEA: Mai bine împreună: Java și clasa Thread.  Partea a III-a — Interacțiune - 7Această inspecție a fost adăugată la IntelliJ IDEA ca parte a ediției IDEA-61117 , care a fost listată în Notele de lansare încă din 2010.

Atomicitatea

Operațiile atomice sunt operații care nu pot fi împărțite. De exemplu, operația de atribuire a unei valori unei variabile trebuie să fie atomică. Din păcate, operația de creștere nu este atomică, deoarece incrementarea necesită până la trei operații CPU: obțineți valoarea veche, adăugați una la ea, apoi salvați valoarea. De ce este importantă atomicitatea? Cu operația de creștere, dacă există o condiție de cursă, atunci resursa partajată (adică valoarea partajată) se poate schimba brusc în orice moment. În plus, operațiunile care implică structuri pe 64 de biți, de exemplu longși double, nu sunt atomice. Mai multe detalii pot fi citite aici: Asigurați atomicitatea atunci când citiți și scrieți valori pe 64 de biți . Problemele legate de atomicitate pot fi văzute în acest exemplu:

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());
    }
}
Clasa specială AtomicIntegerne va oferi întotdeauna 30.000, dar se valueva schimba din când în când. Există o scurtă prezentare generală a acestui subiect: Introducere în variabilele atomice în Java . Algoritmul de „comparare și schimb” se află în centrul claselor atomice. Puteți citi mai multe despre asta aici în Comparația algoritmilor fără blocare - CAS și FAA pe exemplul JDK 7 și 8 sau în articolul Compare-and-swap de pe Wikipedia. Mai bine împreună: Java și clasa Thread.  Partea a III-a — Interacțiune - 9

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

Se întâmplă-înainte

Există un concept interesant și misterios numit „se întâmplă înainte”. Ca parte a studiului dvs. de fire, ar trebui să citiți despre asta. Relația întâmplă-înainte arată ordinea în care vor fi văzute acțiunile dintre fire. Există multe interpretări și comentarii. Iată una dintre cele mai recente prezentări pe acest subiect: Relațiile Java „Se întâmplă-Înainte” .

rezumat

În această recenzie, am explorat câteva dintre detaliile modului în care firele de discuție interacționează. Am discutat despre problemele care pot apărea, precum și despre modalitățile de identificare și eliminare. Lista de materiale suplimentare pe această temă: Mai bine împreună: Java și clasa Thread. Partea I — Fire de execuție Mai bine împreună: Java și clasa Thread. Partea a II-a — Sincronizare Mai bine împreună: Java și clasa Thread. Partea a IV-a — Apelabil, viitor și prieteni Mai bine împreună: Java și clasa Thread. Partea V — Executor, ThreadPool, Fork/Join Better împreună: Java și clasa Thread. Partea a VI-a — Foc departe!
Comentarii
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION