JavaRush /Blog Java /Random-VI /Управление потоками. Ключевое слово volatile и метод yiel...

Управление потоками. Ключевое слово volatile и метод yield()

Xuất bản trong nhóm
Hello! Мы продолжаем изучение многопоточности, и сегодня познакомимся с новым ключевым словом — volatile и методом yield(). Давай разберемся, что это такое :)

Ключевое слово volatile

При создании многопоточных приложений мы можем столкнуться с двумя серьезными проблемами. Во-первых, в процессе работы многопоточного applications разные потоки могут кэшировать значения переменных (подробнее об этом поговорим в лекции «Применение volatile»). Возможна ситуация, когда один поток изменил meaning переменной, а второй не увидел этого изменения, потому что работал со своей, кэшированной копией переменной. Естественно, последствия могут быть серьезными. Представь, что это не просто Howая-то «переменная», а, например, баланс твоей банковской карты, который вдруг начал рандомно сHowать туда-сюда :) Не очень приятно, да? Во-вторых, в Java операции чтения и записи полей всех типов, кроме long и double, являются атомарными. What такое атомарность? Ну, например, если ты в одном потоке меняешь meaning переменной int, а в другом потоке читаешь meaning этой переменной, ты получишь либо ее старое meaning, либо новое — то, которое получилось после изменения в потоке 1. НиHowих «промежуточных вариантов» там появиться не может. Однако с long и double это не работает. Почему? Из-за кроссплатформенности. Помнишь, мы еще на первых уровнях говорor, что принцип Java — «написано однажды — работает везде»? Это и есть кроссплатформенность. То есть Java-приложение запускается на абсолютно разных платформах. Например, на операционных системах Windows, разных вариантах Linux or MacOS, и везде это приложение будет стабильно работать. long и double — самые «тяжеловесные» примитивы в Java: они весят по 64 бита. И в некоторых 32-битных платформах просто не реализована атомарность чтения и записи 64-битных переменных. Такие переменные читаются и записываются в две операции. Сначала в переменную записываются первые 32 бита, потом еще 32. Соответственно, в этих случаях может возникнуть проблема. Один поток записывает Howое-то 64-битное meaning в переменную Х, и делает он это «в два захода». В то же время второй поток пытается прочитать meaning этой переменной, причем делает это How раз посередине, когда первые 32 бита уже записаны, а вторые — еще нет. В результате он читает промежуточное, некорректное meaning, и получается ошибка. Например, если на такой платформе мы попытаемся записать в переменную число — 9223372036854775809 — оно будет занимать 64 бита. В двоичной форме оно будет выглядеть так: 1000000000000000000000000000000000000000000000000000000000000001 Первый поток начнет запись этого числа в переменную, и сначала запишет первые 32 бита: 10000000000000000000000000000000 а потом вторые 32: 0000000000000000000000000000001 И в этот промежуток может вклиниться второй поток, и прочитать промежуточное meaning переменной — 10000000000000000000000000000000, первые 32 бита, которые уже были записаны. В десятичной системе это число равняется 2147483648. То есть мы всего лишь хотели записать число 9223372036854775809 в переменную, но из-за того, что эта операция на некоторых платформах является не атомарной, у нас из ниоткуда возникло «левое», ненужное нам число 2147483648, и неизвестно How оно повлияет на работу программы. Второй поток просто прочитал meaning переменной до того, How оно окончательно записалось, то есть первые 32 бита он увидел, а вторые 32 бита — нет. Эти проблемы, конечно, возникли не вчера, и в Java они решаются с помощью всего одного ключевого слова — volatile. Если мы объявляем в нашей программе Howую-то переменную, со словом volatile…

public class Main {

   public volatile long x = 2222222222222222222L;
  
   public static void main(String[] args) {
      
   }
}
…это означает, что:
  1. Она всегда будет атомарно читаться и записываться. Даже если это 64-битные double or long.
  2. Java-машина не будет помещать ее в кэш. Так что ситуация, когда 10 потоков работают со своими локальными копиями исключена.
Вот так две очень серьезные проблемы решаются одним словом :)

Метод yield()

Мы рассмотрели уже много методов класса Thread, но есть один важный, который будет для тебя новым. Это метод yield(). С английского переводится How «уступать». И это ровно то, что метод делает! Управление потоками. Ключевое слово volatile и метод yield() - 2Когда мы вызываем метод yield у потока, он фактически говорит другим потокам: «Так, ребята, я никуда особо не тороплюсь, так что если кому-то из вас важно получить время процессора — берите, мне не срочно». Вот простой пример того, How это работает:

public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + "give way to others");
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
Мы последовательно создаем и запускаем три потока — Thread-0, Thread-1 и Thread-2. Thread-0 запускается первым и сразу уступает место другим. После него запускается Thread-1, и тоже уступает. После — запускается Thread-2, который тоже уступает. Больше потоков у нас нет, и после того, How Thread-2 последним уступил свое место, планировщик потоков смотрит: «Так, новых потоков больше нет, кто у нас там в очереди? Кто уступал свое место последним, перед Thread-2? Кажется, это был Thread-1? Окей, значит пусть он и выполняется». Thread-1 выполняет свою работу до конца, после чего планировщик потоков продолжает координацию: «Окей, Thread-1 выполнился. Есть у нас кто-то еще в очереди?». В очереди есть Thread-0: он уступал свое место сразу до Thread-1. Теперь дело дошло до него, и он выполняется до конца. После чего планировщик заканчивает координацию потоков: «Ладно, Thread-2, ты уступил место другим потокам, они все уже отработали. Ты уступал место последним, так что теперь твоя очередь». После этого отрабатывает до конца поток Thread-2. Вывод в консоль будет выглядеть так: Thread-0 уступает свое место другим Thread-1 уступает свое место другим Thread-2 уступает свое место другим Thread-1 закончил выполнение. Thread-0 закончил выполнение. Thread-2 закончил выполнение. Планировщик потоков, конечно, может запустить потоки в другом порядке (например, 2-1-0 instead of 0-1-2), но сам принцип неизменный.

Правила «happens-before»

Последнее, чего мы коснемся сегодня, это принципы «happens before». Как ты уже знаешь, в Java основную часть работы по выделению времени и ресурсов потокам для выполнения их задач выполняет планировщик потоков. Также ты не раз видел, How потоки выполняются в произвольном порядке, и чаще всего предсказать его невозможно. Да и вообще, после «последовательного» программирования, которым мы занимались до этого, многопоточность выглядит рандомной штукой. Как ты уже убедился, ход работы многопоточной программы можно контролировать при помощи целого набора методов. Но в дополнение к этому в многопоточности Java существует еще один «островок стабильности» — 4 правила под названием «happens-before». Дословно с английского это переводится How «происходит перед», or «происходит раньше, чем». Понять смысл этих правил достаточно просто. Представь, что у нас есть два потока — A и B. Каждый из этих потоков может выполнять операции 1 и 2. И когда в каждом из правил мы говорим «A happens-before B», это означает, что все изменения, выполненные потоком A до момента операции 1 и изменения, которые повлекла эта операция, видны потоку B в момент выполнения операции 2 и после выполнения этой операции. Каждое из этих правил дает гарантию, что при написании многопоточной программы одни события в 100% случаев будут происходить раньше, чем другие, и что поток B в момент выполнения операции 2 всегда будет в курсе изменений, которые поток А сделал во время операции 1. Давай рассмотрим их.

Правило 1.

Освобождение мьютекса happens before происходит раньше захвата этого же монитора другим потоком. Ну, тут вроде все понятно. Если мьютекс an object or класса захвачен одним потоком, например, потоком А, другой поток (поток B) не может в это же время его захватить. Нужно подождать, пока мьютекс не освободится.

Правило 2.

Метод Thread.start() happens before Thread.run(). Тоже ничего сложного. Ты уже знаешь: чтобы начал выполняться code внутри метода run(), необходимо вызвать у потока метод start(). Именно его, а не сам метод run()! Это правило гарантирует, что установленные до запуска Thread.start() значения всех переменных будут видны внутри начавшего выполнение метода run().

Правило 3.

Завершение метода run() happens before выход из метода join(). Вернемся к нашим двум потокам — А и B. Мы вызываем метод join() таким образом, чтобы поток B обязательно дождался завершения A, прежде чем выполнять свою работу. Это означает, что метод run() an object A обязательно отработает до самого конца. И все изменения в данных, которые произойдут в методе run() потока A стопроцентно будут видны в потоке B, когда он дождется завершения A и начнет работу сам.

Правило 4.

Запись в volatile переменную happens-before чтение из той же переменной. При использовании ключевого слова volatile мы, фактически, всегда будем получать актуальное meaning. Даже в случае с long и double, о проблемах с которыми говорилось ранее. Как ты уже понял, изменения, сделанные в одних потоках, далеко не всегда видны другим потокам. Но, конечно, очень часто встречаются ситуации, когда подобное поведение программы нас не устраивает. Допустим, в потоке A мы присвоor meaning переменной:

int z;.

z= 555;
Если наш поток B должен вывести meaning переменной z на консоль, он requestто может вывести 0, потому что не знает о присвоенном ей значении. Так вот, Правило 4 гарантирует нам: если объявить переменную z How volatile, изменения ее значений в одном потоке всегда будут видны в другом потоке. Если мы добавим в предыдущий code слово volatile...

volatile int z;.

z= 555;
...ситуация, при которой поток B выведет в консоль 0, исключена. Запись в volatile-переменные происходит раньше, чем чтение из них.
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION