410013796724260
• Webmoney
R335386147728
Z369087728698
Многопоточность Thread, RunnableМногопоточное программирование позволяет разделить представление и обработку информации на несколько «легковесных» процессов (light-weight processes), имеющих общий доступ как к методам различных объектов приложения, так и к их полям. Многопоточность незаменима в тех случаях, когда графический интерфейс должен реагировать на действия пользователя при выполнении определенной обработки информации. Потоки могут взаимодействовать друг с другом через основной «родительский» поток, из которого они стартованы. В качестве примера можно привести некоторый поток, отвечающий за представление информации в интерфейсе, который ожидает завершения работы другого потока, загружающего файл, и одновременно отображает некоторую анимацию или обновляет прогресс-бар. Кроме того этот поток может остановить загружающий файл поток при нажатии кнопки «Отмена». Создатели Java предоставили две возможности создания потоков: реализация (implementing) интерфейса Runnable и расширение(extending) класса Thread. Расширение класса - это путь наследования методов и переменных класса родителя. В этом случае можно наследоваться только от одного родительского класса Thread. Данное ограничение внутри Java можно преодолеть реализацией интерфейса Runnable, который является наиболее распространённым способом создания потоков. Преимущества потоков перед процессами
Главный потокКаждое java приложение имеет хотя бы один выполняющийся поток. Поток, с которого начинается выполнение программы, называется главным. После создания процесса, как правило, JVM начинает выполнение главного потока с метода main(). Затем, по мере необходимости, могут быть запущены дополнительные потоки. Многопоточность — это два и более потоков, выполняющихся одновременно в одной программе. Компьютер с одноядерным процессором может выполнять только один поток, разделяя процессорное время между различными процессами и потоками. Класс ThreadВ классе Thread определены семь перегруженных конструкторов, большое количество методов, предназначенных для работы с потоками, и три константы (приоритеты выполнения потока). Конструкторы класса ThreadThread(); Thread(Runnable target); Thread(Runnable target, String name); Thread(String name); Thread(ThreadGroup group, Runnable target); Thread(ThreadGroup group, Runnable target, String name); Thread(ThreadGroup group, String name); где :
Пример создания потока, который входит в группу, реализует интерфейс Runnable и имеет свое уникальное название : Runnable r = new MyClassRunnable(); ThreadGroup tg = new ThreadGroup(); Thread t = new Thread(tg, r, "myThread"); Группы потоков удобно использовать, когда необходимо одинаково управлять несколькими потоками. Например, несколько потоков выводят данные на печать и необходимо прервать печать всех документов поставленных в очередь. В этом случае удобно применить команду ко всем потокам одновременно, а не к каждому потоку отдельно. Но это можно сделать, если потоки отнесены к одной группе. Несмотря на то, что главный поток создаётся автоматически, им можно управлять. Для этого необходимо создать объект класса Thread вызовом метода currentThread(). Методы класса ThreadНаиболее часто используемые методы класса Thread для управления потоками :
Жизненный цикл потокаПри выполнении программы объект Thread может находиться в одном из четырех основных состояний: «новый», «работоспособный», «неработоспособный» и «пассивный». При создании потока он получает состояние «новый» (NEW) и не выполняется. Для перевода потока из состояния «новый» в «работоспособный» (RUNNABLE) следует выполнить метод start(), вызывающий метод run(). Поток может находиться в одном из состояний, соответствующих элементам статически вложенного перечисления Thread.State : NEW — поток создан, но еще не запущен;RUNNABLE — поток выполняется; BLOCKED — поток блокирован; WAITING — поток ждет окончания работы другого потока; TIMED_WAITING — поток некоторое время ждет окончания другого потока; TERMINATED — поток завершен. Пример использования ThreadВ примере ChickenEgg рассматривается параллельная работа двух потоков (главный поток и поток Egg), в которых идет спор, «что было раньше, яйцо или курица?». Каждый поток высказывает свое мнение после небольшой задержки, формируемой методом ChickenEgg.getTimeSleep(). Побеждает тот поток, который последним говорит свое слово. package example; import java.util.Random; class Egg extends Thread { @Override public void run() { for(int i = 0; i < 5; i++) { try { // Приостанавливаем поток sleep(ChickenEgg.getTimeSleep()); System.out.println("Яйцо"); }catch(InterruptedException e){} } } } public class ChickenEgg { public static int getTimeSleep() { final Random random = new Random(); int tm = random.nextInt(1000); if (tm < 10) tm *= 100; else if (tm < 100) tm *= 10; return tm; } public static void main(String[] args) { Egg egg = new Egg (); // Создание потока System.out.println( "Начинаем спор : кто появился первым ?"); egg.start(); // Запуск потока for(int i = 0; i < 5; i++) { try { // Приостанавливаем поток Thread.sleep(ChickenEgg.getTimeSleep()); System.out.println("Курица"); }catch(InterruptedException e){} } if(egg.isAlive()) { // Cказало ли яйцо последнее слово? try { // Ждем, пока яйцо закончит высказываться egg.join(); } catch (InterruptedException e){} System.out.println("Первым появилось яйцо !!!"); } else { //если оппонент уже закончил высказываться System.out.println("Первой появилась курица !!!"); } System.out.println("Спор закончен"); } } При выполнении программы в консоль было выведено следующее сообщение. Начинаем спор : кто появился первым ? Курица Курица Яйцо Курица Яйцо Яйцо Курица Курица Яйцо Яйцо Первым появилось яйцо !!! Спор закончен Невозможно точно предсказать, какой поток закончит высказываться последним. При следующем запуске «победитель» может измениться. Это происходит вследствии так называемого «асинхронного выполнения кода». Асинхронность обеспечивает независимость выполнения потоков. Или, другими словами, параллельные потоки независимы друг от друга, за исключением случаев, когда бизнес-логика зависимости выполнения потоков определяется предусмотренными для этого средств языка. Интерфейс RunnableИнтерфейс Runnable содержит только один метод run() : interface Runnable { void run(); } Метод run() выполняется при запуске потока. После определения объекта Runnable он передается в один из конструкторов класса Thread. Пример класса RunnableExample, реализующего интерфейс Runnablepackage example; class MyThread implements Runnable { Thread thread; MyThread() { thread = new Thread(this, "Дополнительный поток"); System.out.println("Создан дополнительный поток " + thread); thread.start(); } @Override public void run() { try { for (int i = 5; i > 0; i--) { System.out.println( "\tдополнительный поток: " + i); Thread.sleep(500); } } catch (InterruptedException e) { System.out.println( "\tдополнительный поток прерван"); } System.out.println( "\tдополнительный поток завершён"); } } public class RunnableExample { public static void main(String[] args) { new MyThread(); try { for (int i = 5; i > 0; i--) { System.out.println("Главный поток: " + i); Thread.sleep(1000); } } catch (InterruptedException e) { System.out.println("Главный поток прерван"); } System.out.println("Главный поток завершён"); } } При выполнении программы в консоль было выведено следующее сообщение. Создан дополнительный поток Thread[Дополнительный поток,5,main] Главный поток: 5 дополнительный поток: 5 дополнительный поток: 4 Главный поток: 4 дополнительный поток: 3 дополнительный поток: 2 Главный поток: 3 дополнительный поток: 1 дополнительный поток завершён Главный поток: 2 Главный поток: 1 Главный поток завершён Синхронизация потоков, synchronizedВ процессе функционирования потоки часто используют общие ресурсы приложения, определенные вне потока. Если несколько потоков начнут одновременно вносить изменения в общий ресурс, то результаты выполнения программы могут быть непредсказуемыми. Рассмотрим следующий пример : package example; class CommonObject { int counter = 0; } class CounterThread implements Runnable { CommonObject res; CounterThread(CommonObject res) { this.res = res; } @Override public void run() { // synchronized(res) { res.counter = 1; for (int i = 1; i < 5; i++){ System.out.printf("'%s' - %d\n", Thread.currentThread().getName(), res.counter); res.counter++; try { Thread.sleep(100); } catch(InterruptedException e){} } // } } } public class SynchronizedThread { public static void main(String[] args) { CommonObject commonObject= new CommonObject(); for (int i = 1; i < 6; i++) { Thread t; t = new Thread(new CounterThread(commonObject)); t.setName("Поток " + i); t.start(); } } } В примере определен общий ресурс в виде класса CommonObject, в котором имеется целочисленное поле counter. Данный ресурс используется внутренним классом, создающим поток CounterThread для увеличения в цикле значения counter на единицу. При старте потока полю counter присваивается значение 1. После завершения работы потока значение res.counter должно быть равно 4. Две строчки кода класса CounterThread закомментированы. О них речь пойдет ниже. В главном классе программы SynchronizedThread.main запускается пять потоков. То есть, каждый поток должен в цикле увеличить значение res.counter с единицы до четырех; и так пять раз. Но результат работы программы, отображаемый в консоли, будет иным : 'Поток 4' - 1 'Поток 2' - 1 'Поток 1' - 1 'Поток 5' - 1 'Поток 3' - 1 'Поток 2' - 6 'Поток 4' - 7 'Поток 3' - 8 'Поток 5' - 9 'Поток 1' - 10 'Поток 2' - 11 'Поток 4' - 12 'Поток 5' - 13 'Поток 3' - 13 'Поток 1' - 15 'Поток 4' - 16 'Поток 2' - 16 'Поток 3' - 18 'Поток 5' - 18 'Поток 1' - 20 То есть, с общим ресурсов res.counter работают все потоки одновременно, поочередно изменяя значение. Чтобы избежать подобной ситуации, потоки необходимо синхронизировать. Одним из способов синхронизации потоков связан с использованием ключевого слова synchronized. Оператор synchronized позволяет определить блок кода или метод, который должен быть доступен только одному потоку. Можно использовать synchronized в своих классах определяя синхронизированные методы или блоки. Но нельзя использовать synchronized в переменных или атрибутах в определении класса. Блокировка на уровне объектаБлокировать общий ресурс можно на уровне объекта, но нельзя использовать для этих целей примитивные типы. В примере следует удалить строчные комментарии в классе CounterThread, после чего общий ресурс будет блокироваться как только его захватит один из потоков; остальные потоки будут ждать в очереди освобождения ресурса. Результат работы программы при синхронизации доступа к общему ресурсу резко изменится : 'Поток 1' - 1 'Поток 1' - 2 'Поток 1' - 3 'Поток 1' - 4 'Поток 5' - 1 'Поток 5' - 2 'Поток 5' - 3 'Поток 5' - 4 'Поток 4' - 1 'Поток 4' - 2 'Поток 4' - 3 'Поток 4' - 4 'Поток 3' - 1 'Поток 3' - 2 'Поток 3' - 3 'Поток 3' - 4 'Поток 2' - 1 'Поток 2' - 2 'Поток 2' - 3 'Поток 2' - 4 Следующий код демонстрирует порядок использования оператора synchronized для блокирования доступа к объекту. synchronized (оbject) { // other thread safe code } Блокировка на уровне метода и классаБлокировать доступ к ресурсам можно на уровне метода и класса. Следующий код показывает, что если во время выполнения программы имеется несколько экземпляров класса DemoClass, то только один поток может выполнить метод demoMethod(), для других потоков доступ к методу будет заблокирован. Это необходимо когда требуется сделать определенные ресурсы потокобезопасными. public class DemoClass { public synchronized static void demoMethod(){ // ... } } // или public class DemoClass { public void demoMethod(){ synchronized (DemoClass.class) { // ... } } } Каждый объект в Java имеет ассоциированный с ним монитор, который представляет своего рода инструмент для управления доступа к объекту. Когда выполнение кода доходит до оператора synchronized, монитор объекта блокируется, предоставляя монопольный доступ к блоку кода только одному потоку, который произвел блокировку. После окончания работы блока кода, монитор объекта освобождается и он становится доступным для других потоков. Некоторые важные замечания использования synchronized
Примечание : для синхронизации потоков можно использовать объекты синхронизации Synchroniser's пакета java.util.concurrent. Взаимная блокировкаС использованием блокировок необходимо быть очень внимательным, чтобы не создать «взаимоблокировку», которая хорошо известна разработчикам. Этот термин означает, что один из потоков ждет от другого освобождения заблокированного им ресурса, в то время как сам также заблокировал один из ресурсов, доступа к которому ждёт второй поток. В данном процессе могут участвовать два и более потоков. Основные условия возникновения взаимоблокировок в многопотоковом приложении :
Взаимодействие между потоками в Java, wait и notifyПри взаимодействии потоков часто возникает необходимость приостановки одних потоков и их последующего извещения о завершении определенных действий в других потоков. Так например, действия первого потока зависят от результата действий второго потока, и надо каким-то образом известить первый поток, что второй поток произвел/завершил определенную работу. Для подобных ситуаций используются методы :
Все эти методы вызываются только из синхронизированного контекста (синхронизированного блока или метода). Рассмотрим пример «Производитель-Склад-Потребитель» (Producer-Store-Consumer). Пока производитель не поставит на склад продукт, потребитель не может его забрать. Допустим производитель должен поставить 5 единиц определенного товара. Соответственно потребитель должен весь товар получить. Но, при этом, одновременно на складе может находиться не более 3 единиц товара. При реализации данного примера используем методы wait() и notify(). Листинг класса Storepackage example; public class Store { private int counter = 0; public synchronized void get() { while (counter < 1) { try { wait(); } catch (InterruptedException e) {} } counter--; System.out.println("-1 : товар забрали"); System.out.println( "\tколичество товара на складе : " + counter); notify(); } public synchronized void put() { while (counter >= 3) { try { wait(); }catch (InterruptedException e) {} } counter++; System.out.println("+1 : товар добавили"); System.out.println( "\tколичество товара на складе : " + counter); notify(); } } Класс Store содержит два синхронизированных метода для получения товара get() и для добавления товара put(). При получении товара выполняется проверка счетчика counter. Если на складе товара нет, то есть counter < 1, то вызывается метод wait(), который освобождает монитор объекта Store и блокирует выполнение метода get(), пока для этого монитора не будет вызван метод notify(). При добавлении товара также выполняется проверка количества товара на складе. Если на складе больше 3 единиц товара, то поставка товара приостанавливается и вызывается метод notify(), который передает управление методу get() для завершения цикла while(). Листинги классов Producer и ConsumerКлассы Producer и Consumer реализуют интерфейс Runnable, методы run() у них переопределены. Конструкторы этих классов в качестве параметра получают объект склад Store. При старте данных объектов в виде отдельных потоков в цикле вызываются методы put() и get() класса Store для «добавления» и «получения» товара. package example; public class Producer implements Runnable { Store store; Producer(Store store) { this.store=store; } @Override public void run() { for (int i = 1; i < 6; i++) { store.put(); } } } public class Consumer implements Runnable { Store store; Consumer(Store store) { this.store=store; } @Override public void run(){ for (int i = 1; i < 6; i++) { store.get(); } } } Листинг класса TradeВ главном потоке класса Trade (в методе main) создаются объекты Producer-Store-Consumer и стартуются потоки производителя и потребителя. package example; public class Trade { public static void main(String[] args) { Store store = new Store(); Producer producer = new Producer(store); Consumer consumer = new Consumer(store); new Thread(producer).start(); new Thread(consumer).start(); } } При выполнении программы в консоль будут выведены следующие сообщения : +1 : товар добавили количество товара на складе : 1 +1 : товар добавили количество товара на складе : 2 +1 : товар добавили количество товара на складе : 3 -1 : товар забрали количество товара на складе : 2 -1 : товар забрали количество товара на складе : 1 -1 : товар забрали количество товара на складе : 0 +1 : товар добавили количество товара на складе : 1 +1 : товар добавили количество товара на складе : 2 -1 : товар забрали количество товара на складе : 1 -1 : товар забрали количество товара на складе : 0 Поток-демон, daemonJava приложение завершает работу тогда, когда завершает работу последний его поток. Даже если метод main() уже завершился, но еще выполняются порожденные им потоки, система будет ждать их завершения. Однако это правило не относится к потоков-демонам (daemon). Если завершился последний обычный поток процесса, и остались только daemon потоки, то они будут принудительно завершены и выполнение приложения закончится. Чаще всего daemon потоки используются для выполнения фоновых задач, обслуживающих процесс в течение его жизни. Объявить поток демоном достаточно просто. Для этого нужно перед запуском потока вызвать его метод setDaemon(true). Проверить, является ли поток daemon'ом можно вызовом метода isDaemon(). В качестве примера использования daemon-потока можно рассмотреть класс Trade, который принял бы следующий вид : package example; public class Trade { public static void main(String[] args) { Producer producer = new Producer(store); Consumer consumer = new Consumer(store); // new Thread(producer).start(); // new Thread(consumer).start(); Thread tp = new Thread(producer); Thread tc = new Thread(consumer); tp.setDaemon(true); tc.setDaemon(true); tp.start(); tc.start(); try { Thread.sleep(100); } catch (InterruptedException e) {} System.out.println("\nГлавный поток завершен\n"); System.exit(0); } } Здесь можно самостоятельно поэкспериментировать с определением daemon-потока для одного из классов (producer, consumer) или обоих классов, и посмотреть, как система (JVM) будет вести себя. Thread и Runnable, что выбрать ?Зачем нужно два вида реализации многопоточности; какую из них и когда использовать? Ответ несложен. Реализация интерфейса Runnable используется в случаях, когда класс уже наследует какой-либо родительский класс и не позволяет расширить класс Thread. К тому же хорошим тоном программирования в java считается реализация интерфейсов. Это связано с тем, что в java может наследоваться только один родительский класс. Таким образом, унаследовав класс Thread, невозможно наследовать какой-либо другой класс. Расширение класса Thread целесообразно использовать в случае необходимости переопределения других методов класса помимо метода run(). Приоритеты выполнения и голоданиеИногда разработчики используют приоритеты выполнения потока. В Java есть планировщик потоков (Thread Scheduler), который контролирует все запущенные потоки и решает, какие потоки должны быть запущены и какая строка кода должна выполняться. Решение основывается на приоритете потока. Поэтому потоки с меньшим приоритетом получают меньше процессорного времени по сравнению с потоками с бо́льшим приоритет. Данное разумное решением может стать причиной проблем при злоупотреблении. То есть, если бо́льшую часть времени исполняются потоки с высоким приоритетом, то низкоприоритетные потоки начинают «голодать», поскольку не получают достаточно времени для того, чтобы выполнить свою работу должным образом. Поэтому рекомендуется задавать приоритет потока только тогда, когда для этого имеются веские основания. Неочевидный пример «голодания» потока даёт метод finalize(), предоставляющий возможность выполнить код перед тем, как объект будет удалён сборщиком мусора. Однако приоритет финализирующего потока невысокий. Следовательно, возникают предпосылки для потокового голодания, когда методы finalize() объекта тратят слишком много времени (большие задержки) по сравнению с остальным кодом. Другая проблема со временем исполнения может возникнуть от того, что не был определен порядок прохождения потоком блока synchronized. Когда несколько параллельных потоков должны выполнить некоторый код, оформленный блоком synchronized, может получиться так, что одним потокам придётся ждать дольше других, прежде чем войти в блок. Теоретически они могут вообще туда не попасть. Скачать примерыРассмотренные на странице примеры многопоточности и синхронизации потоков в виде проекта Eclipse можно скачать здесь (14Кб). |