Метод yield в Java

Когда поток вызывает метод yield он говорит: мне сейчас не обязательно заканчивать свою работу и занимать время процессора и больше времени передастся другим потокам

Пример программы:

class YieldExample { public static void main(String[] args) { new SomeThread().start(); new SomeThread().start(); new SomeThread().start(); } } class SomeThread extends Thread { public void run() { System.out.println(Thread.currentThread().getName() + ” уступает свое место другим потокам”); Thread.yield(); System.out.println(Thread.currentThread().getName() + ” завершился”); } }

Вывод:

Мы запускаем три потока. Планировщик потоков может запустить эти потоки в разном порядке (например, 2-1-0 или 1-0-2)

Example

Рассмотрим порядок 0-1-2.

Первым запускается 0 поток и с помощью yield он уступает время работы процессора другим потокам, то есть потоку 1 и потоку 2.

Вторым до yield доходит поток 1. Он уступает другим, то есть оставшемуся 2. Поток 2 доходит до yield последним и хочет уступить другим но уступать уже некому

Если больше нет потоков, которым можно уступить место для выполнения, то уступается место для выполнения последнему уступившему перед потоком 2 то есть потоку 1.

Далее последнему уступившему перед 1, то есть 0 и потом 2.

На консоли мы видим именно такую последовательность выполнения.

Синхронизация с помощью Wait/Notify в Java

wait/notify – используется если нам нужно приостановить один поток и чтобы он ждал пока другой поток даст разрешение на продолжение выполнения остановленного потока.

То есть чтобы один поток дождался формирования какой-либо информации в другом потоке чтобы потом ее использовать

Пример программы:

class DemoClass { synchronized void part1() { System.out.println(“Thread t1 started”); //с помощью notify даем разрешение на //продолжение работы потока, который был //остановлен с помощью wait. notify(); System.out.println( “Thread t2 now is not locked by wait method anymore”); for(int i=0;i<15;i++) { System.out.println("Thread t1 is working now..."); } System.out.println("Thread t1 finished his work."); } //wait/notify - вызываются в блоках synchronized //над одним и тем же объектом в данном случае //над объектом DemoClass synchronized void part2() { try { System.out.println("Thread t2 started"); System.out.println("Thread t2 waiting"); //с помощью wait останавливаем поток, который //зашел в этом synchronized метод и теперь //другие потоки могут заходить в другие //synchronized методы для того чтобы в них //с помощью notify дать разрешение на //продолжение работы потока, который //был заблокирован здесь с помощью wait. wait(); System.out.println( "Thread t2 running again"); } catch (Exception e) { System.out.println(e.getClass()); } System.out.println("Thread t2 finished his work."); } } public class WaitNotifyExample { public static void main(String[] args) { DemoClass obj = new DemoClass(); //реализуем run с помощью анонимного класса. //чтобы не создавать два полноценных класса Thread t1 = new Thread(new Runnable() { public void run() { obj.part1(); } }); Thread t2 = new Thread(new Runnable() { public void run() { obj.part2(); } }); t2.start(); try { Thread.sleep(100); } catch(InterruptedException e){} t1.start(); } }

Вывод:

Example

Программа выполняется в следующей последовательности:

  1. Запускается t2
  2. t2 заходит в synchronized метод
  3. все остальные synchronized методы в obj блокируются и запущенный t1 после t2 не сможет зайти в synchronized блок в obj пока t2 не освободит synchronized блок в obj в который он зашел или пока не дойдет до метода wait
  4. t2 доходит до wait метода в synchronized, который приостанавливает поток t2
  5. t1 теперь может зайти в synchronized блок в obj в который он хотел зайти
  6. notify уведомляет t2 о том, что он может продолжать работу после завершения работы synchronized метода в который сейчас выполняет поток t1
  7. t1 завершает свою работу
  8. t2 продолжает работу.

Synchronized static в Java

synchronized static (блокировка на уровне класса), а просто synchronized (блокировка на уровне объекта).

В коде ниже у нас уже будут 2 объекта ресурса одного класса. В них уже будет synchronized static блок.

В отличии от synchronized, блок synchronized static распространяется на все объекты одного класса.

То есть если поток дошел до synchronized static в одном объекте класса, то он блокирует этот кусок кода не только для других потоков, которые доходят до этого synchronized static в этом же объекте, а и для тех потоков, которые доходят до этого synchronized static в другом объекте того же класса, что и первый объект.

Пример программы:

public class StaticSinchExample { public static void main(String[] args) { // объект ресурса номер 1 CommonResource commonResource1 = new CommonResource(); // объект ресурса номер 2 CommonResource commonResource2 = new CommonResource(); for (int i = 1; i < 6; i++) { // запускаем 5 потоков, передаем в каждый из них // первый ресурс, общий для 5 потоков Thread t1 = new Thread(new CountThread(commonResource1)); // запускаем 5 потоков, передаем в каждый из них // второй ресурс, общий для 5 потоков Thread t2 = new Thread(new CountThread(commonResource2)); t1.setName("Thread t1_" + i); t2.setName("Thread t2_" + i); t1.start(); t2.start(); } // То есть, если один из 10 запускаемых выше потоков // дойдет до блока synchronized static в каком-либо // из объектов ресурсов, будь то commonResource1 // или commonResource2, то блок synchronized static // будет заблокирован для всех остальных потоков // во всех объектах ресурсов. } } class CommonResource { int x; static int x1; synchronized static void incrementstatic() { // Верхнюю строчку можно было бы переписать // как synchronized (CommonResource.class) – // блокирует для остальных потоков блок кода // класса, а не блок кода конкретного объекта. x1 = 1; for (int i = 1; i < 5; i++) { System.out.printf("Static %s %d \n", Thread.currentThread().getName(), x1); x1++; try { Thread.sleep(1100); } catch (InterruptedException e) {} } } synchronized void increment() { // Верхнюю строчку можно было бы переписать // как synchronized (this) – блокирует для // остальных потоков блок кода // конкретного объекта. x = 1; for (int i = 1; i < 5; i++) { System.out.printf("%s %d \n", Thread.currentThread().getName(), x); x++; try { Thread.sleep(100); } catch (InterruptedException e) {} } } } class CountThread implements Runnable { CommonResource res; CountThread(CommonResource res) { this.res = res; } public void run() { CommonResource.incrementstatic(); } }

Вывод:

Как видно, все 10 потоков выводятся по очереди.

То есть 4 значения одного потока из 10 потоков, потом 4 значения другого из 10 потоков и так до десятого потока.

Что значит, что блокировка synchronized static распространяется на все объекты ресурсов.


Детали о разнице synchronized static и просто synchronized.

synchronized static (на уровне класса) и просто synchronized (на уровне объекта) это ДВЕ РАЗНЫЕ БЛОКИРОВКИ. Могут выполняться параллельно друг другу.

Один sinchronized метод блокирует доступ и к другим synchronized методам в объекте. То есть если поток зашел в один synchronized метод объекта, то заблокирован для других потоков не только этот synchronized метод в этом объекте, заблокированы для других потоков и другие synchronized методы в этом объекте пока поток не покинет synchronized метод в объекте.

Если же один поток зашел в просто synchronized метод (не static) в объекте, то другие потоки могут спокойно заходить в synchronized static методы в этом объекте не дожидаясь пока поток, который зашел в просто synchronized выйдет оттуда.

То есть просто syncronized метод в объекте и synchronized static метод в этом объекте могут выполняться параллельно друг другу.

Пример программы:

public class StaticSinchExample2 { public static void main(String[] args) { //Объект ресурса CommonResource commonResource = new CommonResource(); for (int i = 1; i < 6; i++) { //запускаем 5 потоков и передаем //в каждый из них ресурс общий для потоков Thread t = new Thread(new CountThread(commonResource)); t.setName("Thread " + i); t.start(); } } } class CommonResource { int x; static int x1; synchronized void increment() { x = 1; for (int i = 1; i < 5; i++) { System.out.printf("%s %d \n", Thread.currentThread().getName(), x); x++; try { Thread.sleep(250); } catch (InterruptedException e) {} } } synchronized static void incrementstatic() { x1 = 1; for (int i = 1; i < 5; i++) { System.out.printf("Static %s %d \n", Thread.currentThread().getName(), x1); x1++; try { Thread.sleep(700); } catch (InterruptedException e) {} } } } //Код synchronized метода и synchronized static метода //не должны пересекаться так как блоки synchronized static //и просто synchronized могут выполняться параллельно, //а если они могут выполняться параллельно значит //они не синхронизированы между собой. class CountThread implements Runnable { CommonResource res; CountThread(CommonResource res) { this.res = res; } public void run() { //один поток может выполнять incrementstatic метод CommonResource.incrementstatic(); //другой поток может в это же время //параллельно выполнять increment res.increment(); } }

Вывод:

Как видно, после выхода Thread 1 из incrementstatic метода Thread 1 входит в increment метод и выполнение этого метода, как видно, происходит параллельно потоку Thread 5, который зашел в incrementstatic метод как только из него вышел Thread 1.

То есть очевидно synchronized static блок и synchronized блок выполняются параллельно друг другу.

Синхронизация в Java с помощью Synchronized

Если несколько параллельных потоков одновременно хотят выполнить один и тот же кусок кода одного и того же объекта, то можно сделать так чтобы они выполняли его поочереди.

То есть они можно сказать выстраиваются в очередь чтобы воспользоваться куском кода в объекте. Смотри пример.

Пример программы:

public class SynchronizedExample { public static void main(String[] args) { // Объект ресурса (общий для потоков ресурс) CommonResource commonResource = new CommonResource(); // Запускаем 5 потоков, передавая в каждый из них // ресурс, общий для потоков. for (int i = 1; i < 6; i++) { Thread t = new Thread(new CountThread(commonResource)); t.setName("Thread " + i); t.start(); } // Когда выполнение кода одного из параллельных // потоков доходит до оператора synchronized, // доступ к блоку кода synchronized объекта // ресурса (commonResource) блокируется, и на // время его блокировки монопольный доступ к // блоку кода в этом объекте имеет только один // поток, который дошел до synchronized и который // произвел блокировку, и все прочие потоки, // которые используют commonResource будут ждать // пока поток, который первый дошел до блока // synchronized, закончит его выполнение. } } class CommonResource { int x; // Когда один из потоков доходит сюда, доступ // к блоку кода synchronized объекта // CommonResource блокируется, и другие потоки // которые дошли до synchronized останавливаются // и ждут, пока поток, дошедший сюда раньше них, // выполнит блок synchronized. Когда он все-таки // выполнил synchronized, в synchronized заходит // другой поток из очереди, и другие ждут, пока уже // этот другой поток выполнит код synchronized, // и так далее со всеми остальными потоками // в очереди. synchronized void increment() { x = 1; for (int i = 1; i < 5; i++) { System.out.printf("%s %d \n", Thread.currentThread().getName(), x); x++; try { Thread.sleep(700); } catch (InterruptedException e) {} } } } class CountThread implements Runnable { CommonResource res; CountThread(CommonResource res) { this.res = res; } public void run() { res.increment(); // вызываем синхронизированный метод } }

Вывод:

Благодаря этому потоки будут работать с ресурсом поочередно и от 1 до 4 сначала выведет первый дошедший до synchronized поток потом второй и так далее. Видим в консоли, что потоки выстроились в очередь в таком порядке – 1 5 2 4 3.

Метод join в Java

Метод join останавливает поток в котором этот метод был вызван пока не закончит выполнение поток к которому этот метод был вызван.

Более понятно на примере:

import java.io.*; class ThreadJoining extends Thread { @Override public void run(){ for (int i = 0; i < 2; i++){ try{ Thread.sleep(500); System.out.println("Current Thread: " + Thread.currentThread().getName()); } catch(Exception e){ System.out.println("Exception has been caught"+e); } System.out.println(i); } } } class JoinExample { public static void main (String[] args){ ThreadJoining t1 = new ThreadJoining(); ThreadJoining t2 = new ThreadJoining(); ThreadJoining t3 = new ThreadJoining(); t1.start(); try{ System.out.println("Current Thread: " + Thread.currentThread().getName()); //ниже join вызван в потоке мейн к потоку t1 t1.join();//приостанавливает текущий поток //(в данном случае поток в котором выполняется мейн) //пока не выполниться t1 } catch(Exception e){ System.out.println("Exception has been caught" + e); } t2.start(); try{ System.out.println("Current Thread: " + Thread.currentThread().getName()); //Опять таки останавливаем мейн //пока не выполниться t2. t2.join(); } catch(Exception e){ System.out.println("Exception has been caught" + e); } t3.start(); try{ System.out.println("Current Thread: " + Thread.currentThread().getName()); //Опять таки останавливаем мейн //пока не выполниться t3. t3.join(); } catch(Exception e){ System.out.println("Exception has been caught" + e); } } }

Вывод:

В консоли видно, что мейн останавливался, давая по очереди полностью выполниться потокам.

Volatile переменная в Java

Для создания общей глобальной переменной, которую будут использовать несколько потоков используется слово volatile.

Создадим два потока. Первый будет добавлять 1 к глобальной переменной и как только он добавил 1 к глобальной переменной, во втором потоке будет происходить вывод на консоль нового значения глобальной переменной. Этот процесс будет продолжаться пока i не станет 8.

Пример программы:

public class VolatileExample { static volatile int i;//объявим i volatile public static void main(String[] args) { new MyThread1().start();//запустим потоки new MyThread2().start(); } static class MyThread1 extends Thread { @Override public void run() { while(i<8){ System.out.println("inc i = " + (++i)); try{ //Sleep для остановки текущего потока //на заданное количество миллисекунд //в данном случае 100. Thread.sleep(100); } catch(InterruptedException e){} } } } static class MyThread2 extends Thread { @Override public void run() { int localvar = i; while(i<8){ if(localvar != i){ //Если бы i не был volatile то здесь //была бы копия i и она вечно была //бы 0 и поток бы ничего не вывел. System.out.println("new i = " + i); localvar = i; } } } } }

Вывод:

Search Icon

Как уже было сказано, если бы i была не volatile, то мы бы не работали непосредственно с переменной i, которая в main, а в каждом потоке бы создавалась копия переменной i и каждый поток работал бы со своей копией i.

Интерфейс Runnable в Java

Реализуя Runnable тоже реализуется метод run в котором пишется код, который будет выполняться в отдельном потоке.

Пример программы:

//Создадим простенький класс. //Дальше поясним зачем он. class Friend { public static void m1() { System.out.println(“Hello Friend!!!”); } } //Также можно увидеть что мы реализуем не только //Runnable но и расширяем Friend. Это преимущество //реализации Runnable перед расширением Thread //так как в Java можно расширять Лишь Один класс //и если бы мы расширили Thread то расширить //какой либо другой класс (например Friend) //уже не имели бы возможности. class MyThread extends Friend implements Runnable { int i=0; public void run() { i++; m1(); } } class RunnableExample { public static void main(String[] args) { //создаем один объект MyThread. MyThread MyThr = new MyThread(); //Можно у одного объекта //в отдельных потоках запускать его метод run //это преимущество Runnable перед Thread. //Подробнее на следующей странице. Thread t1 = new Thread(MyThr);//передается в поток Thread t2 = new Thread(MyThr);//передается в поток 2 раз //run объекта MyThr запускается //в отдельных потоках t1 и t2. t1.start(); t2.start(); //Еще одно преимущество Runnable перед Thread //что код класса реализующего Runnable можно //использовать вне в отдельном потоке. //То есть m1 просто выполнится здесь в потоке метода main MyThr.m1(); //В конце можно увидеть 2. То есть оба потока работали //с одним и тем же объектом. //Все потоки добавили 1 к полю i объекта MyThr. System.out.println(MyThr.i); } }

Вывод:

В консоли можно увидеть 2. То есть оба потока работали с одним и тем же объектом. Все потоки добавляли 1 к полю i объекта MyThr.


Преимущества реализации Runnable

Преимущества реализации Runnable перед расширением Thread:

1. Реализуя Runnable можно создать всего 1 объект и передавать в потоки для параллельного выполнения, в отличие от расширения thread, где для каждого потока необходимо создавать отдельный объект класса, который расширяет Thread.

То есть мы видели в прошлом уроке:

MyThread t1 = new MyThread();

MyThread t2 = new MyThread();

MyThread t3 = new MyThread();

Search Icon

Следовательно реализуя Runnable можно сэкономить память, не создавая кучу объектов.

Также нужно помнить, что раз все потоки работают с одним объектом, то они работают с одними и теми же самыми полями этого одного объекта.

2. Код класса реализующего Runnable можно использовать не в потоке. Объект можно передать не только в Thread, а и, например, в ExecutorService

3. Расширяя Thread нет возможности расширить класс расширяющий Thread еще раз, так как в java нет множ. насл.

Многопоточность в Java. Создание потоков с помощью Thread

Создать нить(поток) можно тремя способами:

  • расширяя Thread;
  • реализуя Runnable;
  • расширяя Callable;

Код класса расширяющего или реализующего что-нибудь выше перечисленное будет выполняться в отдельном потоке.

Мы уже затрагивали создание потоков когда рассматривали PipedOutputStream. И мы помним, что там было два потока и код одного потока выполнялся параллельно коду другого.

Поэтому суть потоков думаю понятна. Параллельное выполнение блоков кода друг другу.


Расширение Thread

Ниже в примере класс MyThread расширяет Thread.

Это значит, что в нем должен быть переопределен метод run.

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

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

Пример программы:

class MyThread extends Thread { @Override public void run() { System.out.println(“run() method “); } } public class ExtendsThread { public static void main(String[] args) { //В отличие от реализации Runnable (разберем //в следующем уроке) для каждого потока класса //расширяющего Thread необходимо создавать новый //объект. Создаются три новых объекта MyThread //как можно увидеть MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); MyThread t3 = new MyThread(); //Теперь с помощью метода start в одном потоке //запускается выполнение метода run у объекта t1, //в другом потоке запускается выполнение метода //run у объекта t2 и в третьем потоке запускается //выполнение метода run у объекта t3. //И ясное дело все эти потоки выполняются //параллельно друг другу. t1.start(); t2.start(); t3.start(); } }

Вывод:

Каждый поток вывел по строке как видим.


Преимущества расширения Thread

Основное преимущество расширения Thread в том, что есть возможность переопределять не только run(), но и другие методы, такие как start().

Пример программы:

class MyThread extends Thread { @Override public void start() { System.out.println(“Overriding a start() method”); super.start(); } @Override public void run() { System.out.println(“run() method “); } } public class ExtendsThread { public static void main(String[] args) { MyThread t1 = new MyThread(); MyThread t2 = new MyThread(); MyThread t3 = new MyThread(); t1.start(); t2.start(); t3.start(); } }

Вывод:

Как видим, теперь и при вызове start тоже выводиться строка.

Причем порядок выведения строк на консоль может быть разный. В данном случае, на консоли мы видим, что один поток уже успел завершить свое выполнение перед тем как запустился третий поток.