JavaRush /جاوا بلاگ /Random-UR /آپ دھاگے کے ساتھ جاوا کو خراب نہیں کر سکتے: حصہ III - تعا...
Viacheslav
سطح

آپ دھاگے کے ساتھ جاوا کو خراب نہیں کر سکتے: حصہ III - تعامل

گروپ میں شائع ہوا۔
دھاگے کے تعامل کی خصوصیات کا ایک مختصر جائزہ۔ پہلے، ہم نے دیکھا کہ تھریڈز ایک دوسرے کے ساتھ کیسے ہم آہنگ ہوتے ہیں۔ اس بار ہم ان مسائل کا جائزہ لیں گے جو اس وقت پیدا ہو سکتے ہیں جب تھریڈز آپس میں بات کرتے ہیں اور ان سے کیسے بچا جا سکتا ہے۔ ہم گہرے مطالعہ کے لیے کچھ مفید لنکس بھی فراہم کریں گے۔ Thread'ом Java не испортишь: Часть III — взаимодействие - 1

تعارف

لہذا، ہم جانتے ہیں کہ جاوا میں تھریڈز موجود ہیں، جن کے بارے میں آپ جائزہ میں پڑھ سکتے ہیں " تھریڈ جاوا کو خراب نہیں کر سکتا: حصہ اول - تھریڈز " اور یہ کہ تھریڈز ایک دوسرے کے ساتھ ہم آہنگ ہو سکتے ہیں، جن کے بارے میں ہم نے جائزہ میں نمٹا تھا۔ دھاگہ جاوا کو خراب نہیں کر سکتا ” خراب کریں: حصہ دوم - ہم وقت سازی ۔ یہ اس بارے میں بات کرنے کا وقت ہے کہ دھاگے ایک دوسرے کے ساتھ کیسے تعامل کرتے ہیں۔ وہ مشترکہ وسائل کیسے بانٹتے ہیں؟ اس کے ساتھ کیا مسائل ہو سکتے ہیں؟

تعطل

سب سے بڑا مسئلہ ڈیڈ لاک ہے۔ جب دو یا دو سے زیادہ دھاگے ہمیشہ ایک دوسرے کا انتظار کرتے ہیں تو اسے ڈیڈ لاک کہتے ہیں۔ آئیے " ڈیڈ لاک " کے تصور کی تفصیل سے اوریکل ویب سائٹ سے ایک مثال لیتے ہیں :
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: Thread'ом Java не испортишь: Часть III — взаимодействие - 2اگر JVisualVM (ٹولز -> پلگ ان کے ذریعے) میں پلگ ان انسٹال ہے، تو ہم دیکھ سکتے ہیں کہ تعطل کہاں واقع ہوا:
"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، اور اس کے برعکس۔ مسدود کرنا ناقابل حل ہے، لہذا یہ مردہ ہے، یعنی مردہ۔ موت کی گرفت (جسے چھوڑا نہیں جا سکتا) اور ایک مردہ بلاک جس سے کوئی بچ نہیں سکتا۔ تعطل کے موضوع پر، آپ ویڈیو دیکھ سکتے ہیں: " Deadlock - Concurrency #1 - Advanced Java

لائیو لاک

اگر ڈیڈ لاک ہے تو کیا لائیو لاک ہے؟ جی ہاں، وہاں ہے) لائیو لاک یہ ہے کہ دھاگے ظاہری طور پر زندہ نظر آتے ہیں، لیکن ساتھ ہی وہ کچھ نہیں کر سکتے، کیونکہ... جس حالت میں وہ اپنا کام جاری رکھنے کی کوشش کر رہے ہیں اسے پورا نہیں کیا جا سکتا۔ جوہر میں، 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();
    }
}
اس کوڈ کی کامیابی کا انحصار اس ترتیب پر ہے جس میں جاوا تھریڈ شیڈیولر تھریڈز کو شروع کرتا ہے۔ اگر یہ پہلے شروع ہوتا ہے 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
...
Как видно из примера, оба потока поочерёдно пытаются захватить оба лока, но им это не удаётся. При этом они не в deadlock, то есть визуально с ними всё хорошо и они выполняют свою работу. Thread'ом Java не испортишь: Часть III — взаимодействие - 3По JVisualVM мы видим периоды sleep и период park (это когда поток пытается занять лок, он переходит в состояние park, How мы разбирали ранее, говоря про синхронизацию потоков). На тему лайвлока можно посмотреть пример: "Java - Thread Livelock".

Starvation

Помимо блокировок (deadlock и livelock) есть ещё одна проблема при работе с многопоточностью — Starvation, or "голодание". От блокировок это явление отличается тем, что потоки не заблокированы, а им просто не хватает ресурсов на всех. Поэтому пока одни потоки на себя берут всё время выполнения, другие не могут выполниться: Thread'ом Java не испортишь: Часть III — взаимодействие - 4

https://www.logicbig.com/

Супер пример можно посмотреть здесь: "Java - Thread Starvation and Fairness". В этом примере показано, How работают потоки при Starvation и How одно маленькое изменение с Thread.sleep на Thread.wait позволяет распределить нагрузку равномерно. Thread'ом Java не испортишь: Часть III — взаимодействие - 5

Race Condition

При работе с многопоточностью есть такое понятие, How "состояние гонки". Это явление заключается в том, что потоки делят между собой некоторый ресурс и 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 между этими двумя командам. Как мы видим, проявилась гонка между потоками. А теперь представьте, How важно не совершать похожие ошибки с денежными операциями... Примеры и схемы можно посмотреть ещё и здесь: "Code to simulate race condition in Java thread".

Volatile

Говоря про взаимодействие потоков стоит особо отметить ключевое слово 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. Whatбы это исправить для поля flag нужно указать ключевое слово volatile. Как же так и почему? Все действия выполняет процессор. Но результаты вычислений нужно где-то хранить. Для этого есть основная память и есть аппаратный кэш у процессора. Эти кэши процессора — своего рода маленький кусочек памяти для более быстрого обращения к данным, чем обращения к основной памяти. Но у всего есть и минус: данные в кэше могут быть не актуальны (How в примере выше, когда meaning флага не обновилось). Так вот, ключевое слово volatile указывает JVM, что мы не хотим кэшировать нашу переменную. Это позволяет увидеть актуальный результат во всех потоках. Это очень упрощённая формулировка. На тему volatile настоятельно рекомендуется к прочтению перевод "JSR 133 (Java Memory Model) FAQ". Подробнее советую также ознакомиться с материалами "Java Memory Model" и "Java Volatile Keyword". Кроме того, важно помнить, что volatile — это про видимость, а не про атомарность измений. Если взять code из "Race Condition", то мы увидим в IntelliJ Idea подсказку: Thread'ом Java не испортишь: Часть III — взаимодействие - 6Данная проверка (Inspection) была добавлена в IntelliJ Idea в рамках issue IDEA-61117, который указан в Release Notes ещё в далёком 2010 году.

Атомарность

Атомарные операции — это операции, которые нельзя разделить. Например, операция присваивания значения переменной — атомарная. К сожалению, инкремент не является атомарной операцией, т.к. для инкремента требуется аж три операции: получить старое meaning, прибавить к нему единицу, сохранить meaning. Почему важна атомарность? В примере с инкрементом, если появится состояние гонки, в любой момент общий ресурс (т.е. общее meaning) может внезапно измениться. Кроме того, важно, что 64-битные структуры тоже не атомарны, например long и double. Подробнее можно прочитать тут: "Ensure atomicity when reading and writing 64-bit values". Пример проблем с атомарностью можно увидеть на примере:
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 будет меняться от раза к разу. На эту тему есть небольшой обзор "An Introduction to Atomic Variables in Java". В основе Atomic'ов лежит алгоритм "Compare-and-Swap". Подробнее про него можно прочитать в статье на хабре "Сравнение Lock-free алгоритмов — CAS и FAA на примере JDK 7 и 8" or на википедии в статье про "Сравнение с обменом". Thread'ом Java не испортишь: Часть III — взаимодействие - 8

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

Happens Before

Есть интересная и загадочная штука — Happens Before. Рассуждая о потоках, про неё стоит тоже прочитать. Отношение Happens Before показывает, в Howом порядке будут видны действия между потоками. Существует немало трактовок и толкований. Одним из самых последних докладов на эту тему является вот этот доклад:
Наверно, лучше, чем это видео ничего не расскажет про это. Поэтому, я просто оставлю ссылку на видео. Прочитать можно "Java - Understanding Happens-before relationship".

Итоги

В данном обзоре мы посмотрели на особенности взаимодействия потоков. Обсудor проблемы, которые могут возникнуть и способы их обнаружения и устранения. Список дополнительных материалов по теме: #Viacheslav
تبصرے
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION