Справочник по синхронизаторам java.util.concurrent.*
Целью данной публикации не является полный анализ синхронизаторов из пакета java.util.concurrent. Пишу её, прежде всего, как справочник, который облегчит вхождение в тему и покажет возможности практического применения классов для синхронизации потоков (далее поток = thread).
В java.util.concurrent много различных классов, которые по функционалу можно поделить на группы: Concurrent Collections, Executors, Atomics и т.д. Одной из этих групп будет Synchronizers (синхронизаторы).
Синхронизаторы – вспомогательные утилиты для синхронизации потоков, которые дают возможность разработчику регулировать и/или ограничивать работу потоков и предоставляют более высокий уровень абстракции, чем основные примитивы языка (мониторы).
Semaphore
Синхронизатор Semaphore реализует шаблон синхронизации Семафор. Чаще всего, семафоры необходимы, когда нужно ограничить доступ к некоторому общему ресурсу. В конструктор этого класса ( Semaphore(int permits) или Semaphore(int permits, boolean fair) ) обязательно передается количество потоков, которому семафор будет разрешать одновременно использовать заданный ресурс.
Доступ управляется с помощью счётчика: изначально значение счётчика равно int permits , когда поток заходит в заданный блок кода, то значение счётчика уменьшается на единицу, когда поток его покидает, то увеличивается. Если значение счётчика равно нулю, то текущий поток блокируется, пока кто-нибудь не выйдет из блока (в качестве примера из жизни с permits = 1 , можно привести очередь в кабинет в поликлинике: когда пациент покидает кабинет, мигает лампа, и заходит следующий пациент).
Пример использования Semaphore
Рассмотрим следующий пример. Существует парковка, которая одновременно может вмещать не более 5 автомобилей. Если парковка заполнена полностью, то вновь прибывший автомобиль должен подождать пока не освободится хотя бы одно место. После этого он сможет припарковаться.
import java.util.concurrent.Semaphore; public class Parking < //Парковочное место занято - true, свободно - false private static final boolean[] PARKING_PLACES = new boolean[5]; //Устанавливаем флаг "справедливый", в таком случае метод //aсquire() будет раздавать разрешения в порядке очереди private static final Semaphore SEMAPHORE = new Semaphore(5, true); public static void main(String[] args) throws InterruptedException < for (int i = 1; i > public static class Car implements Runnable < private int carNumber; public Car(int carNumber) < this.carNumber = carNumber; >@Override public void run() < System.out.printf("Автомобиль №%d подъехал к парковке.\n", carNumber); try < //acquire() запрашивает доступ к следующему за вызовом этого метода блоку кода, //если доступ не разрешен, поток вызвавший этот метод блокируется до тех пор, //пока семафор не разрешит доступ SEMAPHORE.acquire(); int parkingNumber = -1; //Ищем свободное место и паркуемся synchronized (PARKING_PLACES)< for (int i = 0; i < 5; i++) if (!PARKING_PLACES[i]) < //Если место свободно PARKING_PLACES[i] = true; //занимаем его parkingNumber = i; //Наличие свободного места, гарантирует семафор System.out.printf("Автомобиль №%d припарковался на месте %d.\n", carNumber, i); break; >> Thread.sleep(5000); //Уходим за покупками, к примеру synchronized (PARKING_PLACES) < PARKING_PLACES[parkingNumber] = false;//Освобождаем место >//release(), напротив, освобождает ресурс SEMAPHORE.release(); System.out.printf("Автомобиль №%d покинул парковку.\n", carNumber); > catch (InterruptedException e) < >> > >
Результат работы программы
Автомобиль №1 подъехал к парковке.
Автомобиль №1 припарковался на месте 0.
Автомобиль №2 подъехал к парковке.
Автомобиль №2 припарковался на месте 1.
Автомобиль №3 подъехал к парковке.
Автомобиль №3 припарковался на месте 2.
Автомобиль №4 подъехал к парковке.
Автомобиль №4 припарковался на месте 3.
Автомобиль №5 подъехал к парковке.
Автомобиль №5 припарковался на месте 4.
Автомобиль №6 подъехал к парковке.
Автомобиль №7 подъехал к парковке.
Автомобиль №1 покинул парковку.
Автомобиль №6 припарковался на месте 0.
Автомобиль №2 покинул парковку.
Автомобиль №7 припарковался на месте 1.
Автомобиль №3 покинул парковку.
Автомобиль №4 покинул парковку.
Автомобиль №5 покинул парковку.
Автомобиль №6 покинул парковку.
Автомобиль №7 покинул парковку.
Семафор отлично подходит для решения такой задачи: он не дает автомобилю (потоку) припарковаться (зайти в заданный блок кода и воспользоваться общим ресурсом) если мест на парковке нет (счётчик равен 0) Стоит отметить, что класс Semaphore поддерживает захват и освобождение более чем одного разрешения за раз, но в данном задаче это не нужно.
CountDownLatch
CountDownLatch (замок с обратным отсчетом) предоставляет возможность любому количеству потоков в блоке кода ожидать до тех пор, пока не завершится определенное количество операций, выполняющихся в других потоках, перед тем как они будут «отпущены», чтобы продолжить свою деятельность. В конструктор CountDownLatch ( CountDownLatch(int count) ) обязательно передается количество операций, которое должно быть выполнено, чтобы замок «отпустил» заблокированные потоки.
Блокировка потоков снимается с помощью счётчика: любой действующий поток, при выполнении определенной операции уменьшает значение счётчика. Когда счётчик достигает 0, все ожидающие потоки разблокируются и продолжают выполняться (примером CountDownLatch из жизни может служить сбор экскурсионной группы: пока не наберется определенное количество человек, экскурсия не начнется).
Пример использования CountDownLatch
- Каждый из пяти автомобилей подъехал к стартовой прямой;
- Была дана команда «На старт!»;
- Была дана команда «Внимание!»;
- Была дана команда «Марш!».
import java.util.concurrent.CountDownLatch; public class Race < //Создаем CountDownLatch на 8 "условий" private static final CountDownLatch START = new CountDownLatch(8); //Условная длина гоночной трассы private static final int trackLength = 500000; public static void main(String[] args) throws InterruptedException < for (int i = 1; i while (START.getCount() > 3) //Проверяем, собрались ли все автомобили Thread.sleep(100); //у стартовой прямой. Если нет, ждем 100ms Thread.sleep(1000); System.out.println("На старт!"); START.countDown();//Команда дана, уменьшаем счетчик на 1 Thread.sleep(1000); System.out.println("Внимание!"); START.countDown();//Команда дана, уменьшаем счетчик на 1 Thread.sleep(1000); System.out.println("Марш!"); START.countDown();//Команда дана, уменьшаем счетчик на 1 //счетчик становится равным нулю, и все ожидающие потоки //одновременно разблокируются > public static class Car implements Runnable < private int carNumber; private int carSpeed;//считаем, что скорость автомобиля постоянная public Car(int carNumber, int carSpeed) < this.carNumber = carNumber; this.carSpeed = carSpeed; >@Override public void run() < try < System.out.printf("Автомобиль №%d подъехал к стартовой прямой.\n", carNumber); //Автомобиль подъехал к стартовой прямой - условие выполнено //уменьшаем счетчик на 1 START.countDown(); //метод await() блокирует поток, вызвавший его, до тех пор, пока //счетчик CountDownLatch не станет равен 0 START.await(); Thread.sleep(trackLength / carSpeed);//ждем пока проедет трассу System.out.printf("Автомобиль №%d финишировал!\n", carNumber); >catch (InterruptedException e) < >> > >
Результат работы программы
Автомобиль №1 подъехал к стартовой прямой.
Автомобиль №2 подъехал к стартовой прямой.
Автомобиль №3 подъехал к стартовой прямой.
Автомобиль №4 подъехал к стартовой прямой.
Автомобиль №5 подъехал к стартовой прямой.
На старт!
Внимание!
Марш!
Автомобиль №4 финишировал!
Автомобиль №1 финишировал!
Автомобиль №3 финишировал!
Автомобиль №5 финишировал!
Автомобиль №2 финишировал!
CountDownLatch может быть использован в самых разных схемах синхронизации: к примеру, чтобы пока один поток выполняет работу, заставить другие потоки ждать или, наоборот, чтобы заставить поток ждать других, чтобы выполнить работу.
CyclicBarrier
CyclicBarrier реализует шаблон синхронизации Барьер. Циклический барьер является точкой синхронизации, в которой указанное количество параллельных потоков встречается и блокируется. Как только все потоки прибыли, выполняется опционное действие (или не выполняется, если барьер был инициализирован без него), и, после того, как оно выполнено, барьер ломается и ожидающие потоки «освобождаются». В конструктор барьера ( CyclicBarrier(int parties) и CyclicBarrier(int parties, Runnable barrierAction) ) обязательно передается количество сторон, которые должны «встретиться», и, опционально, действие, которое должно произойти, когда стороны встретились, но перед тем когда они будут «отпущены».
Барьер похож на CountDownLatch, но главное различие между ними в том, что вы не можете заново использовать «замок» после того, как его счётчик достигнет нуля, а барьер вы можете использовать снова, даже после того, как он сломается. CyclicBarrier является альтернативой метода join() , который «собирает» потоки только после того, как они выполнились.
Пример использования CyclicBarrier
Рассмотрим следующий пример. Существует паромная переправа. Паром может переправлять одновременно по три автомобиля. Чтобы не гонять паром лишний раз, нужно отправлять его, когда у переправы соберется минимум три автомобиля.
import java.util.concurrent.CyclicBarrier; public class Ferry < private static final CyclicBarrier BARRIER = new CyclicBarrier(3, new FerryBoat()); //Инициализируем барьер на три потока и таском, который будет выполняться, когда //у барьера соберется три потока. После этого, они будут освобождены. public static void main(String[] args) throws InterruptedException < for (int i = 0; i < 9; i++) < new Thread(new Car(i)).start(); Thread.sleep(400); >> //Таск, который будет выполняться при достижении сторонами барьера public static class FerryBoat implements Runnable < @Override public void run() < try < Thread.sleep(500); System.out.println("Паром переправил автомобили!"); >catch (InterruptedException e) < >> > //Стороны, которые будут достигать барьера public static class Car implements Runnable < private int carNumber; public Car(int carNumber) < this.carNumber = carNumber; >@Override public void run() < try < System.out.printf("Автомобиль №%d подъехал к паромной переправе.\n", carNumber); //Для указания потоку о том что он достиг барьера, нужно вызвать метод await() //После этого данный поток блокируется, и ждет пока остальные стороны достигнут барьера BARRIER.await(); System.out.printf("Автомобиль №%d продолжил движение.\n", carNumber); >catch (Exception e) < >> > >
Результат работы программы
Автомобиль №0 подъехал к паромной переправе.
Автомобиль №1 подъехал к паромной переправе.
Автомобиль №2 подъехал к паромной переправе.
Автомобиль №3 подъехал к паромной переправе.
Паром переправил автомобили!
Автомобиль №2 продолжил движение.
Автомобиль №1 продолжил движение.
Автомобиль №0 продолжил движение.
Автомобиль №4 подъехал к паромной переправе.
Автомобиль №5 подъехал к паромной переправе.
Автомобиль №6 подъехал к паромной переправе.
Паром переправил автомобили!
Автомобиль №5 продолжил движение.
Автомобиль №4 продолжил движение.
Автомобиль №3 продолжил движение.
Автомобиль №7 подъехал к паромной переправе.
Автомобиль №8 подъехал к паромной переправе.
Паром переправил автомобили!
Автомобиль №8 продолжил движение.
Автомобиль №6 продолжил движение.
Автомобиль №7 продолжил движение.
Когда три потока достигают метода await() , барьерное действие запускается, и паром переправляет три автомобиля из скопившихся. После этого начинается новый цикл.
Exchanger
Exchanger (обменник) может понадобиться, для того, чтобы обменяться данными между двумя потоками в определенной точки работы обоих потоков. Обменник — обобщенный класс, он параметризируется типом объекта для передачи.
Обменник является точкой синхронизации пары потоков: поток, вызывающий у обменника метод exchange() блокируется и ждет другой поток. Когда другой поток вызовет тот же метод, произойдет обмен объектами: каждая из них получит аргумент другой в методе exchange() . Стоит отметить, что обменник поддерживает передачу null значения. Это дает возможность использовать его для передачи объекта в одну сторону, или, просто как точку синхронизации двух потоков.
Пример использования Exchanger
Рассмотрим следующий пример. Есть два грузовика: один едет из пункта A в пункт D, другой из пункта B в пункт С. Дороги AD и BC пересекаются в пункте E. Из пунктов A и B нужно доставить посылки в пункты C и D. Для этого грузовики в пункте E должны встретиться и обменяться соответствующими посылками.
import java.util.concurrent.Exchanger; public class Delivery < //Создаем обменник, который будет обмениваться типом String private static final ExchangerEXCHANGER = new Exchanger<>(); public static void main(String[] args) throws InterruptedException < String[] p1 = new String[]D>", "C>">;//Формируем груз для 1-го грузовика String[] p2 = new String[]C>", "D>">;//Формируем груз для 2-го грузовика new Thread(new Truck(1, "A", "D", p1)).start();//Отправляем 1-й грузовик из А в D Thread.sleep(100); new Thread(new Truck(2, "B", "C", p2)).start();//Отправляем 2-й грузовик из В в С > public static class Truck implements Runnable < private int number; private String dep; private String dest; private String[] parcels; public Truck(int number, String departure, String destination, String[] parcels) < this.number = number; this.dep = departure; this.dest = destination; this.parcels = parcels; >@Override public void run() < try < System.out.printf("В грузовик №%d погрузили: %s и %s.\n", number, parcels[0], parcels[1]); System.out.printf("Грузовик №%d выехал из пункта %s в пункт %s.\n", number, dep, dest); Thread.sleep(1000 + (long) Math.random() * 5000); System.out.printf("Грузовик №%d приехал в пункт Е.\n", number); parcels[1] = EXCHANGER.exchange(parcels[1]);//При вызове exchange() поток блокируется и ждет //пока другой поток вызовет exchange(), после этого произойдет обмен посылками System.out.printf("В грузовик №%d переместили посылку для пункта %s.\n", number, dest); Thread.sleep(1000 + (long) Math.random() * 5000); System.out.printf("Грузовик №%d приехал в %s и доставил: %s и %s.\n", number, dest, parcels[0], parcels[1]); >catch (InterruptedException e) < >> > >
Результат работы программы
В грузовик №1 погрузили: <посылка A->D> и <посылка A->C>.
Грузовик №1 выехал из пункта A в пункт D.
В грузовик №2 погрузили: <посылка B->C> и <посылка B->D>.
Грузовик №2 выехал из пункта B в пункт C.
Грузовик №1 приехал в пункт Е.
Грузовик №2 приехал в пункт Е.
В грузовик №2 переместили посылку для пункта C.
В грузовик №1 переместили посылку для пункта D.
Грузовик №2 приехал в C и доставил: <посылка B->C> и <посылка A->C>.
Грузовик №1 приехал в D и доставил: <посылка A->D> и <посылка B->D>.
посылка>
Как мы видим, когда один грузовик (один поток) приезжает в пункт Е (достигает точки синхронизации), он ждет пока другой грузовик (другой поток) приедет в пункт Е (достигнет точки синхронизации). После этого происходит обмен посылками (String) и оба грузовика (потока) продолжают свой путь (работу).
Phaser
Phaser (фазер), как и CyclicBarrier, является реализацией шаблона синхронизации Барьер, но, в отличии от CyclicBarrier, предоставляет больше гибкости. Этот класс позволяет синхронизировать потоки, представляющие отдельную фазу или стадию выполнения общего действия. Как и CyclicBarrier, Phaser является точкой синхронизации, в которой встречаются потоки-участники. Когда все стороны прибыли, Phaser переходит к следующей фазе и снова ожидает ее завершения.
- Каждая фаза (цикл синхронизации) имеет номер;
- Количество сторон-участников жестко не задано и может меняться: поток может регистрироваться в качестве участника и отменять свое участие;
- Участник не обязан ожидать, пока все остальные участники соберутся на барьере. Чтобы продолжить свою работу достаточно сообщить о своем прибытии;
- Случайные свидетели могут следить за активностью в барьере;
- Поток может и не быть стороной-участником барьера, чтобы ожидать его преодоления;
- У фазера нет опционального действия.
Phaser() Phaser(int parties)
Параметр parties указывает на количество сторон-участников, которые будут выполнять фазы действия. Первый конструктор создает объект Phaser без каких-либо сторон, при этом барьер в этом случае тоже «закрыт». Второй конструктор регистрирует передаваемое в конструктор количество сторон. Барьер открывается когда все стороны прибыли, или, если снимается последний участник. (У класса Phaser еще есть конструкторы, в которые передается родительский объект Phaser, но мы их рассматривать не будем.)
- int register() — регистрирует нового участника, который выполняет фазы. Возвращает номер текущей фазы;
- int getPhase() — возвращает номер текущей фазы;
- int arriveAndAwaitAdvance() — указывает что поток завершил выполнение фазы. Поток приостанавливается до момента, пока все остальные стороны не закончат выполнять данную фазу. Точный аналог CyclicBarrier.await() . Возвращает номер текущей фазы;
- int arrive() — сообщает, что сторона завершила фазу, и возвращает номер фазы. При вызове данного метода поток не приостанавливается, а продолжает выполнятся;
- int arriveAndDeregister() — сообщает о завершении всех фаз стороной и снимает ее с регистрации. Возвращает номер текущей фазы;
- int awaitAdvance(int phase) — если phase равно номеру текущей фазы, приостанавливает вызвавший его поток до её окончания. В противном случае сразу возвращает аргумент.
Пример использования Phaser
Рассмотрим следующий пример. Есть пять остановок. На первых четырех из них могут стоять пассажиры и ждать автобуса. Автобус выезжает из парка и останавливается на каждой остановке на некоторое время. После конечной остановки автобус едет в парк. Нам нужно забрать пассажиров и высадить их на нужных остановках.
import java.util.ArrayList; import java.util.concurrent.Phaser; public class Bus < private static final Phaser PHASER = new Phaser(1);//Сразу регистрируем главный поток //Фазы 0 и 6 - это автобусный парк, 1 - 5 остановки public static void main(String[] args) throws InterruptedException < ArrayListpassengers = new ArrayList<>(); for (int i = 1; i < 5; i++) < //Сгенерируем пассажиров на остановках if ((int) (Math.random() * 2) >0) passengers.add(new Passenger(i, i + 1));//Этот пассажир выходит на следующей if ((int) (Math.random() * 2) > 0) passengers.add(new Passenger(i, 5)); //Этот пассажир выходит на конечной > for (int i = 0; i < 7; i++) < switch (i) < case 0: System.out.println("Автобус выехал из парка."); PHASER.arrive();//В фазе 0 всего 1 участник - автобус break; case 6: System.out.println("Автобус уехал в парк."); PHASER.arriveAndDeregister();//Снимаем главный поток, ломаем барьер break; default: int currentBusStop = PHASER.getPhase(); System.out.println("Остановка № " + currentBusStop); for (Passenger p : passengers) //Проверяем, есть ли пассажиры на остановке if (p.departure == currentBusStop) < PHASER.register();//Регистрируем поток, который будет участвовать в фазах p.start(); // и запускаем >PHASER.arriveAndAwaitAdvance();//Сообщаем о своей готовности > > > public static class Passenger extends Thread < private int departure; private int destination; public Passenger(int departure, int destination) < this.departure = departure; this.destination = destination; System.out.println(this + " ждёт на остановке № " + this.departure); >@Override public void run() < try < System.out.println(this + " сел в автобус."); while (PHASER.getPhase() < destination) //Пока автобус не приедет на нужную остановку(фазу) PHASER.arriveAndAwaitAdvance(); //заявляем в каждой фазе о готовности и ждем Thread.sleep(1); System.out.println(this + " покинул автобус."); PHASER.arriveAndDeregister(); //Отменяем регистрацию на нужной фазе >catch (InterruptedException e) < >> @Override public String toString() < return "Пассажир" + destination + '>'; > > >
Результат работы программы
Пассажир <1 ->2> ждёт на остановке № 1
Пассажир <1 ->5> ждёт на остановке № 1
Пассажир 3> ждёт на остановке № 2
Пассажир 5> ждёт на остановке № 2
Пассажир 4> ждёт на остановке № 3
Пассажир 5> ждёт на остановке № 4
Пассажир 5> ждёт на остановке № 4
Автобус выехал из парка.
Остановка № 1
Пассажир <1 ->5> сел в автобус.
Пассажир <1 ->2> сел в автобус.
Остановка № 2
Пассажир 3> сел в автобус.
Пассажир <1 ->2> покинул автобус.
Пассажир 5> сел в автобус.
Остановка № 3
Пассажир 3> покинул автобус.
Пассажир 4> сел в автобус.
Остановка № 4
Пассажир 5> сел в автобус.
Пассажир 4> покинул автобус.
Пассажир 5> сел в автобус.
Остановка № 5
Пассажир <1 ->5> покинул автобус.
Пассажир 5> покинул автобус.
Пассажир 5> покинул автобус.
Пассажир 5> покинул автобус.
Автобус уехал в парк.
1>
Кстати, функционалом фазера можно воспроизвести работу CountDownLatch.
Пример из CountDownLatch с использованием Phaser
import java.util.concurrent.Phaser; public class NewRace < private static final Phaser START = new Phaser(8); private static final int trackLength = 500000; public static void main(String[] args) throws InterruptedException < for (int i = 1; i while (START.getRegisteredParties() > 3) //Проверяем, собрались ли все автомобили Thread.sleep(100); //у стартовой прямой. Если нет, ждем 100ms Thread.sleep(100); System.out.println("На старт!"); START.arriveAndDeregister(); Thread.sleep(100); System.out.println("Внимание!"); START.arriveAndDeregister(); Thread.sleep(100); System.out.println("Марш!"); START.arriveAndDeregister(); > public static class Car implements Runnable < private int carNumber; private int carSpeed; public Car(int carNumber, int carSpeed) < this.carNumber = carNumber; this.carSpeed = carSpeed; >@Override public void run() < try < System.out.printf("Автомобиль №%d подъехал к стартовой прямой.\n", carNumber); START.arriveAndDeregister(); START.awaitAdvance(0); Thread.sleep(trackLength / carSpeed); System.out.printf("Автомобиль №%d финишировал!\n", carNumber); >catch (InterruptedException e) < >> > >
Если кому-нибудь пригодилось, то я очень рад=)
Более подробно о Phaser здесь.
Почитать ещё о синхронизаторах и посмотреть примеры можно здесь.
Отличный обзор java.util.concurrent смотрите здесь.
Что такое барьер в канкаренси
— Сегодня будет небольшая, но интересная и полезная тема – сортировки коллекций.
— Сортировка? Я что-то про это слышал.
— Давным-давно каждый программист обязан был уметь писать сортировку. Умел и писал. Но те времена канули в лету. Сегодня написание своей сортировки считается дурным тоном, как и написание всего, что уже было придумано.
В Java (да и других языках программирования) сортировки уже реализованы. Твоя задача – научиться правильно пользоваться тем, что есть.
— У вспомогательного класса Collections есть статический метод sort, который используется для сортировки коллекций, а если точнее – списков. Элементы в коллекциях Map и Set не имеют порядка/номера, значит, и сортировать там нечего.
— Да, я вспомнил, я когда-то уже использовал этот метод для сортировки списка чисел.
— Отлично. Но этот метод гораздо мощнее чем, кажется на первый взгляд. Он может сортировать не только числа, но и любые объекты, по любым критериям. И помогают ему в этом два интерфейса: Comparable и Comparator.
Иногда бывает нужно отсортировать объекты, а не числа. Например, у тебя есть список людей, и ты хочешь отсортировать их по возрасту. Для этого есть интерфейс Comparable.
Давай я сначала покажу тебе пример, и все станет понятнее:
public Woman(int age) this.age = age;
>
Чтобы объекты можно было сортировать, сначала нужно научиться их сравнивать. Для этого и используется Comparable. Интерфейс Comparable является generic’ом – т.е. типом с параметром. У него всего один generic-метод – compare(To). В этом методе и происходит сравнение переданного объекта и текущего (this). Т.е. надо переопределить этот метод в своем классе и сравнить в нем текущий объект (this) с переданным.
— А как работает compare? Я думал, что он будет возвращать true/false в зависимости от того – больше переданный объект или меньше.
— Тут все немного хитрее. Метод compare возвращает не true/false, а значение типа int. На самом деле так сделано для простоты.
Когда компьютеру нужно определить больше ли одно число, чем другое, он просто вычитает из первого числа второе, а потом смотрит, что получилось. Если 0 – числа равны, если получилось число меньше нуля, то второе число больше, а если результат больше нуля, то больше уже первое число.
Тут используется та же логика. Согласно спецификации метод compare должен вернуть ноль, если сравниваемые объекты равны. Если метод compare вернул число больше нуля, это значит, что наш (this) объект больше, чем переданный. Если метод compare вернул число меньше нуля, то объект this меньше чем переданный.
— Да, но если ты сравниваешь объекты просто по какому-то параметру-числу, то можешь просто вернуть разницу между ними – вычесть один из другого. Как это и сделано в примере выше.
public int compareTo(Woman o) return this.age — o.age; > |
— Вроде все понятно. Хотя может и не все. Но почти все.
— Отлично. Теперь рассмотрим более практическую задачу. Ты написал крутой сайт по пошиву женской одежды в Китае. Для описания своих пользователей ты используешь класс Woman. Ты даже сделал страницу с таблицей, где можешь посмотреть их всех. Но есть проблема…
Объект Woman содержит у тебя не только возраст, а еще целую кучу данных: имя, фамилию, рост, вес, количество детей, …
В таблице пользователей есть много колонок, и тут встает вопрос: а как сортировать пользователей по разным критериям? По весу, по возрасту, по фамилии?
— Гм. Действительно, часто вижу таблицы с сортировкой колонок. И как это сделать?
— А для этого есть второй интерфейс, о котором я хотел тебе сегодня рассказать – это интерфейс Comparator. И у него тоже есть метод compare, только он принимает не один параметр, а два. Вот как это работает:
Comparator compareByHeight = new Comparator() public int compare(Woman o1, Woman o2) <
return o1.height — o2.height;
>
>;
При использовании интерфейса Comparator, логика сравнения пары объектов не прячется внутрь класса/объекта, а реализуется в отдельном классе.
— Т.е. я могу сделать несколько классов, реализующих интерфейс Comparator, но в каждом из них сравнивать разные параметры? В одном – weight, в другом – age, в третьем – height?
— Да, это очень просто и удобно.
Мы просто вызываем метод Collections.sort, передаем туда список объектов и еще специальный объект во втором параметре, который реализует интерфейс Comparator и говорит, как правильно сравнивать пары объектов в процессе сортировки.
— Гм. Вроде все понятно. Дай-ка я сам попробую. Допустим, мне нужно отсортировать пользователей по весу, это будет так:
— Отлично. А если я хочу отсортировать в обратном порядке?
— А подумать? Ответ очень простой!
— Придумал! Вот так:
Сортировка по возрастанию: |
---|
return o1.weight — o2.weight; |
Сортировка по убыванию: |
return o2.weight – o1.weight; |
— А если я хочу сортировать по фамилии? Как сортировать строки, Билаабо?
— А у строк уже реализован метод compare, надо просто вызвать его:
— Это был отличный урок, Билаабо, спасибо тебе большое.
— И тебе спасибо, друг!
2. Задачи на сортировку и comparator
3. Разделяемые ресурсы, Конфликты, Проблема совместного доступа
— Привет, Амиго! Хочу тебе рассказать о совместном использовании ресурсов. Разными нитями, ясное дело.
Я все время говорю о проблемах при работе нескольких нитей и о том, как их решать. Это не значит, что использование нитей – это плохо. Нити – это очень мощный инструмент. Фактически, они позволяют увеличить скорость, и даже надежность работы твоей программы. Чем сложнее программа – тем больше в ней нитей и различных самостоятельных частей.
Разбиение программы на независимые (слабосвязанные) части очень выгодно.
Представь, что твоя программа внутри разбита на 100 нитей. Но у тебя всего двухъядерный процессор. Это значит, что на каждом ядре исполняется в среднем 50 нитей.
Если же тебе нужно нарастить мощность программы, ты просто покупаешь двухпроцессорный сервер и пару крутых процессоров для него. В результате у него суммарно может быть до 32- ядер, и производительность твоей программы может вырасти в 2-20 раз. В зависимости от того, насколько действительно независимых частей она разбита.
Это одна из причин доминирования Java в секторе Enterprise-разработки. Если у компании есть сложная программа для внутренних нужд, которую пишут 20 разработчиков, то купить еще один сервер гораздо дешевле, чем ускорить программу в 2 раза.
— Так вот оказывается в чем дело.
— Но! Каждый раз, когда разработчик решает использовать еще одну нить, он решает одну проблему, а создает две. Слишком много нитей не увеличат производительность программы до бесконечности.
Во-первых, в любой программе есть работа, которую невозможно разбить на части и выполнять параллельно в разных нитях. Во-вторых, все нити выполняются на одном и том же процессоре. Чем больше нитей, тем медленнее работает каждая из них.
И, самое главное – нити часто используют одни и те же объекты (их обычно называют разделяемыми ресурсами).
Например, нить хочет сохранить в файл информацию о сделанной работе. Если таких нитей несколько, и они хотят записать информацию в один файл – они будут мешать друг другу. Чтобы в файле не было мешанины, каждая нить пользуется уникальным доступом к файлу – т.е. пока файлом пользуется одна нить, другие ждут.
— Да, я помню, это делается с помощью ключевого слова synchronized.
— А если нити пишут в разные файлы?
— Формально – это разные объекты, но жесткий диск же один.
— Т.е. реально что-то распараллелить можно только внутри процессора?
— Формально – Да, как только твоей нити надо что-то еще кроме данных, которые у нее есть, это что-то уже может быть занято другой нитью и придется ждать.
— И что же делать? Как узнать – делать много нитей или нет?
— Это определяется непосредственно архитектурой программы. У любого проекта есть его «архитектор», который знает все «ресурсы», которые используются в программе, знает их ограничения, и насколько они хорошо/плохо параллелятся.
— А если я не знаю?
— Тут есть два варианта:
а) поработать под началом такого специалиста
б) набить шишек самому
— Я – робот, у меня не бывает шишек – только вмятины.
— Ну, значит, набить вмятин.
— Ясно, спасибо. Ты прояснила некоторые вопросы, о которых я уже начал ломать голову.
Java.util.concurrent Синхронизаторы. Барьеры (1) Барьер – это средство синхронизации, которое используется для того, чтобы некоторое множество потоков. — презентация
Презентация на тему: » Java.util.concurrent Синхронизаторы. Барьеры (1) Барьер – это средство синхронизации, которое используется для того, чтобы некоторое множество потоков.» — Транскрипт:
1 java.util.concurrent Синхронизаторы. Барьеры (1) Барьер – это средство синхронизации, которое используется для того, чтобы некоторое множество потоков ожидало друг друга в некоторой точке программы, называемой обычно точкой синхронизации. В тот момент, когда все потоки достигают точки синхронизации, они разблокируются и могут продолжать выполнение. На практике барьеры обычно используются для сбора результатов выполнения некоторой распараллеленной задачи. В качестве примера можно рассмотреть задачу умножения матриц. При распараллеливании данной задачи каждому потоку будет поручено умножение определенных строк на определенные столбцы. В точке синхронизации все полученные результаты собираются из потоков и в одно из них строится результирующая матрица. В пакете java.util.concurrent для реализации барьерной синхронизации используется класс CyclicBarrier. Конструкторы этого класса получают количество потоков, которые должны достичь точки синхронизации и, опционально, экземпляр интерфейса Runnable, который должен быть исполнен в момент достижения этой точки всеми потоками: CyclicBarrier( int parties ); CyclicBarrier( int parties, Runnable barrierAction );
2 java.util.concurrent Синхронизаторы. Барьеры (2) intawait( )Ожидание, пока заданное в конструкторе количество потоков выполнит вызов этого метода. intawait( long timeout, TimeUnit unit ) Ожидание, пока заданное в конструкторе количество потоков выполнит вызов этого метода или пока не закончится заданный тайм-аут. intgetNumberWaiting( )Возвращает количество ожидающих потоков. intgetParties( )Возвращает количество потоков, требуемых для преодоления этого барьера (установленное при создании экземпляра). booleanisBroken( )Возвращает true, если хотя бы один поток прерван или ушел из ожидания по тайм-ауту. voidreset( )Сброс барьера в исходное состояние. Методы класса java.util.concurrent.CyclicBarrier: Нужно отметить, что в момент срабатывания барьера экземпляр класса восстанавливает начальное состояние и может отрабатывать следующие parties срабатываний потоков.
3 java.util.concurrent Синхронизаторы. Барьеры (3) В этой программе параллельно вычисляются суммы элементов строк матрицы (каждой строке сопоставляется свой поток): import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; public class Main < private static int matrix[ ][ ] = < … >;// инициализация или чтение матрицы private static int results[ ]; static Boolean flag = false; private static class Summator extends Thread < int row;// индекс строки для экземпляра сумматора CyclicBarrier barrier;// барьерный синхронизатор Summator( CyclicBarrier barrier, int row ) < // конструктор сумматора this.barrier = barrier; this.row = row; >
6 java.util.concurrent Синхронизаторы. Барьеры (6) CyclicBarrier barrier = new CyclicBarrier( rows, merger ); for ( int i = 0; i
7 java.util.concurrent Синхронизаторы. Обменники Обменник – это параметризованный класс Exchanger пакета java.util.concurrent, с помощью которого можно осуществлять передачу данных между потоками, не заботясь о синхронизации (она реализуется внутренними средствами класса). В двух потоках, которым нужно обменяться данными, вызывается метод exchange() экземпляра класса Exchanger, доступного каждому из них. В качестве аргумента метода указывается значение (его тип должен быть приводим к параметру используемого экземпляра обменника), которое должно быть отдано другому потоку: V exchange( V sendingValue ) Поток, вызвавший этот метод, засыпает до тех пор, пока какой-либо другой поток не вызовет этот же метод этого же экземпляра обменника. Существует возможность указать продолжительность тайм-аута ожидания, после истечения которого проснувшийся поток получит для обработки исключение TimeoutException: V exchange(V sendingValue, long timeout, TimeUnit tUnit) Обменники можно использовать и как средство синхронизации. Часто это оказывается значительно более удобным, чем другие средства. Перепишем только что рассматривавшийся пример.
8 java.util.concurrent Синхронизаторы. Барьеры и обменники (1) import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CyclicBarrier; import java.util.concurrent.Exchanger; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; public class Main < private static int matrix[ ][ ] = < … >;// инициализация или чтение матрицы private static int results[ ]; static Boolean flag = false; private static class Summator extends Thread < int row; // индекс строки для экземпляра сумматора CyclicBarrier barrier;// барьерный синхронизатор Summator( CyclicBarrier barrier, int row ) < // конструктор сумматора this.barrier = barrier; this.row = row; >
12 java.util.concurrent Синхронизаторы. Защелка (1) Защелка (или щеколда) – средство синхронизации, которое используется для того, чтобы один или несколько потоков могли дождаться выполнения заданного количества операций в других потоках. Класс CountDownLatch пакета java.util.concurrent является соответствующим средством синхронизации. Он работает по принципу таймера, только отсчеты вырабатываются программно, а не кварцевым генератором. При создании экземпляра класса его внутренний счетчик инициализируется начальным значением, передаваемым конструктору. Значение счетчика может быть уменьшено на 1 путем вызова метода countDown(). При вызове метода await() каким-либо потоком, он переходит в состояние ожидания момента достижения счетчиком значения 0. Есть перегруженный метод await(long timeout, TimeUnit unit), позволяющий организовать ожидание не дольше, чем в течение заданного интервала. Есть также метод getCount(), позволяющий получить текущее значение счетчика. В отличие от класса CyclicBarrier экземпляры этого класса одноразовые. На практике данный класс удобно использовать для координации момента начала и окончания определенного числа потоков: можно сделать так, чтобы исполнение заданного числа потоков начиналось в один и тот же момент времени; можно отследить момент окончания заданного числа потоков.
15 java.util.concurrent.atomic Классы с атомарными операциями Подпакет java.util.concurrent.atomic содержит несколько классов, предоставляющих атомарные операции: AtomicBoolean AtomicInteger AtomicIntegerArray AtomicIntegerFieldUpdater AtomicLong AtomicLongArray AtomicLongFieldUpdater AtomicMarkableReference AtomicReference AtomicReferenceArray AtomicReferenceFieldUpdater AtomicStampedReference
16 java.util.concurrent.atomic Основные методы атомарных классов (1) У всех (кроме *Reference): booleancompareAndSet( * expect, * update ) сравнить текущее значение с expect, если совпадают – атомарно заменить текущее на update и вернуть true, иначе – вернуть false *get() вернуть текущее значение *getAndSet( * newValue) атомарно заменить значение на newValue, вернуть предыдущее значение voidlazySet( * newValue ) На процессорах x86 практически ничем не отличается от просто set, но использовать можно, только тщательно разобравшись в отличии от метода set voidset( * newValue ) атомарно заменить значение на newValue booleanweakCompareAndSet ( * expect, * update ) На процессорах x86 практически ничем не отличается от просто compareAndSet, но использовать можно, только тщательно разобравшись в особенностях
17 java.util.concurrent.atomic Основные методы атомарных классов (2) У всех численных( Integer, Long, IntegerArray, LongArray; для *Array все эти методы имеют дополнительный первый аргумент int i): *addAndGet(* delta) Атомарно добавляет к текущему значению аргумент delta и возвращает полученное значение *decrementAndGet() Атомарно уменьшает на единицу текущее значение и возвращает результат (аналог префиксной операции – –) *getAndAdd(* delta) Атомарно добавляет к текущему значению аргумент delta и возвращает предыдущее значение *getAndDecrement() Атомарно уменьшает на единицу текущее значение и возвращает предыдущее значение (аналог постфиксной операции – –) *getAndIncrement() Атомарно уменьшает на единицу текущее значение и возвращает предыдущее значение (аналог постфиксной операции + +) *incrementAndGet() Атомарно увеличивает на единицу текущее значение и возвращает результат (аналог префиксной операции + +)
18 java.util.concurrent.atomic Классы с атомарными операциями. Пример. public final class Counter < private int value = 0; public synchronized int getValue( ) < return value; >public synchronized int increment( ) < return ++value; >public class NonblockingCounter < private AtomicInteger value = new AtomicInteger( 0 ); public int getValue( ) < return value.get( ); >public int increment( )
19 Коллекции Java (Java Collections Framework) Параллельные коллекции пакета java.util.concurrent базируются на средствах, реализованных ранее в пакете java.util, но отличаются возможностью безопасного применения в многопоточных приложениях при относительно небольших затратах. Коллекции или контейнеры это классы позволяющие хранить и производить операции над множеством объектов. Коллекции используются для сохранения, получения, манипулирования данными и обеспечивают агрегацию одних объектов другими. Во многих языках программирования (java, C, C++, Pascal, …) единственным встроенным в язык средством хранения наборов объектов являются массивы. Однако, массивы обладают значительными недостатками. Одним из них является фиксированный размер массива, порождающий необходимость постоянно следить за значениями индексов. Другим индексная адресация, что не всегда удобно, т.к. ограничивает возможности добавления и удаления объектов. Чтобы избавиться от этих недостатков, уже несколько десятилетий программисты используют рекурсивные типы данных, такие как списки, деревья и множества. Стандартный набор коллекций Java, реализованный в пакете java.util, служит для избавления программиста от необходимости самостоятельно реализовывать эти типы данных и снабжает его дополнительными возможностями по сравнению с массивами.
20 Пакет java.util. Коллекции Взаимосвязь основных классов и интерфейсов (1)
21 Пакет java.util. Коллекции Взаимосвязь основных классов и интерфейсов (2) S Object Dictionary
22 Пакет java.util. Коллекции Интерфейсы Интерфейс Collection Является корнем всей иерархии классов-коллекций. Он определяет базовую функциональность любой коллекции – набор методов, которые позволяют добавлять, удалять, выбирать элементы коллекции. Классы, которые реализуют интерфейс Collection, могут содержать дубликаты и пустые (null) значения. Класс AbstractCollection, как абстрактный класс, служит основой для создания конкретных классов коллекций и содержит реализацию некоторых методов, определенных в интерфейсе Collection. При наследовании от этого класса все реализованные методы можно заместить.
23 Пакет java.util. Коллекции Методы интерфейса Collection (1) booleanadd( E e )Если коллекция изменилась в результате вызова этого метода, то возвращается true, иначе (например, если это Set и добавляемый элемент уже в нем содержится) возвращается false. booleanaddAll( Collection c )Если коллекция изменилась в результате вызова этого метода, то возвращается true, иначе возвращается false. voidclear( )Удаляются все элементы из коллекции. booleancontains( Object o )Если коллекция содержит указанный элемент, то возвращается true, иначе – false. booleancontainsAll( Collection c )Если коллекция содержит все элементы из коллекции, заданной аргументом, то возвращается true, иначе – false. booleanisEmpty( )Если коллекция пуста (не содержит ни одного элемента, то возвращается true, иначе – false.
24 Пакет java.util. Коллекции Методы интерфейса Collection (2) Iterator iterator( )Возвращается объект, реализующий интерфейс java.util.Iterator. booleanremove( Object o )Удаляется одно (или ни одного) вхождение указанного элемента из коллекции. Если коллекция изменилась, то возвращается true, иначе возвращается false. booleanremoveAll( Collection c)Удаляются все вхождения элементов заданной коллекции. Если коллекция изменилась, то возвращается true, иначе возвращается false. booleanretainAll( Collection c )Удаляются все элементы, не входящие в указанную коллекцию. Если коллекция изменилась, то возвращается true, иначе – false. intsize( )Возвращается количество элементов. Object[ ]toArray( )Возвращается массив, содержащий все элементы коллекции. T[ ]toArray( T[ ] a )Возвращается массив, содержащий все элементы коллекции (либо a, либо создается новый, если длина указанного массива меньше количества элементов).
25 Пакет java.util. Коллекции Интерфейсы *Set Интерфейс Set Классы, которые реализуют этот интерфейс, не допускают наличия дубликатов в составе коллекции. В коллекции этого типа разрешено наличие только одной ссылки типа null. Интерфейс Set расширяет интерфейс Collection (не добавляя методов), таким образом, любой класс, реализующий интерфейс Set, имеет все методы, определенные в Collection. Любой объект, добавляемый в Set, должен реализовать метод equals, чтобы его можно было сравнить с другими элементами. AbstractSet, являясь абстрактным классом, представляет собой основу для реализации различных вариантов интерфейса Set. Интерфейс SortedSet Этот интерфейс расширяет Set, требуя, чтобы содержимое набора было упорядочено. Такие коллекции могут содержать объекты, которые реализуют интерфейс Comparable, либо могут сравниваться с использованием внешнего Comparator. Интерфейс NavigableSet — расширение SortedSet с приблизительным доступом к элементу. Например, метод higher( ) позволяет получить наименьшей элемент в множестве, который больше указанного.
Thread’ом Java не испортишь: Часть VI — К барьеру!
Потоки — штука интересная. В прошлых обзорах мы рассмотрели некоторые доступные средства для реализации многопоточности. Давайте посмотрим, что мы можем сделать ещё интересного. К этому моменту мы многое что знаем. Например, из «Thread’ом Java не испортишь: Часть I — потоки» мы знаем, что поток — это Thread. Мы знаем, что поток выполняет некоторую задачу. Если мы хотим, чтобы нашу задачи могли запустить ( run ), то мы должны указать потоку некий Runnable . Чтобы вспомнить, можем воспользоваться Tutorialspoint Java Online Compiler’ом:
public static void main(String []args) < Runnable task = () ->< Thread thread = Thread.currentThread(); System.out.println("Hello from " + thread.getName()); >; Thread thread = new Thread(task); thread.start(); >
Так же мы знаем о том, что у нас есть такое понятие, как лок. О нем мы читали в «Thread’ом Java не испортишь: Часть II — синхронизация». Поток может занимать лок и тогда другой поток, который попытается занять лок, будет вынужден ждать освобождения лока:
import java.util.concurrent.locks.*; public class HelloWorld < public static void main(String []args)< Lock lock = new ReentrantLock(); Runnable task = () ->< lock.lock(); Thread thread = Thread.currentThread(); System.out.println("Hello from " + thread.getName()); lock.unlock(); >; Thread thread = new Thread(task); thread.start(); > >
Думаю, пора поговорить о том, что мы ещё можем интересное сделать.
Семафоры
Самое простое средство контроля за тем, сколько потоков могут одновременно работать — семафор. Как на железной дороге. Горит зелёный — можно. Горит красный — ждём. Что мы ждём от семафора? Разрешения. Разрешение на английском — permit. Чтобы получить разрешение — его нужно получить, что на английском будет acquire. А когда разрешение больше не нужно мы его должны отдать, то есть освободить его или избавится от него, что на английском будет release. Посмотрим, как это работает. Нам потребуется импорт класса java.util.concurrent.Semaphore . Пример:
public static void main(String[] args) throws InterruptedException < Semaphore semaphore = new Semaphore(0); Runnable task = () -> < try < semaphore.acquire(); System.out.println("Finished"); semaphore.release(); >catch (InterruptedException e) < e.printStackTrace(); >>; new Thread(task).start(); Thread.sleep(5000); semaphore.release(1); >
Как видим, запомнив английские слова, мы понимаем, как работает семафор. Интересно, что главное условие — на «счету» семафора должен быть положительное количество permit’ов. Поэтому, инициировать его можно и с минусом. И запрашивать (acquire) можно больше, чем 1.
CountDownLatch
Следующий механизм — CountDownLatch . CountDown на английском — это отсчёт, а Latch — задвижка или защёлка. То есть если переводить, то это защёлка с отсчётом. Тут нам понадобится соответствующий импорт класса java.util.concurrent.CountDownLatch . Это похоже на бега или гонки, когда все собираются у стартовой линии и когда все готовы — дают разрешение, и все одновременно стартуют. Пример:
public static void main(String[] args) < CountDownLatch countDownLatch = new CountDownLatch(3); Runnable task = () -> < try < countDownLatch.countDown(); System.out.println("Countdown: " + countDownLatch.getCount()); countDownLatch.await(); System.out.println("Finished"); >catch (InterruptedException e) < e.printStackTrace(); >>; for (int i = 0; i < 3; i++) < new Thread(task).start(); >>
await на английском — ожидать. То есть мы сначала говорим countDown . Как говорит гугл переводчик, count down — «an act of counting numerals in reverse order to zero», то есть выполнить действие по обратному отсчёту, цель которого — досчитать до нуля. А дальше говорим await — то есть ожидать, пока значение счётчика не станет ноль. Интересно, что такой счётчик — одноразовый. Как сказано в JavaDoc — «When threads must repeatedly count down in this way, instead use a CyclicBarrier», то есть если нужен многоразовый счёт — надо использовать другой вариант, который называется CyclicBarrier .
CyclicBarrier
Как и следует из названия, CyclicBarrier — это циклический барьер. Нам понадобится импорт класса java.util.concurrent.CyclicBarrier . Посмотрим на пример:
public static void main(String[] args) throws InterruptedException < Runnable action = () ->System.out.println("На старт!"); CyclicBarrier berrier = new CyclicBarrier(3, action); Runnable task = () -> < try < berrier.await(); System.out.println("Finished"); >catch (BrokenBarrierException | InterruptedException e) < e.printStackTrace(); >>; System.out.println("Limit: " + berrier.getParties()); for (int i = 0; i < 3; i++) < new Thread(task).start(); >>
Как видим, поток выполняет await , то есть ожидает. При этом уменьшается значение барьера. Барьер считается сломанным ( berrier.isBroken() ), когда отсчёт дошёл до нуля. Чтобы сбросить барьер, нужно вызвать berrier.reset() , чего не хватало в CountDownLatch .
Exchanger
Следующее средство — Exchanger . Exchange с английского переводится как обмен или обмениваться. А Exchanger — обменник, то есть то, через что обмениваются. Посмотрим на простейший пример:
public static void main(String[] args) < Exchangerexchanger = new Exchanger<>(); Runnable task = () -> < try < Thread thread = Thread.currentThread(); String withThreadName = exchanger.exchange(thread.getName()); System.out.println(thread.getName() + " обменялся с " + withThreadName); >catch (InterruptedException e) < e.printStackTrace(); >>; new Thread(task).start(); new Thread(task).start(); >
Тут мы запускаем два потока. Каждый из них выполняет метод exchange и ожидает, когда другой поток так жевыполнит метод exchange. Таким образом, потоки обменяются между собой переданными аргументами. Интересная штука. Ничего ли она вам не напоминает? А напоминает он SynchronousQueue , которая лежит в основе cachedThreadPool ‘а. Для наглядности — пример:
public static void main(String[] args) throws InterruptedException < SynchronousQueuequeue = new SynchronousQueue<>(); Runnable task = () -> < try < System.out.println(queue.take()); >catch (InterruptedException e) < e.printStackTrace(); >>; new Thread(task).start(); queue.put("Message"); >
В примере видно, что запустив новый поток, данный поток уйдёт в ожидание, т.к. в очереди будет пусто. А дальше main поток положит в очередь текст «Message». При этом он сам остановится на время, которой нужно, пока не получат из очереди этот текстовый элемент. По этой теме так же можно почитать «SynchronousQueue Vs Exchanger».
Phaser
И напоследок самое сладкое — Phaser . Нам понадобится импорт класса java.util.concurrent.Phaser . Посмотрим на простой пример:
public static void main(String[] args) throws InterruptedException < Phaser phaser = new Phaser(); // Вызывая метод register, мы регистрируем текущий поток (main) как участника phaser.register(); System.out.println("Phasecount is " + phaser.getPhase()); testPhaser(phaser); testPhaser(phaser); testPhaser(phaser); // Через 3 секунды прибываем к барьеру и снимаемся регистрацию. Кол-во прибывших = кол-во регистраций = пуск Thread.sleep(3000); phaser.arriveAndDeregister(); System.out.println("Phasecount is " + phaser.getPhase()); >private static void testPhaser(final Phaser phaser) < // Говорим, что будет +1 участник на Phaser phaser.register(); // Запускаем новый поток new Thread(() ->< String name = Thread.currentThread().getName(); System.out.println(name + " arrived"); phaser.arriveAndAwaitAdvance(); //threads register arrival to the phaser. System.out.println(name + " after passing barrier"); >).start(); >
Из примера видно, что барьер при использовании Phaser ‘а прорывается, когда количество регистраций совпадает с количеством прибывших к барьеру. Подробнее можно ознакомиться с Phaser ‘ом в статье с хабра «Новый синхронизатор Phaser».
Итоги
Как видно из примеров, существуют различные способы синхронизации потоков. Ранее я постарался уже вспомнить что-то из многопоточности, надеюсь прошлые части были полезны. Говорят, что путь к многопоточности начинается с книги «Java Concurrency in Practice». Хотя она вышла в 2006 году, люди отвечают, что книга довольно фундаментальна и до сих пор держит удар. Например, можно прочитать обсуждения здесь: «Is Java Concurrency In Practice still valid?». Также полезно прочитать ссылки из обсуждения. Например, там есть ссылка на книгу «The Well-Grounded Java Developer», в которой стоит обратить на «Chapter 4. Modern concurrency». Есть ещё целый обзор на эту же тему: «Is Java cocurrency in pracitce still relevant in era of java 8». Там также есть советы по поводу того, что ещё следует почитать, чтобы действительно понять эту тему. После этого, можно присмотреться к такой замечательной книге, как «OCA OCP JavaSE 8 Programmer Practice Tests». Нас интересует вторая часть, то есть OCP. И там есть тесты в «∫». В этой книге есть как вопросы, так и ответы со объяснением. Например: Многие могут начать говорить, что это очередное заучивание методов. С одной стороны — да. С другой стороны, на этот вопрос можно дать ответ, вспомнив, что ExecutorService — это своего рода «апгрейд» Executor ‘а. А Executor призван просто скрыть способ создания потоков, но не основной способ их выполнения, то есть запуск в новом потоке Runnable . Поэтому execute(Callable) и нет, т.к. в ExecutorService к Executor ‘у просто добавили методы submit , которые умеют возвращать Future . Как видите, мы можем и заучить список методов, но куда проще догадаться, зная природу самих классов. Ну и немного дополнительных материалов по теме:
- «Справочник по синхронизаторам java.util.concurrent.*»
- «Синхронизаторы пакета concurrent»
- Юрий Ткач «Синхронизаторы — Concurrency #4 — Advanced Java»
#Viacheslav
Что еще почитать: |
---|
Thread’ом Java не испортишь: Часть I — потоки Thread’ом Java не испортишь: Часть II — синхронизация Thread’ом Java не испортишь : Часть III — взаимодействие Thread’ом Java не испортишь : Часть IV — Callable, Future и друзья Thread’ом Java не испортишь: Часть V — Executor, ThreadPool, Fork Join |