Когда поток вызывает метод 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)
Рассмотрим порядок 0-1-2.
Первым запускается 0 поток и с помощью yield он уступает время работы процессора другим потокам, то есть потоку 1 и потоку 2.
Вторым до yield доходит поток 1. Он уступает другим, то есть оставшемуся 2. Поток 2 доходит до yield последним и хочет уступить другим но уступать уже некому
Если больше нет потоков, которым можно уступить место для выполнения, то уступается место для выполнения последнему уступившему перед потоком 2 то есть потоку 1.
Далее последнему уступившему перед 1, то есть 0 и потом 2.
На консоли мы видим именно такую последовательность выполнения.
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();
}
}
Вывод:
Программа выполняется в следующей последовательности:
Запускается t2
t2 заходит в synchronized метод
все остальные synchronized методы в obj блокируются и запущенный t1 после t2 не сможет зайти в synchronized блок в obj пока t2 не освободит synchronized блок в obj в который он зашел или пока не дойдет до метода wait
t2 доходит до wait метода в synchronized, который приостанавливает поток t2
t1 теперь может зайти в synchronized блок в obj в который он хотел зайти
notify уведомляет t2 о том, что он может продолжать работу после завершения работы synchronized метода в который сейчас выполняет поток t1
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 блок выполняются параллельно друг другу.
Если несколько параллельных потоков одновременно хотят выполнить один и тот же кусок кода одного и того же объекта, то можно сделать так чтобы они выполняли его поочереди.
То есть они можно сказать выстраиваются в очередь чтобы воспользоваться куском кода в объекте. Смотри пример.
Пример программы:
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.
Для создания общей глобальной переменной, которую будут использовать несколько потоков используется слово 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;
}
}
}
}
}
Вывод:
Как уже было сказано, если бы i была не volatile, то мы бы не работали непосредственно с переменной i, которая в main, а в каждом потоке бы создавалась копия переменной i и каждый поток работал бы со своей копией i.
Реализуя 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();
Следовательно реализуя Runnable можно сэкономить память, не создавая кучу объектов.
Также нужно помнить, что раз все потоки работают с одним объектом, то они работают с одними и теми же самыми полями этого одного объекта.
2. Код класса реализующего Runnable можно использовать не в потоке. Объект можно передать не только в Thread, а и, например, в ExecutorService
3. Расширяя Thread нет возможности расширить класс расширяющий Thread еще раз, так как в java нет множ. насл.
Код класса расширяющего или реализующего что-нибудь выше перечисленное будет выполняться в отдельном потоке.
Мы уже затрагивали создание потоков когда рассматривали 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 тоже выводиться строка.
Причем порядок выведения строк на консоль может быть разный. В данном случае, на консоли мы видим, что один поток уже успел завершить свое выполнение перед тем как запустился третий поток.