JavaRush /Java блогы /Random-KK /Сіз Java-ны жіппен бүлдіре алмайсыз: III бөлім - Өзара әр...
Viacheslav
Деңгей

Сіз Java-ны жіппен бүлдіре алмайсыз: III бөлім - Өзара әрекеттесу

Топта жарияланған
Жіптің өзара әрекеттесу ерекшеліктеріне қысқаша шолу. Бұрын біз ағындардың бір-бірімен синхрондалатынын қарастырдық. Бұл жолы біз ағындар өзара әрекеттесу кезінде туындауы мүмкін проблемаларды қарастырамыз және оларды қалай болдырмауға болатыны туралы сөйлесеміз. Біз сондай-ақ тереңірек зерттеу үшін кейбір пайдалы сілтемелерді береміз. Сіз Java-ны жіппен бұза алмайсыз: III бөлім - өзара әрекеттесу - 1

Кіріспе

Сонымен, біз Java-да ағындар бар екенін білеміз, олар туралы « Java-ны бұза алмайды: I бөлім - жіптер » шолуында оқуға болады және ағындар бір-бірімен синхрондалады, біз оларды шолуда қарастырдық « Thread Can't Spoil Java ” Бұзылуы: II бөлім - Синхрондау ." Жіптердің бір-бірімен қалай әрекеттесетіні туралы айтудың уақыты келді. Олар ортақ ресурстарды қалай бөліседі? Осыған байланысты қандай проблемалар болуы мүмкін?

Тұйық

Ең қиын мәселе - тығырыққа тірелу. Екі немесе одан да көп ағындар бір-бірін мәңгі күткенде, бұл Тұйықталу деп аталады. Oracle веб-сайтынан « Тұйыққа тірелу » түсінігінің сипаттамасынан мысал келтірейік :
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();
    }
}
Мұндағы тығырықтан шығу бірінші рет пайда болмауы мүмкін, бірақ сіздің бағдарламаңыздың орындалуы тоқтап қалса, іске қосу уақыты келді jvisualvm: Сіз Java-ны жіппен бұза алмайсыз: III бөлім - өзара әрекеттесу - 2JVisualVM жүйесінде плагин орнатылған болса (Құралдар -> Плагиндер арқылы), біз тығырықтың қай жерде орын алғанын көре аламыз:
"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
1-ші ағын 0-ден құлыптауды күтуде. Неліктен бұл орын алады? Thread-1орындауды бастайды және әдісті орындайды Friend#bow. Ол кілт сөзбен белгіленеді synchronized, яғни біз мониторды арқылы аламыз this. Әдіске кіре берісте біз басқасына сілтеме алдық Friend. Енді жіп Thread-1басқа әдісті орындағысы келеді Friend, осылайша одан да құлып алады. Бірақ егер басқа ағын (бұл жағдайда Thread-0) әдіске кіре алса bow, онда құлып қазірдің өзінде бос емес және Thread-1күтуде Thread-0және керісінше. Блоктау шешілмейді, сондықтан ол Өлі, яғни өлі. Өлім тұтқасы (оны босату мүмкін емес) және қашып құтыла алмайтын өлі блок. Тұйықталу тақырыбы бойынша сіз бейнені көре аласыз: " Тұйықталу - №1 параллельдік - кеңейтілген Java ".

Livelock

Егер тығырыққа тірелсе, онда Livelock бар ма? Иә, бар) Livelock - бұл жіптер сыртқы жағынан тірі болып көрінеді, бірақ сонымен бірге олар ештеңе істей алмайды, өйткені ... олардың жұмысын жалғастыруға тырысатын шартты орындау мүмкін емес. Негізінде Livelock тұйыққа ұқсайды, бірақ ағындар мониторды күтіп тұрған жүйеде «ілулі» қалмайды, бірақ әрқашан бірдеңе жасайды. Мысалы:
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();
    }
}
Бұл codeтың сәтті болуы Java ағынын жоспарлаушы ағындарды бастау ретіне байланысты. Егер ол бірінші басталса Thead-1, біз 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
...
Мысалдан көрініп тұрғандай, екі ағын да кезек-кезек екі құлыпты ұстауға тырысады, бірақ олар сәтсіздікке ұшырайды. Оның үстіне, олар тығырыққа тірелген жоқ, яғни көзбен қарағанда оларда бәрі жақсы және олар өз жұмысын істеп жатыр. JVisualVM сәйкес, біз ұйқы кезеңдерін және парк кезеңін көреміз (бұл жіп құлыпты иеленуге тырысқанда, ол ағынды синхрондауСіз Java-ны жіппен бұза алмайсыз: III бөлім - өзара әрекеттесу - 3 туралы бұрын талқылағанымыздай, парк күйіне өтеді ). Livelock тақырыбында сіз мысалды көре аласыз: " Java - Thread Livelock ".

Аштық

Блоктаудан басқа (тұйықталу және тығырықтану) көп ағынмен жұмыс істегенде тағы бір мәселе бар - Аштық немесе «аштық». Бұл құбылыс блоктаудан ағындар бұғатталмағанымен ерекшеленеді, бірақ олардың барлығына жеткілікті ресурстары жоқ. Сондықтан, кейбір ағындар барлық орындау уақытын алады, ал басқаларын орындау мүмкін емес: Сіз Java-ны жіппен бұза алмайсыз: III бөлім - өзара әрекеттесу - 4

https://www.logicbig.com/

Керемет мысалды мына жерден табуға болады: " Java - Thread Starvation and Fairness ". Бұл мысал ағындардың аштықта қалай жұмыс істейтінін және Thread.sleep-тен Thread.wait-ке бір кішкене өзгерістің жүктемені біркелкі тарата алатынын көрсетеді. Сіз Java-ны жіппен бұза алмайсыз: III бөлім - өзара әрекеттесу - 5

Жарыс шарты

Көп ағынмен жұмыс істегенде «жарыс шарты» деген ұғым бар. Бұл құбылыс ағындардың белгілі бір ресурсты өзара бөлісуінде және code бұл жағдайда дұрыс жұмыс істеуді қамтамасыз етпейтін етіп жазылуында жатыр. Мысал қарастырайық:
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();
    }
}
Бұл code бірінші рет қате тудырмауы мүмкін. Және бұл келесідей болуы мүмкін:
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)
Көріп отырғаныңыздай, оны тағайындау кезінде newValueбірдеңе дұрыс болмады және newValueодан да көп болды. Жарыс күйіндегі кейбір жіптер valueосы екі команда арасында өзгере алды. Көріп отырғанымыздай, жіптер арасындағы жарыс пайда болды. Енді ақша транзакцияларында осындай қателіктер жібермеу қаншалықты маңызды екенін елестетіп көріңіз... Мысалдар мен диаграммаларды мына жерден табуға болады: “ Java ағынындағы жарыс жағдайын имитациялау codeы ”.

Ұшқыш

Жіптердің өзара әрекеттесуі туралы айта отырып, кілт сөзді ерекше атап өткен жөн volatile. Қарапайым мысалды қарастырайық:
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;
    }
}
Ең қызығы, жоғары ықтималдықпен ол жұмыс істемейді. Жаңа ағын өзгерісті көрмейді flag. Мұны түзету үшін өріс үшін flagкілт сөзді көрсету керек volatile. Қалай және неге? Барлық әрекеттерді процессор орындайды. Бірақ есептеу нәтижелерін бір жерде сақтау керек. Осы мақсатта процессорда негізгі жады және аппараттық кэш бар. Бұл процессор кэштері негізгі жадқа қол жеткізуге қарағанда деректерге жылдам қол жеткізуге арналған жадтың кішкене бөлігі сияқты. Бірақ бәрінің де кемшілігі бар: кэштегі деректер ағымдағы болмауы мүмкін (жоғарыдағы мысалдағыдай, жалаушаның мәні жаңартылмаған кезде). Сонымен, кілт сөз volatileJVM-ге айнымалы мәнімізді кэштегіміз келмейтінін айтады. Бұл барлық ағындардағы нақты нәтижені көруге мүмкіндік береді. Бұл өте жеңілдетілген тұжырым. Бұл тақырып бойынша « JSR 133 (Java жады үлгісі)volatile жиі қойылатын сұрақтар» аудармасын оқу ұсынылады . Сондай-ақ, мен сізге « Java жады үлгісі » және « Java тұрақты кілт сөзі » материалдары туралы көбірек оқуға кеңес беремін . Сонымен қатар, бұл өзгерістердің атомдылығы туралы емес, көріну туралы екенін есте ұстаған жөн . Егер біз «Жарыс шартынан» codeты алсақ, IntelliJ Idea-да кеңесті көреміз: Бұл тексеру (тексеру) IntelliJ Idea-ға IDEA-61117 шығарылымының бөлігі ретінде қосылды , ол 2010 жылы шығарылым ескертпелерінде тізімделген .volatileСіз Java-ны жіппен бұза алмайсыз: III бөлім - өзара әрекеттесу - 6

Атомдық

Атомдық операциялар деп бөлуге болмайтын операцияларды айтады. Мысалы, айнымалыға мән беру операциясы атомдық болып табылады. Өкінішке орай, өсу атомдық операция емес, өйткені өсім үш әрекетті қажет етеді: ескі мәнді алыңыз, оған біреуін қосыңыз және мәнді сақтаңыз. Неліктен атомдық маңызды? Үстеме мысалда жарыс шарты орын алса, кез келген уақытта ортақ ресурс (яғни, ортақ мән) кенеттен өзгеруі мүмкін. Сонымен қатар, 64-биттік құрылымдар да атомдық емес болуы маңызды, мысалы longжәне double. Толығырақ мына жерден оқи аласыз: " 64 биттік мәндерді оқу және жазу кезінде атомдылықты қамтамасыз етіңіз ". Атомдық есептердің мысалын келесі мысалда көруге болады:
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());
    }
}
Атоммен жұмыс істеуге арналған арнайы сынып Integerбізге әрқашан 30000 көрсетеді, бірақ valueол мезгіл-мезгіл өзгереді. Осы тақырып бойынша қысқаша шолу бар « Java тіліндегі атомдық айнымалыларға кіріспе ». Atomic «Салыстыру және ауыстыру» алгоритміне негізделген. Бұл туралы толығырақ Habré мақаласында « JDK 7 және 8 мысалдарын қолдана отырып, құлыпсыз алгоритмдерді - CAS және FAA салыстыру » немесе Википедиядағы « Айырбаспен салыстыру » туралы мақаладан оқи аласыз . Сіз Java-ны жіппен бұза алмайсыз: III бөлім - өзара әрекеттесу - 8

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

Бұрын болады

Қызықты және жұмбақ нәрсе бар - Бұрын болады. Ағындар туралы айтатын болсақ, бұл туралы да оқу керек. Бұрын болады қатынасы ағындар арасындағы әрекеттердің көріну ретін көрсетеді. Көптеген түсіндірмелер мен түсіндірулер бар. Осы тақырып бойынша ең соңғы есептердің бірі мына есеп:
Бұл бейнеде бұл туралы ештеңе айтылмағаны жақсы шығар. Сондықтан мен видеоның сілтемесін қалдырамын. Сіз « Java - қарым-қатынасқа дейін болатынын түсіну » бөлімін оқи аласыз .

Нәтижелер

Бұл шолуда біз жіптердің өзара әрекеттесу ерекшеліктерін қарастырдық. Біз туындауы мүмкін проблемаларды және оларды анықтау және жою жолдарын талқыладық. Тақырып бойынша қосымша материалдар тізімі: #Вячеслав
Пікірлер
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION