Thread’ом java не испортишь: часть v

Использование Queues

Очередь(Queues Python) может быть использована для стековых реализаций «пришел первым – ушел первым» (first-in-first-out (FIFO)) или же «пришел последним – ушел последним» (last-in-last-out (LILO)) , если вы используете их правильно.

В данном разделе, мы смешаем потоки и создадим простой скрипт файлового загрузчика, чтобы продемонстрировать, как работает Queues Python со случаями, которые мы хотим паралеллизировать. Чтобы помочь объяснить, как работает Queues, мы перепишем загрузочный скрипт из предыдущей секции для использования Queues. Приступим!

Python

# -*- coding: utf-8 -*-

import os
import threading
import urllib.request
from queue import Queue

class Downloader(threading.Thread):
«»»Потоковый загрузчик файлов»»»

def __init__(self, queue):
«»»Инициализация потока»»»
threading.Thread.__init__(self)
self.queue = queue

def run(self):
«»»Запуск потока»»»
while True:
# Получаем url из очереди
url = self.queue.get()

# Скачиваем файл
self.download_file(url)

# Отправляем сигнал о том, что задача завершена
self.queue.task_done()

def download_file(self, url):
«»»Скачиваем файл»»»
handle = urllib.request.urlopen(url)
fname = os.path.basename(url)

with open(fname, «wb») as f:
while True:
chunk = handle.read(1024)
if not chunk:
break
f.write(chunk)

def main(urls):
«»»
Запускаем программу
«»»
queue = Queue()

# Запускаем потом и очередь
for i in range(5):
t = Downloader(queue)
t.setDaemon(True)
t.start()

# Даем очереди нужные нам ссылки для скачивания
for url in urls:
queue.put(url)

# Ждем завершения работы очереди
queue.join()

if __name__ == «__main__»:
urls = [«http://www.irs.gov/pub/irs-pdf/f1040.pdf»,
«http://www.irs.gov/pub/irs-pdf/f1040a.pdf»,
«http://www.irs.gov/pub/irs-pdf/f1040ez.pdf»,
«http://www.irs.gov/pub/irs-pdf/f1040es.pdf»,
«http://www.irs.gov/pub/irs-pdf/f1040sb.pdf»]

main(urls)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

# -*- coding: utf-8 -*-
 

importos

importthreading

importurllib.request

fromqueueimportQueue

classDownloader(threading.Thread)

«»»Потоковый загрузчик файлов»»»

def__init__(self,queue)

«»»Инициализация потока»»»

threading.Thread.__init__(self)

self.queue=queue

defrun(self)

«»»Запуск потока»»»

whileTrue

# Получаем url из очереди

url=self.queue.get()

# Скачиваем файл

self.download_file(url)

# Отправляем сигнал о том, что задача завершена

self.queue.task_done()

defdownload_file(self,url)

«»»Скачиваем файл»»»

handle=urllib.request.urlopen(url)

fname=os.path.basename(url)

withopen(fname,»wb»)asf

whileTrue

chunk=handle.read(1024)

ifnotchunk

break

f.write(chunk)

defmain(urls)

«»»

    Запускаем программу
    «»»

queue=Queue()

# Запускаем потом и очередь

foriinrange(5)

t=Downloader(queue)

t.setDaemon(True)

t.start()

# Даем очереди нужные нам ссылки для скачивания

forurl inurls

queue.put(url)

# Ждем завершения работы очереди

queue.join()

if__name__==»__main__»

urls=»http://www.irs.gov/pub/irs-pdf/f1040.pdf»,

«http://www.irs.gov/pub/irs-pdf/f1040a.pdf»,

«http://www.irs.gov/pub/irs-pdf/f1040ez.pdf»,

«http://www.irs.gov/pub/irs-pdf/f1040es.pdf»,

«http://www.irs.gov/pub/irs-pdf/f1040sb.pdf»

main(urls)

Давайте притормозим. В первую очередь, нам нужно взглянуть на определение главной функции для того, чтобы увидеть, как все протекает. Здесь мы видим, что она принимает список url адресов. Далее, функция main создаете экземпляр очереди, которая передана пяти демонизированным потокам. Основная разница между демонизированным и недемонизированным потоком в том, что вам нужно отслеживать недемонизированные потоки и закрывать их вручную, в то время как поток «демон» нужно только запустить и забыть о нем. Когда ваше приложение закроется, закроется и поток. Далее мы загрузили очередь (при помощи метода put) вместе с переданными url. Наконец, мы указываем очереди подождать, пока потоки выполнят свои процессы через метод join. В классе download у нас есть строчка self.queue.get(), которая выполняет функцию блока, пока очередь делает что-либо для возврата. Это значит, что потоки скромно будут дожидаться своей очереди. Также это значит, чтобы поток получал что-нибудь из очереди, он должен вызывать метод очереди под названием get. Таким образом, добавляя что-нибудь в очередь, пул потоков, поднимет или возьмет эти объекты и обработает их. Это также известно как dequeing. После того, как все объекты в очередь обработаны, скрипт заканчивается и закрывается. На моем компьютере были загружены первые 5 документов за секунду.

1 Нововведения в Java 8: Функциональное программирование

Вместе с выходом Java 8 в ней появилась мощная поддержка функционального программирования. Можно даже сказать, долгожданная поддержка функционального программирования. Код стал писаться быстрее, хотя читать его стало сложнее

Перед изучением функционального программирования в Java, рекомендуем хорошо разобраться в трех вещах:

  1. ООП, наследование и интерфейсы (1-2 уровни квеста Java Core).
  2. Дефолтная реализация методов в интерфейсе.
  3. Внутренние и анонимные классы.

Хорошая новость заключается в том, что без знания всего этого можно пользоваться многими возможностями функционального программирования в Java. Плохая новость — понять, как именно все устроено и как все работает, без тех же внутренних анонимных классов уже сложно.

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

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

Превращение в Observable

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

Если вы используете EventHandler’ы, то с помощь из событий можно создать последовательность.

В зависимости от конкретного события, его тип (в данном случае ) сам по себе может нести достаточно информации чтобы стать типом вашего . Однако, очень часто вам может понадобиться что-нибудь другое, например, значение некого поля в момент события. Получить значение такого поля лучше всего внутри хендлера, пока UI поток заблокирован и значения поля актуально. И хотя не существует гарантий, что значение останется неизменным до достижения конечного подписчика, в правильно реализованном Rx коде изменения контролируются на стороне потребителя .

Почему поток пулов?

Работа многих серверных приложений, таких как Web-серверы, серверы базы данных, серверы файлов или почтовые серверы, связана с совершением большого количества коротких задач, поступающих от какого-либо удаленного источника. Запрос прибывает на сервер определенным образом, например, через сетевые протоколы (такие как HTTP, FTP или POP), через очередь JMS, или, возможно, путем опроса базы данных. Независимо от того, как запрос поступает, в серверных приложениях часто бывает, что обработка каждой индивидуальной задачи кратковременна, а количество запросов большое.

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

В дополнение к издержкам создания и разрушения потоков, активные потоки потребляют системные ресурсы. Создание слишком большого количества потоков в одной JVM (виртуальной Java-машине) может привести к нехватке системной памяти или пробуксовке из-за чрезмерного потребления памяти. Для предотвращения пробуксовки ресурсов, серверным приложениям нужны некоторые меры по ограничению количества запросов, обрабатываемых в заданное время.

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

Java Thread Join(). Теория

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

: Этот метод приостановит выполнение текущего потока на указанное время в миллисекундах. Выполнение этого метода зависит от реализации ОС, поэтому Java не гарантирует, что текущий поток будет ждать указанное вами время.

: Этот метод приостановит выполнение текущего потока до тех пор, пока другой поток не закончит свое выполнение на время заданное в миллисекундах плюс наносекундах.

Вот простой пример, показывающий использование метода . Цель программы: убедиться в том, что третий поток начнет работу только тогда, когда первый закончит выполнение.

Java

package ua.com.prologistic;

public class ThreadJoinExample {

public static void main(String[] args) {
Thread t1 = new Thread(new MyRunnable(), «t1»);
Thread t2 = new Thread(new MyRunnable(), «t2»);
Thread t3 = new Thread(new MyRunnable(), «t3»);

t1.start();

//стартуем второй поток только после 2-секундного ожидания первого потока (или когда он умрет/закончит выполнение)
try {
t1.join(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}

t2.start();

//стартуем 3-й поток только после того, как 1 поток закончит свое выполнение
try {
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

t3.start();

//даем всем потокам возможность закончить выполнение перед тем, как программа (главный поток) закончит свое выполнение
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(«Все потоки отработали, завершаем программу»);
}

}

class MyRunnable implements Runnable{

@Override
public void run() {
System.out.println(«Поток начал работу:::» + Thread.currentThread().getName());
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(«Поток отработал:::» + Thread.currentThread().getName());
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

packageua.com.prologistic;

publicclassThreadJoinExample{

publicstaticvoidmain(Stringargs){

Thread t1=newThread(newMyRunnable(),»t1″);

Thread t2=newThread(newMyRunnable(),»t2″);

Thread t3=newThread(newMyRunnable(),»t3″);

t1.start();

//стартуем второй поток только после 2-секундного ожидания первого потока (или когда он умрет/закончит выполнение)

try{

t1.join(2000);

}catch(InterruptedExceptione){

e.printStackTrace();

}

t2.start();

//стартуем 3-й поток только после того, как 1 поток закончит свое выполнение

try{

t1.join();

}catch(InterruptedExceptione){

e.printStackTrace();

}

t3.start();

//даем всем потокам возможность закончить выполнение перед тем, как программа (главный поток) закончит свое выполнение

try{

t1.join();

t2.join();

t3.join();

}catch(InterruptedExceptione){

e.printStackTrace();

}

System.out.println(«Все потоки отработали, завершаем программу»);

}

}

classMyRunnableimplementsRunnable{

@Override

publicvoidrun(){

System.out.println(«Поток начал работу:::»+Thread.currentThread().getName());

try{

Thread.sleep(4000);

}catch(InterruptedExceptione){

e.printStackTrace();

}

System.out.println(«Поток отработал:::»+Thread.currentThread().getName());

}

}

Результат выполнения программы:

Java

Поток начал работу:::t1
Поток начал работу:::t2
Поток отработал:::t1
Поток начал работу:::t3
Поток отработал:::t2
Поток отработал:::t3
Все потоки отработали, завершаем программу

1
2
3
4
5
6
7

Потокначалработу::t1

Потокначалработу::t2

Потокотработал::t1

Потокначалработу::t3

Потокотработал::t2

Потокотработал::t3

Всепотокиотработали,завершаемпрограмму

Следите за обновлениями раздела Многопоточность и параллелизм в Java

Дополнительные материалы

Чтение

  • Блог Алексея Шипилёва — знаю, что очевидно, но просто грех не упомянуть
  • Блог Черемина Руслана — последнее время не пишет активно, нужно искать в блоге его старые записи, поверьте это стоит того — там кладезь
  • Хабр Глеба Смирнова — есть отличные статьи про многопоточность и модель памяти
  • Блог Романа Елизарова — заброшен, но археологические раскопки провести нужно. В целом Роман очень много сделал для просветления народа в области теории многопоточного программирования, ищите его в медиа.

Подкасты

  • SDCast #62: в гостях Александр Титов и Амир Аюпов, инженеры из Intel и Алексей Маркин, программист из МЦСТ
  • SDCast #63: в гостях Алексей Маркин, программист из МЦСТ
  • Разбор Полетов: #107 Истории альпинистов
  • Разбор Полетов: #154 Кишочки — Атака на Новый Год

Видео

  • Computer Science Center — Лекция 11. Модели памяти и проблемы видимости
  • Теория и практика многопоточного программирования

Syntax of sleep() method in Java:

public static void sleep(long milliseconds) throws InterruptedException
public static void sleep(long milliseconds, int nanoseconds) throws InterruptedException

The sleep() method can throw an exception named InterruptedException when interrupted in a program. Therefore, using sleep() method needs either we throw InterruptedException to the caller or the call to sleep() method must be enclosed in Java try-catch block otherwise, the program will not complete.

try
{
  Thread.sleep(1000); // sleeps a thread for at least 1000 milliseconds (1 sec).
}
catch(InterruptedException ie)
{
   // catch handler.
}

Конкуренция и параллелизм

найдешь

  • Конкуренция — это способ одновременного решения множества задач
  • Параллелизм — это способ выполнения разных частей одной задачи

тут

  • Наличие нескольких потоков управления (например Thread в Java, корутина в Kotlin), если поток управления один, то конкурентного выполнения быть не может
  • Недетерминированный результат выполнения. Результат зависит от случайных событий, реализации и того как была проведена синхронизация. Даже если каждый поток полностью детерминированный, итоговый результат будет недетерминированным
  • Необязательно имеет несколько потоков управления
  • Может приводить к детерминированному результату, так например, результат умножения каждого элемента массива на число, не изменится, если умножать его по частям параллельно
  • битов (например в 32-разрядных машинах сложение происходит в одно действие, параллельно обрабатывая все 4 байта 32-разрядного числа)
  • инструкций (на одном ядре, в одном потоке процессор может выполнять инструкции параллельно, несмотря на то что код последовательный)
  • данных (существуют архитектуры с параллельной обработкой данных (Single Instruction Multiple Data), способные выполнять одну инструкцию на большом наборе данных)
  • задач (подразумевается наличие нескольких процессоров или ядер)

параллельного

Класс Thread

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Thread

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

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

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

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

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

НазадВперед

1 Потоки данных

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

Все эти вещи мы можем назвать одним словом — процесс обмена данными между программой и внешним миром. Хотя это уже не одно слово.

Сам процесс обмена данными можно разделить на два типа: получение данных и отправка данных. Например, вы считываете данные с клавиатуры с помощью объекта — это получение данных. И выводите данные на экран с помощью команды — это отправка данных.

Для описания процесса обмена данными в программировании используется термин поток. Откуда вообще взялось такое название?

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

Потоки — это универсальный инструмент. Они позволяют программе получать данные откуда угодно (входящие потоки) и отправляют данные куда угодно (исходящие потоки). Делятся на два вида:

  • Входящий поток (Input): используется для получения данных
  • Исходящий поток (Output): используется для отправки данных

Чтобы потоки можно было «потрогать руками», разработчики Java написали два класса: и .

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

Байтовые потоки

Что же это за данные и в каком виде их можно читать? Другими словами, какие типы данных поддерживаются этими классами?

О, это универсальные классы, и поэтому они поддерживают самый распространённый тип данных — . В можно записывать байты (и массивы байт), а из объекта можно читать байты (или массивы байт). Все — никакие другие типы данных они не поддерживают.

Поэтому такие потоки еще называют байтовыми потоками.

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

Именно так работает чтение с клавиатуры через класс : вы читаете данные с клавиатуры последовательно: строка за строкой. Прочитали строку, прочитали следующую строку, прочитали следующую строку и т.д. Поэтому метод чтения строки и называется (дословно — «следующая срока»).

Запись данных в поток тоже происходит последовательно. Хороший пример — вывод на экран. Вы выводите строку, за ней еще одну и еще одну. Это последовательный вывод. Вы не можете вывести 1-ю строку, затем 10-ю, а затем вторую. Все данные записываются в поток вывода только последовательно.

Символьные потоки

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

Java-программисты учли этот факт и написали еще два класса: и . Класс — это аналог класса , только его метод читает не байты, а символы — . Класс соответствует классу , и так же, как и класс , работает с символами (), а не байтами.

Если сравнить эти четыре класса, мы получим такую картину:

Байты (byte) Символы (char)
Чтение данных
Запись данных

Практическое применение

Сами классы , , и в явном виде никто не использует: они не присоединены ни к каким внешним объектам, из которых можно читать данные (или в которые можно писать данные). Однако у этих четырех классов много классов-наследников, которые умеют очень многое.

Runnable vs Thread

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

Реализация Runnable является более предпочтительной, поскольку Java поддерживает реализацию нескольких интерфейсов. Если вы наследуете класс Thread, то вы уже не можете наследовать другие классы.

Интересно знать

Начиная с Java 8, Runnable представляет собой функциональный интерфейс и мы можем использовать лямбда-выражения для его реализации, вместо анонимного класса. Следите за обновлениями сайта и вы увидите полное руководство по лямбда выражениям в Java!

Ошибки при использовании классического подхода

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

Во-первых, часто вместо вызова метода start для запуска потока программист сразу вызывает метод run, что приводит к неправильному результату. Если сразу вызвать метод run на строке 8 в листинге 1 или листинге 2, то программа отработает без всяких видимых изменений. Ошибка заключается в том, что при непосредственном вызове метода run задача будет выполняться, но в однопоточном, а не в многопоточном режиме. Поэтому, если такая задача всего одна, программа отработает нормально, хотя и несколько медленнее (но «невооруженным» глазом это будет незаметно), а вот в случае с несколькими задачами падение производительности окажется фатальным.

Другая проблема связана с самим использованием наследования класса Thread вместо реализации интерфейса Runnable. Если при реализации интерфейса требуется обязательное соблюдение сигнатуры при переопределении метода (в данном случае метода run), то в наследовании такого ограничения не существует. Поэтому ошибка в сигнатуре метода run в листинге 1 автоматически меняет состояние этого метода с «переопределенный» на «перегруженный», при этом при компиляции не будет выведено никаких предупреждений или сообщений об ошибках. Однако запуск подобной программы опять приведет к возникновению непредусмотренного результата, а точнее, полному отсутствию такового. Это будет связано с тем, что при отсутствии переопределенной версии метода run будет вызвана реализация этого метода по умолчанию, которая расположена в классе Thread. Эта реализация по умолчанию не содержит никакой функциональности, соответственно поток запустится и тут же остановится, так как никакой работы для него нет.

Для решения этой проблемы на помощь приходит одна из возможностей, появившихся в JSE 5, — аннотация Override, как показано на строке 8 в листинге 1. Эта аннотация заставляет компилятор выполнить проверку, действительно ли объявленный метод переопределяет какой-нибудь из методов суперкласса. В случае, если метод, отмеченный этой аннотацией, не является переопределением метода из суперкласса, то компилятор выводит сообщение об ошибке. Вообще, аннотацию Override рекомендуется применять во всех случаях, когда выполняется переопределение методов, так как это повышает качество разрабатываемого кода.

Выбор между интерфейсом java.lang.Runnable и классом java.lang.Thread

Как было показано ранее, при необходимости обеспечить параллельное выполнение нескольких задач у программиста есть возможность выбрать, как именно реализовать эти задачи: с помощью класса Thread или интерфейса Runnable. У каждого подхода есть свои преимущества и недостатки.

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

Использование интерфейса Runnable по умолчанию лишено этого недостатка, но если реализовать задачу таким способом, то придется потратить дополнительные усилия на ее запуск. Как было показано в листинге 2, для запуска Runnable-задачи все равно потребуется объект Thread, также в этом случае исчезнет возможность прямого управления потоком из задачи. Хотя последнее ограничение можно обойти с помощью статических методов класса Thread (например, метод currentThread() возвращает ссылку на текущий поток).

Поэтому сделать однозначный вывод о превосходстве какого-либо подхода довольно сложно, и чаще всего в приложениях одновременно используются оба варианта, но для решения задач различной направленности. Считается, что наследование класса Thread следует применять только тогда, когда действительно необходимо создать «новый вид потока, который должен дополнить функциональность класса java.lang.Thread», и подобное решение применяется при разработке системного ПО, например, серверов приложений или инфраструктур. Использование интерфейса Runnable показано в случаях, когда просто «необходимо одновременно выполнить несколько задач» и не требуется вносить изменений в сам механизм многопоточности, поэтому в бизнес-ориентированных приложениях в основном используется вариант с интерфейсом Runnable.

Ограничения классического подхода

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

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

Первым, что бросается в глаза, оказывается слияние низкоуровневого кода, отвечающего за многопоточное исполнение, и высокоуровневого кода, отвечающего за основную функциональность приложения (так называемый «спагетти-код»). В листинге 1 показано, что бизнес—код и поточный код вообще находятся в одном классе, но даже в более удачном варианте из листинга 2 для выполнения задачи все равно требуется создать объект Thread и запустить его. Подобное перемешивание снижает качество архитектуры приложения и может затруднить его последующее сопровождение.

Но даже если удалось отделить поточный код от основного, то возникает проблема, связанная уже с управлением самими потоками. Потоки в Java запускаются только путем вызова метода start и останавливаются после вызова соответствующих методов или самостоятельно после завершения работы метода run. Также после того, как поток остановился, его нельзя запустить второй раз, что и приводит к следующим негативным моментам:

  • поток занимает относительно много места в куче, так что после его завершения необходимо проследить, чтобы память, занимаемая им, была освобождена (например, присвоить ссылке на поток значение null);
  • для выполнения новой задачи потребуется запустить новый поток, что приведет к увеличению «накладных расходов» на виртуальную машину, так как запуск потока – это одна из самых требовательных к ресурсам операций.

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

Важно сказать, что «среднестатистический» программист будет отнюдь не первым, кто сталкивается с подобными проблемами в многозадачных приложениях. Все эти проблемы были давно проанализированы Java-сообществом и нашли решение в признанных шаблонах проектирования (например, ThreadPool (пул потоков) и WorkerThread (рабочий поток))

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

Заключение

Пул потока – полезный инструмент для организации серверов приложений. Он довольно простой по сути, но есть некоторые моменты, с которыми следует быть осторожными во время применения и использования, такие как взаимоблокировка, пробуксовка ресурсов, и сложности, связанные с и . Если вам потребуется пул потоков для вашего приложения, рассмотрите использование одного из классов из , такой как , вместо создания нового с нуля. Если вам нужно создать потоки для решения краткосрочных задач, вам определенно следует рассмотреть использование вместо этого пула потоков.

Похожие темы

  • Оригинал статьи: Thread pools and work queues.
  • Даг Ли, Параллельное программирование в Java: принципы дизайна и модели, второе издание — умело написанная книга о проблемных вопросах, связанных с многопоточным программированием в Java-приложениях.
  • Изучите пакет Дага Ли , который содержит множество полезных классов для построения эффективных параллельных приложений.
  • Формируется пакет на основе Java Community Process JSR 166, для включения в версию 1.5 JDK (комплект разработчика для Java).
  • Книга Аллена Холуб Укрощение потоков Java — интересное знакомство с проблемами программирования Java-потоков.
  • Конечно, есть некоторые проблемы в Java Thread API; прочитайте, что сделал бы Аллен Холуб, если бы был королем (developerWorks, октябрь 2000 г.)
  • Алекс Ройтер предлагает некоторые рекомендации для написания классов с многопоточной поддержкой (developerWorks, февраль 2001 г.)
  • Другие ресурсы Java можно найти в разделе технологий Java developerWorks.
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector