CodeGym/Java блог/Случаен/По-добре заедно: Java и клас Thread. Част III — Взаимодей...
John Squirrels
Ниво
San Francisco

По-добре заедно: Java и клас Thread. Част III — Взаимодействие

Публикувано в групата
Кратък преглед на подробностите за взаимодействието на нишките. Преди това разгледахме How нишките се синхронизират един с друг. Този път ще се потопим в проблемите, които могат да възникнат при взаимодействие на нишки, и ще говорим за това How да ги избегнем. Ще предоставим и някои полезни връзки за по-задълбочено проучване. По-добре заедно: Java и клас Thread.  Част III — Взаимодействие - 1

Въведение

И така, знаем, че Java има нишки. Можете да прочетете за това в ревюто, озаглавено По-добре заедно: Java и класът Thread. Част I — Нишки на изпълнение . И ние проучихме факта, че нишките могат да се синхронизират една с друга в рецензията, озаглавена По-добре заедно: Java и класът Thread. Част II — Синхронизация . Време е да поговорим за това How нишките взаимодействат една с друга. Как споделят споделени ресурси? Какви проблеми могат да възникнат тук? По-добре заедно: Java и клас Thread.  Част III — Взаимодействие - 2

Безизходица

Най-страшният проблем от всички е задънената улица. Безизходица е, когато две or повече нишки вечно чакат другата. Ще вземем пример от уеб pageта на 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 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();
    }
}
Тук може да не се появи безизходица за първи път, но ако програмата ви увисне, значи е време да стартирате jvisualvm: По-добре заедно: Java и клас Thread.  Част III — Взаимодействие - 3С инсталиран плъгин JVisualVM (чрез Инструменти -> Добавки), можем да видим къде е възникнала блокировката:
"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
Нишка 1 чака заключването от нишка 0. Защо се случва това? Thread-1започва да работи и изпълнява Friend#bowметода. Той е маркиран с synchronizedключовата дума, което означава, че придобиваме монитора за this(текущия обект). Входът на метода беше препратка към другия Friendобект. Сега Thread-1иска да изпълни метода на другия Friendи трябва да получи неговото заключване, за да го направи. Но ако другата нишка (в този случай Thread-0) успя да влезе в bow()метода, тогава заключването вече е придобито и Thread-1чакаThread-0, и обратно. Тази задънена улица е неразрешима и ние я наричаме задънена улица. Като смъртоносна хватка, която не може да бъде освободена, задънената улица е взаимно блокиране, което не може да бъде прекъснато. За друго обяснение на безизходното блокиране можете да гледате това видео: Обяснено блокиране и блокиране на живо .

Livelock

Ако има безизходица, има ли и livelock? Да, има :) Livelock се случва, когато нишките външно изглеждат живи, но не са в състояние да направят нищо, тъй като conditionто(ата), изисквани за тях, за да продължат работата си, не може да бъде изпълнено. По принцип livelock е подобен на deadlock, но нишките не "висят" в очакване на монитор. Вместо това те вечно правят нещо. Например:
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
...
Както можете да видите от примера, и двете нишки се опитват да придобият двете ключалки на свой ред, но не успяват. Но те не са в задънена улица. Външно всичко е наред и си вършат работата. По-добре заедно: Java и клас Thread.  Част III — Взаимодействие - 4Според JVisualVM виждаме периоди на заспиване и период на паркиране (това е, когато нишка се опитва да получи заключване — тя влиза в състояние на паркиране, Howто обсъдихме по-рано, когато говорихме за синхронизация на нишки ) . Можете да видите пример за livelock тук: Java - Thread Livelock .

Гладуване

В допълнение към блокиране и блокиране на живо, има друг проблем, който може да възникне по време на многопоточност: глад. Това явление се различава от предишните форми на блокиране по това, че нишките не са блокирани — те просто нямат достатъчно ресурси. В резултат на това, докато някои нишки отнемат цялото време за изпълнение, други не могат да се изпълняват: По-добре заедно: Java и клас Thread.  Част III — Взаимодействие - 5

https://www.logicbig.com/

Можете да видите супер пример тук: Java - Thread Starvation and Fairness . Този пример показва Howво се случва с нишките по време на гладуване и How една малка промяна от Thread.sleep()към Thread.wait()ви позволява да разпределите натоварването равномерно. По-добре заедно: Java и клас Thread.  Част III — Взаимодействие - 6

Състезателни условия

В многонишковостта има такова нещо като "състезание". Това явление се случва, когато нишките споделят ресурс, но 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 .

Летлив

Говорейки за взаимодействието на нишките, 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ключовата дума. Как и защо? Процесорът изпълнява всички действия. Но резултатите от изчисленията трябва да се съхраняват някъде. За това има основна памет и има кеш памет на процесора. Кешовете на процесора са като малка част от паметта, използвана за по-бърз достъп до данни, отколкото при достъп до основната памет. Но всичко има обратна страна: данните в кеша може да не са актуални (Howто в примера по-горе, когато стойността на полето за флаг не е актуализирана). Така чеvolatileключова дума казва на JVM, че не искаме да кешираме нашата променлива. Това позволява актуалният резултат да се вижда във всички теми. Това е силно опростено обяснение. Що се отнася до volatileключовата дума, горещо ви препоръчвам да прочетете тази статия . За повече информация също ви съветвам да прочетете Java Memory Model и Java Volatile Keyword . Освен това е важно да запомните, че volatileстава въпрос за видимостта, а не за атомарността на промените. Разглеждайки codeа в раздела „Условия на състезанието“, ще видим подсказка в IntelliJ IDEA: По-добре заедно: Java и клас Thread.  Част III — Взаимодействие - 7Тази проверка беше добавена към IntelliJ IDEA като част от проблем IDEA-61117 , който беше посочен в бележките по изданието през 2010 г.

Атомност

Атомарните операции са операции, които не могат да бъдат разделени. Например операцията за присвояване на стойност на променлива трябва да бъде атомарна. За съжаление, операцията за увеличаване не е атомарна, тъй като увеличаването изисква до три операции на процесора: вземете старата стойност, добавете една към нея и след това запазете стойността. Защо атомарността е важна? С операцията за нарастване, ако има състояние на състезание, тогава споделеният ресурс (т.е. споделената стойност) може внезапно да се промени по всяко време. Освен това операциите, включващи 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());
    }
}
Специалният AtomicIntegerклас винаги ще ни дава 30 000, но valueще се променя от време на време. Има кратък преглед на тази тема: Въведение в атомарните променливи в Java . Алгоритъмът "сравняване и размяна" е в основата на атомните класове. Можете да прочетете повече за това тук в Сравнение на алгоритми без заключване - CAS и FAA на примера на JDK 7 и 8 or в статията Compare and swap в Wikipedia. По-добре заедно: Java и клас Thread.  Част III — Взаимодействие - 9

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

Случва се-преди

Има една интересна и мистериозна концепция, наречена „случва се преди“. Като част от изучаването на нишките, трябва да прочетете за това. Връзката случва-преди показва реда, в който ще се виждат действията между нишките. Има много тълкувания и коментари. Ето една от най-новите презентации по тази тема: Връзки "Случва се преди" в Java .

Резюме

В този преглед проучихме някои от спецификите на взаимодействието между нишките. Обсъдихме проблемите, които могат да възникнат, Howто и начините за тяхното идентифициране и отстраняване. Списък с допълнителни материали по темата: По-добре заедно: Java и клас Thread. Част I — Нишки за изпълнение По-добре заедно: Java и класът Thread. Част II — По-добра синхронизация заедно: Java и класът Thread. Част IV — Callable, Future и приятели По-добре заедно: Java и класът Thread. Част V — Executor, ThreadPool, Fork/Join Better заедно: Java и класът Thread. Част VI — Изстрелвай!
Коментари
  • Популярен
  • Нов
  • Стар
Трябва да сте влезли, за да оставите коментар
Тази страница все още няма коментари