Обеспечение эффективной многопоточности Java приложений Андрей Дмитриев Ноябрь 2009
2 Программа Введение. Основы параллельного исполнения. Привнесение дополнительной сложности. Расширенные возможности.
3 Что такое поток? Определение: –последовательность выполняемых операций внутри программы. Для чего нужны потоки? –Одновременное исполнение действий.
4 Вопрос для рассуждений Задача о параллельном выполнении. Есть две работы. Посчитать скорость выполнения их в пакетном и в параллельном режимах работы. Взято из «Joel on Software»
5 Многопоточность в Java Каждое приложение на Java имеет по крайней мере один пользовательский поток. С точки зрения программирования, каждая программа начинается с выполнения одного потока – потока Main. Main поток может создавать и запускать другие потоки.
6 Состояния потока New – создан, но не запущен. Ready-to-run – готов к работе. –Поток помещается в очередь, ожидающую доступа к ресурсам ЦП. Running – активно использует ресурсы CPU. Blocked – ожидает ресурсов или событий. Waiting, Sleeping – поток перестал использовать ЦП. –Самостоятельно или был принудительно снят с исполнения.
7 Приоритет Позволяет определить порядок использования ресурсов ЦП. Определение. Целое число от 1 до 10. Чем больше приоритет потока, тем у него больше шансов получить управление.
8 Класс java.lang.Thread Данный класс предоставляет абстракцию, позволяющую выполнять инструкции языка Java в отдельном потоке. Имеет следующие конструкторы. Thread() Создает новый класс. Thread(String name) Создает новый именованный класс-поток. Thread(Runnable target) Создает новый класс-поток на основе экземпляра Runnable, чей метод run() будет исполняться в отдельном потоке. Thread(Runnable target, String name) Создает новый именованный класс-поток на основе Runnable.
9 Класс java.lang.Thread (cont.) Имеет несколько публичных полей, отражающих относительные приоритеты. public final static int MAX_PRIORITY Максимальный приоритет (10). public final static int MIN_PRIORITY Минимальный приоритет (1). public final static int NORM_PRIORITY Обычный приоритет (5).
10 Класс java.lang.Thread (cont.) Среди методов класса можно выделить: public static Thread currentThread() Возвращает ссылку на активный в настоящее время поток. public final String getName() Возвращает имя потока. public final String setName(String name) Устанавливает имя потока. public void interrupt() Прерывает поток. public int getPriority() Возвращает приоритет потока. public final boolean isAlive() Определяет, работает ли поток.
11 Класс java.lang.Thread (cont.) public void run() Метод запускается в отдельном потоке. public void start() Запускает выполнение метода run() в отдельном потоке. public void join(Thread other) Приостанавливает исполнение текущего потока до тех пор пока поток other не завершится. public void stop() Завершает выполнение потока. public void resume() Восстанавливает выполнение потока. public void sleep(long delay) Приостанавливает выполнение потока на delay миллисекунд. public void yield() Метод пробует отказаться от выполнения на ЦП в пользу других потоков.
12 Два способа создать поток Наследование от класса Thread. Реализация интерфейса Runnable.
13 Расширение класса Thread Наследник класса Thread переопределяет метод run(). Создается экземпляр подкласса. Вызов метода start() данного подкласса запускает выполнение потока. > Виртуальная машина начинает выполнение потока вызовом метода run().
14 Две схемы запуска потока Конструктор подкласса класса Thread не вызывает метод start(). > Нужно явно вызвать метод start() для данного подкласса. Помещение вызова метода start() в конструктор подкласса класса Thread. > Создание экземпляра класса автоматически запустит новый поток.
15 Пример создания потока (Thread) class SimpleCombat extends Thread { public void run(){ //конкретные действия поединка takePosition(); fire(); } … } //запуск инструкций в новом потоке new SimpleCombat().start();
16 Интерфейс java.lang.Runnable Интерфейс Runnable должен быть реализован классом, предназначенным для выполнения в отдельном потоке. Класс должен реализовывать один метод run(). > Этот метод похож на метод main(String s[]). Экземпляр класса должен быть передан конструктору класса Thread в качестве аргумента. Запуск потока осуществляется методом start() класса Thread.
17 Две схемы запуска потока Конструктор класса, реализующего интерфейс Runnable в своем конструкторе не вызывает метод start(). > Нужно явно вызвать метод start() для данного класса. Помещение вызова метода start() в конструктор класса, реализующего интерфейс Runnable. > Создание экземпляра класса автоматически запустит новый поток.
18 Пример создания потока (Runnable) class SimpleCombat implements Runnable{ public void run(){ //конкретные действия поединка takePosition(); fire(); } SimpleCombat combat = new SimpleCombat(); new Thread(combat).start();
19 Наследование класса Thread и реализация интерфейса Runnable Реализация интерфейса. > Требует больше кода. > Реализация интерфейса. > Создание класса Thread. > Еще можно наследоваться от другого класса. Наследование от класса Thread. > Проще в реализации. > Класс больше не может никого расширять. Если нет других ограничений, то выбор между двумя подходами – дело вкуса.
20 Создание анонимного класса new Thread() { public void run(){ // код } }.start();
21 Класс java.lang.ThreadGroup Потоки могут объединяться в группы > Группы могут включать в себя другие группы. Потоку разрешено получать информацию о своей группе. > Запрещено о других группах и о более крупных группах (являющихся надгруппами). Создается объект класса ThreadGroup и передается в конструктор при создании объекта класса Thread.
22 Потоки и исключительные ситуации Исключительная ситуация обрабатывается в следующем порядке > в самом потоке > дается запрос на обработчик UncaughtExceptionHandler через метод Thread.getUncaughtExceptionHandler() и вызывается метод uncaughtException() этого обработчика > ThreadGroup выступает в качестве UncaughtExceptionHandler > делается запрос в обработчик по умолчанию Thread.getDefaultUncaughtExceptionHandler() Если поток или группа потоков не обрабатывает исключение, то оно обрабатывается JVM.
23 Потоки и исключительные ситуации class MyThread extends Thread { public void run() { throw new RuntimeException("xxx"); } class CustomHandler implements Thread.UncaughtExceptionHandler { public void uncaughtException(Thread t, Throwable e) { System.out.println("custom " + e.getMessage()); } public class Test { public static void main (String[] args) { MyThread thr = new MyThread(); thr.setUncaughtExceptionHandler(new CustomHandler()); thr.start(); }
24 Планировщик потоков class Hero extends Thread { String name; //как зовут героя Hero enemy; //кто враг и кого атаковать public Combat(String name){ this.name = name; } public void assignEnemy(Hero enemy){ this.enemy = enemy; } public void run(){ for (int i=0;i
25 Планировщик потоков (cont.) //создаем два персонажа: Hero batman = new Hero(Batman); Hero joker = new Hero(Joker); //назначаем соперников: batman.assignEnemy(joker); joker.assignEnemy(batman); //негодяй нападает первым: joker.start(); batman.start(); Перед началом поединка нужно создать персонажи, назначить соперников. Исход поединка определится тем, насколько часто один или другой поток получат управление.
26 Приоритет Потоку можно установить относительный приоритет выполнения. // Приоритет назначается в зависимости от рода задачи, выполняемой на нем class Runner extends Thread{ public Runner(){ setPriority(Thread.MAX_PRIORITY); //Thread.MIN_PRIORITY //Thread.NORM_PRIORITY start(); }
27 Остановка потока public class Combat extends Thread{ public Combat() { //при создании экземпляра запускаем поток this.start(); } public void run(){ while (true){/*бой продолжается*/ } } //На другом потоке: Thread combat = new Combat(); combat.stop(); Поток можно принудительно остановить, если необходимость в нем отпала. Метод stop() использовать не рекомендуется:
28 Остановка потока (cont.) public class FairCombat extends Thread { volatile boolean win = false; public void run(){ while (true){// бой продолжается if(enemy.isEmpty()) {//до последнего противника win = true; return; } /* или до победы*/ if (win) {return;} }//while } Корректный способ остановки потока – с использованием выхода из цикла:
29 Соревнование потоков Соревнование имеет место при участии нескольких асинхронных потоков, использующих один и тот же ресурс (разделяемый ресурс). В данном случае потоки могут возвращать неверные результаты. Пример: > Одновременное чтение и запись в файл может повредить данные в нем. Проблем можно избежать, если синхронизовать потоки, использующие разделяемые ресурсы. Так же называется thread race, thread condition.
30 Работа с разделяемым ресурсом public class SeniorStable { private int horseCount = 10; public void returnHorse(){horseCount++;} public void takeHorse(){horseCount--;} public int getHorseCount(){ return horseCount; } Класс содержит в себе целое число и методы для увеличения этого числа на 1 и уменьшения на 1.
31 Работа с разделяемым ресурсом (cont.) final SeniorStable ss = new SeniorStable (); new Thread(Quest"){ //чтобы выполнить задание public void run(){ //абсолютно необходима лошадь while (true){ if(ss.getHorseCount()>0){ ss.takeHorse(); //берем ее в конюшне doQuest(); ss.returnHorse(); //возвращаем обратно } }.start();
32 Если запустить несколько потоков, то может случиться так, что два человека одновременно забирают последнюю лошадь. В конюшне остается -1 лошадь?! Что будет если два человека заберут по одной лошади?
33 Решение: попытка 1 public class SeniorStable { private int horseCount = 10; public void returnHorse(){horseCount++;} public void takeHorse(){ (if horseCount>0) horseCount--; } public int getHorseCount(){ return horseCount; } Задания выполнены, лошадей вернули. В конюшне появилась лишняя лошадь! Волшебство продолжается?
34 Решение: попытка 2 public class SeniorStable { private int horseCount = 10; public void synchronized returnHorse(){ horseCount++; } public void synchronized takeHorse(){ horseCount--; } public int synchronized getHorseCount(){ return horseCount; }} Метод или блок кода, выполнение которого не должно прерываться исполнением того же самого кода но на другом потоке, можно помечать модификатором synchronized.
35 Объект синхронизации Для задания синхронизации между методами нужен некий объект. Обычно таким объектом является разделяемый ресурс. Синхронизованный метод становится «владельцем» монитора объекта. Разделяемым ресурсом может быть ссылочный тип (объект) класс
36 Задача: возможно ли одновременное исполнение методов? class A{ void a1(){} synchronized void a2(){} static synchronized void a3(){} } A x = new A(); A y = new A(); Th1 Th2 x.a1() и x.a1() x.a1() и y.a1() x.a2() и x.a2() x.a2() и y.a2() x.a3() и x.a3() x.a3() и y.a3()
37 Объект синхронизации public class SeniorStable { private Integer horseCount = new Integer(10); public void returnHorse(){horseCount++;} public void takeHorse(){ synchronized(horseCount) {horseCount--;} } public int getHorseCount(){ return horseCount; } В данном случае объектом синхронизации потоков является horseCount – ссылочный тип.
38 Объект синхронизации: класс При этом синхронизация происходит по объекту класса SeniorStable. public class SeniorStable { private int horseCount = 10; public void synchronized returnHorse(){ horseCount++; } public void synchronized takeHorse(){ horseCount--; } public int synchronized getHorseCount(){ return horseCount; }}
39 Возможные причины блокировки потока Был вызван метод sleep(). > Поток блокируется на определенное время. > По истечению этого времени поток переводится в активное состояние. Поток пытается войти в критический участок, но соответствующий ресурс заблокирован каким-то другим потоком. > Данный поток блокируется до тех пор, пока не будет разблокирован этот ресурс (если нет других потоков, ожидающих тот же ресурс). Поток выполняет операцию ввода/вывода. > Поток блокируется до окончания этой операции. У потока был вызван метод wait(). > Поток блокируется до окончания отведенного временного интервала или вызова метода notify() или notifyAll().
40 Реакция на прерывание потока public class Quest extends Thread{ public void run(){ try { sleep(100); } catch (InterruptedException ex) { ex.printStackTrace(); } Во время ожидания может произойти прерывание. Их необходимо обработать:
41 Межпотоковое взаимодействие public final void wait() throws InterruptedException Заставляет поток ожидать когда какой-то другой поток вызовет метод notify() или notifyAll() для данного объекта. public final void notify() Пробуждает поток, который вызвал метод wait() для этого же объекта. public final void notifyAll() Пробуждает все потоки, который вызвали метод wait() для этого же объекта. Следующие методы класса Object используются для реализации взаимодействия потоков между собой.
42 Метод Object.wait() Метод wait() отпускает блокировку объекта, удерживаемого данным потоком. Метод должен вызываться только из synchronized блока. Метод может быть вызван только потоком, владеющим блокировкой для данного объекта. Вызов метода должен быть окружен блоком try-catch для исключения типа IOExcpetion.
43 Метод Object.wait() (cont.) При вызове метода wait() поток становится недоступным для диспетчеризации (ему не может быть передано управление) до тех пор, пока не выполнится одно из следующих условий: Другой поток вызвал метод notify(). Другой поток вызвал метод notifyAll(). Другой поток прервал ожидающий поток. Заданный интервал времени истек (wait(long millis)) После выполнения одного из условий поток становится снова диспетчеризуемым.
44 Метод Object.notify() Пробуждает один поток, ожидающий на данном объекте. Если ожидают много потоков, то для пробуждения выбирается только один из них. Выбор зависит от реализации. Может вызываться только внутри синхронизованных секций. Пробужденный поток не способен продолжать работать до тех пор, пока текущий поток не освободит объект (не покинет синхронизованный блок).
45 Читатель-писатель Два потока взаимодействуют между собой посредством некоторого хранилища данных. Первый поток (писатель) в хранилище записывает данные. Второй поток (читатель) из хранилища считывает данные, затем удаляя их. Решение данной задачи предполагает наличие некого протокола взаимодействия двух потоков.
46 Несинхронизованное взаимодействие Класс-абстракция разделяемого ресурса. public class CubbyHole { private int contents; public int get() { return contents; } public synchronized void put(int value){ contents = value; }
47 Класс Писатель Писатель помещает число и ждет случайный по длительности интервал времени. public class Producer extends Thread { private CubbyHole cubbyhole; private int number; public Producer(CubbyHole c, int number) { cubbyhole = c; this.number = number; } public void run() { for (int i = 0; i < 10; i++) { cubbyhole.put(i); System.out.println("Producer #" + this.number + " put: " + i); try { sleep((int)(Math.random() * 100)); } catch (InterruptedException e) { } }
48 Класс Читатель Читатель считывает число. public class Consumer extends Thread { private CubbyHole cubbyhole; private int number; public Consumer(CubbyHole c, int number) { cubbyhole = c; this.number = number; } public void run() { int value = 0; for (int i = 0; i < 10; i++) { value = cubbyhole.get(); System.out.println("Consumer #" + this.number + " got: " + value); }
49 Запуск программы Класс, создающий независимые потоки Писателя и Читателя. public class ProducerConsumerUnsynchronized { public static void main(String[] args) { CubbyHole c = new CubbyHole(); Producer p1 = new Producer(c, 1); Consumer c1 = new Consumer(c, 1); p1.start(); c1.start(); }
50 Результат работы Непредсказуемые. > Одно число может быть прочитано множество раз. > Число может не быть прочитано ни разу. Consumer #1 got: 0 Producer #1 put: 0 Consumer #1 got: 0 Producer #1 put: 1 Producer #1 put: 2 Producer #1 put: 3 Producer #1 put: 4 Producer #1 put: 5 Producer #1 put: 6 Producer #1 put: 7 Producer #1 put: 8 Producer #1 put: 9
51 Правильное решение Синхронизация доступа к разделяемому ресурсу. public class CubbyHole { private int contents; private boolean available = false; public synchronized int get() { while (available == false) { try { wait(); } catch (InterruptedException e) { } } available = false; notifyAll(); return contents; } // continued
52 Правильное решение Синхронизация доступа к разделяемому ресурсу: public synchronized void put(int value) { while (available == true) { try { wait(); } catch (InterruptedException e) { } } contents = value; available = true; notifyAll(); }
53 Результат работы Предсказуемые. > Каждому записанному числу соответствует единственное чтение. Producer 1 put: 0 Consumer 1 got: 0 Producer 1 put: 1 Consumer 1 got: 1 Producer 1 put: 2 Consumer 1 got: 2 Producer 1 put: 3 Consumer 1 got: 3 Producer 1 put: 4 Consumer 1 got: 4 Producer 1 put: 5 Consumer 1 got: 5 Producer 1 put: 6 Consumer 1 got: 6 Producer 1 put: 7 Consumer 1 got: 7 Producer 1 put: 8 Consumer 1 got: 8 Producer 1 put: 9 Consumer 1 got: 9
54 Повторное использование блокировок public synchronized void first(){ second(); } public synchronized void second(){..} Вызов синхронизованного метода из другого синхронизованного метода возможен при условии что объект синхронизации одинаков.
55 Тупик (deadlock) synchronized(a){ synchronized(b) {…} } … synchronized(b){ synchronized(a) {…} } Тупиком называется состояние взаимной блокировки потоков, каждому из которых требуется ресурс, блокируемый другим потоком. Так же называется взаимная блокировка или клинч.
56 Демон (daemon) public class ColdWarDaemon extends Thread{ public ColdWarDaemon() setDaemon(true); start(); } public void run(){ while (true){…} } Поток с выставленным флагом isDaemon называется демоном. Потоки-демоны, в отличие от обычных потоков, не препятствуют завершению работы виртуальной машины. Если поток-демон создает другие потоки, то они также получат статус потока-демона.
57 Исполнение по расписанию Класс Timer. Предоставляет возможность выполнить задание в будущем (поставить в очередь) в отдельном потоке. Задания могут выполняться единожды или через указанный интервал времени. Каждый класс Timer владеет потоком, на котором исполняются все задания. Класс TimerTask. Абстрактный класс с абстрактным методом run(). Конкретный класс должен реализовать метод run().
58 Пример исполнения по расписанию Сообщение выдается через несколько секунд после инициализации приложения. public class TimerReminder { Timer timer; public TimerReminder(int seconds) { timer = new Timer(); timer.schedule(new RemindTask(), seconds*1000); } class RemindTask extends TimerTask { public void run() { System.out.format("Time's up!%n"); timer.cancel(); //Terminate the timer thread } public static void main(String args[]) { System.out.format("About to schedule task.%n"); new TimerReminder(5); System.out.format("Task scheduled.%n"); }
59 Ссылки Doug Lea, Concurrent Programming in Java П.Ноутон, Г.Шилдт, Java2. Наиболее полное руководство
Q&A
Обеспечение эффективной многопоточности Java приложений Андрей Дмитриев Ноябрь 2009