Параллелизм Java – Разница между yield() и join()

Многопоточность — очень популярная тема среди интервьюеров уже давно. Хотя я лично считаю, что очень немногие из нас получают реальный шанс поработать над сложным многопоточным приложением(у меня был только один шанс за последние 7 лет), все же это помогает иметь концепции под рукой, чтобы повысить вашу уверенность ТОЛЬКО. Ранее я обсуждал похожий вопрос о разнице между методами wait() и sleep(), на этот раз я обсуждаю разницу между методами join() и yield(). Честно говоря, я не использовал ни один из этих методов на практике, поэтому, пожалуйста, приведите аргумент, если вы считаете иначе в любой момент.

Немного информации о планировании потоков Java

Виртуальная машина Java должна реализовать вытесняющий, приоритетный планировщик среди своих различных потоков. Это означает, что каждому потоку в программе Java назначается определенный приоритет, положительное целое число, которое попадает в четко определенный диапазон. Этот приоритет может быть изменен разработчиком. Виртуальная машина Java никогда не изменяет приоритет потока, даже если поток выполняется в течение определенного периода времени.

Значение приоритета важно, поскольку контракт между виртуальной машиной Java и базовой операционной системой заключается в том, что операционная система должна, как правило, выбирать для запуска поток Java с наивысшим приоритетом. Именно это мы имеем в виду, когда говорим, что Java реализует планировщик на основе приоритетов. Этот планировщик реализован в упреждающем режиме, что означает, что когда появляется поток с более высоким приоритетом, этот поток прерывает(вытесняет) любой поток с более низким приоритетом, работающий в это время. Однако контракт с операционной системой не является абсолютным, что означает, что операционная система иногда может выбрать для запуска поток с более низким приоритетом. [Ненавижу это в многопоточности… ничего не гарантировано 🙁 ]

Также обратите внимание, что java не требует, чтобы ее потоки были квантованы по времени, но большинство операционных систем это делают. Здесь часто возникает путаница в терминологии: вытеснение часто путают с квантованием по времени. Фактически, вытеснение означает только то, что поток с более высоким приоритетом запускается вместо потока с более низким приоритетом, но когда потоки имеют одинаковый приоритет, они не вытесняют друг друга. Обычно они подвергаются квантованию по времени, но это не является требованием Java.

Понимание приоритетов потоков

Понимание приоритетов потоков — это следующий важный шаг в изучении многопоточности и, в частности, того, как работает yield().

  1. Помните, что все потоки имеют обычный приоритет, если приоритет не указан.
  2. Приоритеты можно указать от 1 до 10. 10 — наивысший приоритет, 1 — низший приоритет, а 5 — нормальный приоритет.
  3. Помните, что поток с наивысшим приоритетом будет иметь преимущество при выполнении. Но нет гарантии, что он будет в состоянии выполнения в момент запуска.
  4. Всегда текущий выполняющийся поток может иметь более высокий приоритет по сравнению с потоками в пуле, которые ждут своего шанса.
  5. Планировщик потоков решает, какой поток следует выполнить.
  6. t.setPriority() можно использовать для установки приоритетов потоков.
  7. Помните, что приоритеты следует устанавливать до вызова метода запуска потоков.
  8. Для установки приоритетов можно использовать константы MIN_PRIORITY, MAX_PRIORITY и NORM_PRIORITY.

Теперь, когда у нас есть базовые знания о планировании потоков и приоритетах потоков, давайте перейдем к сути.

метод yield()

Теоретически, «уступить» означает отпустить, сдаться, сдаться. Уступающий поток сообщает виртуальной машине, что он готов позволить другим потокам быть запланированными вместо него. Это указывает на то, что он не делает что-то слишком критическое. Обратите внимание, что это всего лишь намек, и не гарантируется, что он даст какой-либо эффект.

yield() определен в Thread.java следующим образом.

/*** A hint to the scheduler that the current thread is willing to yield its current use of a processor. The scheduler is free to ignore* this hint. Yield is a heuristic attempt to improve relative progression between threads that would otherwise over-utilize a CPU.* Its use should be combined with detailed profiling and benchmarking to ensure that it actually has the desired effect.*/public static native void yield();

Давайте перечислим важные моменты из приведенного выше определения:

  • Yield — это статический метод, а также нативный.
  • Yield сообщает текущему выполняемому потоку о необходимости предоставить шанс потокам, имеющим равный приоритет в пуле потоков.
  • Нет никакой гарантии, что Yield немедленно переведет текущий выполняющийся поток в работоспособное состояние.
  • Он может только перевести поток из состояния выполнения в состояние готовности к выполнению, но не в состояние ожидания или блокировки.

пример использования метода yield()

В примере программы ниже я создал два потока с именами producer и consumer без какой-либо конкретной причины. Producer установлен на минимальный приоритет, а consumer — на максимальный приоритет. Я запущу код ниже с комментированием или без него строки Thread.yield(). Без yield(), хотя вывод иногда меняется, но обычно сначала печатаются все строки consumer, а затем все строки produce.

При использовании метода yield() оба метода почти всегда выводят по одной строке за раз и передают эту возможность другому потоку.

package test.core.threads;public class YieldExample{public static void main(String[] args){Thread producer = new Producer();Thread consumer = new Consumer();producer.setPriority(Thread.MIN_PRIORITY); //Min Priorityconsumer.setPriority(Thread.MAX_PRIORITY); //Max Priorityproducer.start();consumer.start();}}class Producer extends Thread{public void run(){for(int i = 0; i < 5; i++){System.out.println("I am Producer : Produced Item " + i);Thread.yield();}}}class Consumer extends Thread{public void run(){for(int i = 0; i < 5; i++){System.out.println("I am Consumer : Consumed Item " + i);Thread.yield();}}}

Вывод вышеуказанной программы «без» метода yield()

 Я потребитель: Потребляемый товар 0Я потребитель: Потребляемый товар 1Я потребитель: Потребляемый товар 2Я потребитель: Потребляемый товар 3Я потребитель: Потребляемый товар 4Я продюсер: Произведено 0 предметовЯ продюсер: Произведенный элемент 1Я продюсер: Произведенный элемент 2Я продюсер: Произведенный элемент 3Я продюсер: Произведенный элемент 4

Вывод программы выше «с» добавленным методом yield()

 Я продюсер: Произведено 0 предметовЯ потребитель: Потребляемый товар 0Я продюсер: Произведенный элемент 1Я потребитель: Потребляемый товар 1Я продюсер: Произведенный элемент 2Я потребитель: Потребляемый товар 2Я продюсер: Произведенный элемент 3Я потребитель: Потребляемый товар 3Я продюсер: Произведенный элемент 4Я потребитель: Потребляемый товар 4

метод join()

Метод join() экземпляра Thread может использоваться для «присоединения» начала выполнения потока к концу выполнения другого потока, так что поток не начнет выполняться, пока не завершится другой поток. Если join() вызывается для экземпляра Thread, текущий выполняющийся поток будет заблокирован до тех пор, пока экземпляр Thread не завершит выполнение.

//Waits for this thread to die.public final void join() throws InterruptedException

Указание тайм-аута в join() приведет к тому, что эффект join() будет аннулирован после указанного тайм-аута. Когда тайм-аут достигнут, основной поток и taskThread являются равновероятными кандидатами на выполнение. Однако, как и в случае со сном, join зависит от ОС в плане времени, поэтому не следует предполагать, что join будет ждать ровно столько, сколько вы укажете.

Как и sleep, join реагирует на прерывание выходом с InterruptedException.

пример использования метода join()

package test.core.threads;public class JoinExample{public static void main(String[] args) throws InterruptedException{Thread t = new Thread(new Runnable(){public void run(){System.out.println("First task started");System.out.println("Sleeping for 2 seconds");try{Thread.sleep(2000);} catch(InterruptedException e){e.printStackTrace();}System.out.println("First task completed");}});Thread t1 = new Thread(new Runnable(){public void run(){System.out.println("Second task completed");}});t.start(); // Line 15t.join(); // Line 16t1.start();}}Output:First task startedSleeping for 2 secondsFirst task completedSecond task completed

Вот и все об этой небольшой, но важной концепции. Дайте мне знать ваши мысли в разделе комментариев.

Прокрутить вверх