Java примеры потоки. Процессы, потоки и приоритеты. Листинг класса Store

Последнее обновление: 27.04.2018

Большинство языков программирования поддерживают такую важную функциональность как многопоточность, и Java в этом плане не исключение. При помощи многопоточности мы можем выделить в приложении несколько потоков, которые будут выполнять различные задачи одновременно. Если у нас, допустим, графическое приложение, которое посылает запрос к какому-нибудь серверу или считывает и обрабатывает огромный файл, то без многопоточности у нас бы блокировался графический интерфейс на время выполнения задачи. А благодаря потокам мы можем выделить отправку запроса или любую другую задачу, которая может долго обрабатываться, в отдельный поток. Поэтому большинство реальных приложений, которые многим из нас приходится использовать, практически не мыслимы без многопоточности.

Класс Thread

В Java функциональность отдельного потока заключается в классе Thread . И чтобы создать новый поток, нам надо создать объект этого класса. Но все потоки не создаются сами по себе. Когда запускается программа, начинает работать главный поток этой программы. От этого главного потока порождаются все остальные дочерние потоки.

С помощью статического метода Thread.currentThread() мы можем получить текущий поток выполнения:

Public static void main(String args) { Thread t = Thread.currentThread(); // получаем главный поток System.out.println(t.getName()); // main }

По умолчанию именем главного потока будет main .

Для управления потоком класс Thread предоставляет еще ряд методов. Наиболее используемые из них:

    getName() : возвращает имя потока

    setName(String name) : устанавливает имя потока

    getPriority() : возвращает приоритет потока

    setPriority(int proirity) : устанавливает приоритет потока. Приоритет является одним из ключевых факторов для выбора системой потока из кучи потоков для выполнения. В этот метод в качестве параметра передается числовое значение приоритета - от 1 до 10. По умолчанию главному потоку выставляется средний приоритет - 5.

    isAlive() : возвращает true, если поток активен

    isInterrupted() : возвращает true, если поток был прерван

    join() : ожидает завершение потока

    run() : определяет точку входа в поток

    sleep() : приостанавливает поток на заданное количество миллисекунд

    start() : запускает поток, вызывая его метод run()

Мы можем вывести всю информацию о потоке:

Public static void main(String args) { Thread t = Thread.currentThread(); // получаем главный поток System.out.println(t); // main }

Консольный вывод:

Thread

Первое main будет представлять имя потока (что можно получить через t.getName()), второе значение 5 предоставляет приоритет потока (также можно получить через t.getPriority()), и последнее main представляет имя группы потоков, к которому относится текущий - по умолчанию также main (также можно получить через t.getThreadGroup().getName())

Недостатки при использовании потоков

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

На некоторых платформах запуск новых потоков может замедлить работу приложения. Что может иметь большое значение, если нам критичная производительность приложения.

Для каждого потока создается свой собственный стек в памяти, куда помещаются все локальные переменные и ряд других данных, связанных с выполнением потока. Соответственно, чем больше потоков создается, тем больше памяти используется. При этом надо помнить, в любой системе размеры используемой памяти ограничены. Кроме того, во многих системах может быть ограничение на количество потоков. Но даже если такого ограничения нет, то в любом случае имеется естественное ограничение в виде максимальной скорости процессора.

6 ответов

Существует ровно один способ создать новый поток в Java и создать экземпляр java.lang.Thread (чтобы запустить этот поток, вам также нужно позвонить start()).

Все остальное, что создает потоки в Java-коде, возвращается к этому одному пути за крышкой (например, реализация ThreadFactory будет создавать экземпляры объектов Thread в какой-то момент,...).

Существует два разных способа указать, какой код следует запускать в этом потоке:

  • Внедрите интерфейс java.lang.Runnable и передайте экземпляр класса, реализующего его, в конструктор Thread .
  • Расширьте Thread и переопределите его метод run() .

Первый подход (реализация Runnable) обычно считается более правильным, потому что вы обычно не создаете новый "вид" Thread, но просто хотите запустить некоторый код (т.е. a Runnable) в выделенный поток.

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

  • Расширить java.lang. Тема класс
class SampleThread extends Thread { //method where the thread execution will start public void run(){ //logic to execute in a thread } //let’s see how to start the threads public static void main(String args){ Thread t1 = new SampleThread(); Thread t2 = new SampleThread(); t1.start(); //start the first thread. This calls the run() method. t2.start(); //this starts the 2nd thread. This calls the run() method. } }
  1. Внедрить интерфейс java.lang. Runnable
class A implements Runnable{ @Override public void run() { // implement run method here } public static void main() { final A obj = new A(); Thread t1 = new Thread(new A()); t1.start(); } }
  1. Внедрить интерфейс java.util.concurrent. Callable
class Counter implements Callable { private static final int THREAD_POOL_SIZE = 2; // method where the thread execution takes place public String call() { return Thread.currentThread().getName() + " executing ..."; } public static void main(String args) throws InterruptedException, ExecutionException { // create a pool of 2 threads ExecutorService executor = Executors .newFixedThreadPool(THREAD_POOL_SIZE); Future future1 = executor.submit(new Counter()); Future future2 = executor.submit(new Counter()); System.out.println(Thread.currentThread().getName() + " executing ..."); //asynchronously get from the worker threads System.out.println(future1.get()); System.out.println(future2.get()); } }

Использовать интерфейс Callable с инфраструктурой Executor для объединения потоков.

Интерфейс Runnable или Callable предпочтительнее расширения класса Thread

Есть только два способа создания потока, о котором вы уже упоминали, но третий способ - вызвать Thread.

В java1.5 есть еще один способ вызвать поток. То есть "Исполнительная служба". Все эти классы взяты из пакета java.util.concurrent. Существуют различные способы создания "ExecutorService" с использованием класса "Исполнители" factory. Ниже приведен один из способов создания "ExecutorService" .

ExecutorService es = Executors.newSingleThreadExecutor();

RunnableImpl r = new RunnableImpl();

Будущее fu = es.submit(r);

Используя методы "ExecutorService" , мы можем отправить истребителю Runnable или Callable в службу для выполнения.

Как бы то ни было, это невозможно назвать новым способом создания Thread. Это связано с тем, что ExecutorService внутренне использует класс ThreadFactory для создания нового потока, который внутренне использует истребитель первого или второго метода. Поэтому мы должны сказать, что есть только два способа создания потоков, но есть новый способ в java1.5 для вызова потока, но не для создания потока.

Многопоточное программирование позволяет разделить представление и обработку информации на несколько «легковесных» процессов (light-weight processes), имеющих общий доступ как к методам различных объектов приложения, так и к их полям. Многопоточность незаменима в тех случаях, когда графический интерфейс должен реагировать на действия пользователя при выполнении определенной обработки информации. Потоки могут взаимодействовать друг с другом через основной «родительский» поток, из которого они стартованы.

В качестве примера можно привести некоторый поток, отвечающий за представление информации в интерфейсе, который ожидает завершения работы другого потока, загружающего файл, и одновременно отображает некоторую анимацию или обновляет прогресс-бар. Кроме того этот поток может остановить загружающий файл поток при нажатии кнопки «Отмена».

Создатели Java предоставили две возможности создания потоков: реализация (implementing) интерфейса Runnable и расширение(extending) класса Thread . Расширение класса - это путь наследования методов и переменных класса родителя. В этом случае можно наследоваться только от одного родительского класса Thread . Данное ограничение внутри Java можно преодолеть реализацией интерфейса Runnable , который является наиболее распространённым способом создания потоков.

Преимущества потоков перед процессами

  • потоки намного легче процессов поскольку требуют меньше времени и ресурсов;
  • переключение контекста между потоками намного быстрее, чем между процессами;
  • намного проще добиться взаимодействия между потоками, чем между процессами.

Главный поток

Каждое java приложение имеет хотя бы один выполняющийся поток. Поток, с которого начинается выполнение программы, называется главным. После создания процесса, как правило, JVM начинает выполнение главного потока с метода main(). Затем, по мере необходимости, могут быть запущены дополнительные потоки. Многопоточность - это два и более потоков, выполняющихся одновременно в одной программе. Компьютер с одноядерным процессором может выполнять только один поток, разделяя процессорное время между различными процессами и потоками.

Класс Thread

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

Конструкторы класса Thread

Thread(); 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);

  • target – экземпляр класса реализующего интерфейс Runnable;
  • name – имя создаваемого потока;
  • group – группа к которой относится поток.

Пример создания потока, который входит в группу, реализует интерфейс Runnable и имеет свое уникальное название:

Runnable r = new MyClassRunnable(); ThreadGroup tg = new ThreadGroup(); Thread t = new Thread(tg, r, "myThread");

Группы потоков удобно использовать, когда необходимо одинаково управлять несколькими потоками. Например, несколько потоков выводят данные на печать и необходимо прервать печать всех документов поставленных в очередь. В этом случае удобно применить команду ко всем потокам одновременно, а не к каждому потоку отдельно. Но это можно сделать, если потоки отнесены к одной группе.

Несмотря на то, что главный поток создаётся автоматически, им можно управлять. Для этого необходимо создать объект класса Thread вызовом метода currentThread() .

Методы класса Thread

Наиболее часто используемые методы класса Thread для управления потоками:

  • long getId() - получение идентификатора потока;
  • String getName() - получение имени потока;
  • int getPriority() - получение приоритета потока;
  • State getState() - определение состояния потока;
  • void interrupt() - прерывание выполнения потока;
  • boolean isAlive() - проверка, выполняется ли поток;
  • boolean isDaemon() - проверка, является ли поток «daemon»;
  • void join() - ожидание завершения потока;
  • void join(millis) - ожидание millis милисекунд завершения потока;
  • void notify() - «пробуждение» отдельного потока, ожидающего «сигнала»;
  • void notifyAll() - «пробуждение» всех потоков, ожидающих «сигнала»;
  • void run() - запуск потока, если поток был создан с использованием интерфейса Runnable;
  • void setDaemon(bool) - определение «daemon» потока;
  • void setPriority(int) - определение приоритета потока;
  • void sleep(int) - приостановка потока на заданное время;
  • void start() - запуск потока.
  • void wait() - приостановка потока, пока другой поток не вызовет метод notify();
  • void wait(millis) - приостановка потока на millis милисекунд или пока другой поток не вызовет метод notify();

Жизненный цикл потока

При выполнении программы объект 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, реализующего интерфейс Runnable

Package 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

  1. Синхронизация в Java гарантирует, что два потока не могут выполнить синхронизированный метод одновременно.
  2. Оператор synchronized можно использовать только с методами и блоками кода, которые могут быть как статическими, так и не статическими.
  3. Если один из потоков начинает выполнять синхронизированный метод или блок, то этот метод/блок блокируются. Когда поток выходит из синхронизированного метода или блока JVM снимает блокировку. Блокировка снимается, даже если поток покидает синхронизированный метод после завершения из-за каких-либо ошибок или исключений.
  4. Синхронизация в Java вызывает исключение NullPointerException, если объект, используемый в синхронизированном блоке, не определен, т.е. равен null.
  5. Синхронизированные методы в Java вносят дополнительные затраты на производительность приложения. Поэтому следует использовать синхронизацию, когда она абсолютно необходима.
  6. В соответствии со спецификацией языка нельзя использовать synchronized в конструкторе, т.к. приведет к ошибке компиляции.

Примечание: для синхронизации потоков можно использовать объекты синхронизации Synchroniser"s пакета java.util.concurrent .

Взаимная блокировка

С использованием блокировок необходимо быть очень внимательным, чтобы не создать «взаимоблокировку», которая хорошо известна разработчикам. Этот термин означает, что один из потоков ждет от другого освобождения заблокированного им ресурса, в то время как сам также заблокировал один из ресурсов, доступа к которому ждёт второй поток. В данном процессе могут участвовать два и более потоков.

Основные условия возникновения взаимоблокировок в многопотоковом приложении:

  • наличие ресурсов, которые должны быть доступны только одному потоку в произвольный момент времени;
  • при захвате ресурса поток пытается захватить еще один уникальный ресурс;
  • отсутствует механизм освобождения ресурса при продолжительном его удержании;
  • во время исполнения несколько потоков могут захватить разные уникальные ресурсы и ждать друг от друга их освобождения.

Взаимодействие между потоками в Java, wait и notify

При взаимодействии потоков часто возникает необходимость приостановки одних потоков и их последующего извещения о завершении определенных действий в других потоков. Так например, действия первого потока зависят от результата действий второго потока, и надо каким-то образом известить первый поток, что второй поток произвел/завершил определенную работу. Для подобных ситуаций используются методы:

  • wait() - освобождает монитор и переводит вызывающий поток в состояние ожидания до тех пор, пока другой поток не вызовет метод notify();
  • notify() - продолжает работу потока, у которого ранее был вызван метод wait();
  • notifyAll() - возобновляет работу всех потоков, у которых ранее был вызван метод wait().

Все эти методы вызываются только из синхронизированного контекста (синхронизированного блока или метода).

Рассмотрим пример «Производитель-Склад-Потребитель» (Producer-Store-Consumer). Пока производитель не поставит на склад продукт, потребитель не может его забрать. Допустим производитель должен поставить 5 единиц определенного товара. Соответственно потребитель должен весь товар получить. Но, при этом, одновременно на складе может находиться не более 3 единиц товара. При реализации данного примера используем методы wait() и notify() .

Листинг класса Store

Package 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

Поток-демон, daemon

Java приложение завершает работу тогда, когда завершает работу последний его поток. Даже если метод 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Кб).




Top