Допустим, есть задача: создать поток, который скачивает что-то из интернета, а из основного потока ему валятся задания. Стандартный путь реализации такой:
class DownloadThread extends Thread {
public interface Listener {
void onImageLoaded(Image image);
}
private String reqUrl_ = null;
private final Listener listener_;
private boolean isFinished_ = false;
DownloadThread(Listener listener) {
listener_ = listener;
}
public void imageRequest(String url) {
synchronized (this) {
// Запрос на загрузку изображения
reqUrl_ = url;
}
}
@Override
public void run() {
while (!isFinished_)
String url = null;
synchronized (this) {
// Проверка: есть ли запрос?
if (null != reqUrl_) {
url = reqUrl_;
reqUrl_ = null;
}
}
if (null != url) {
// Запрос есть, загружаем изображение
Image image = download(url);
listener_.onImageLoaded(image);
}
try {
// Поспим одну милисекунду, чтоб не грузить CPU
sleep(1);
}
catch (InterruptedException e) {
isFinished_ = true;
break;
}
}
}
public void stopThread() {
isFinished_ = true;
}
}
Почему этот плохой, негодный пример? Потому что, когда заданий много, между ними одна миллисекунда бездействия минимум — это во-первых. Во-вторых, если задач для потока нет, то он работает без всякой пользы, создавая лишнюю нагрузку процессору.
Рассмотрим пример номер два:
class DownloadThread extends Thread {
public interface Listener {
void onImageLoaded(Image image);
}
private String reqUrl_ = null;
private final Listener listener_;
private boolean isFinished_ = false;
DownloadThread(Listener listener) {
listener_ = listener;
}
public void imageRequest(String url) {
synchronized (this) {
// Запрос на загрузку изображения
reqUrl_ = url;
// Уведомляем поток, что есть задание
notify();
}
}
@Override
public void run() {
while (!isFinished_)
String url = null;
synchronized (this) {
if (isFinished_) {
break;
}
// Проверка: есть ли запрос?
if (null != reqUrl_) {
url = reqUrl_;
reqUrl_ = null;
}
else {
try {
// Ждем вызова notify
wait();
}
catch (InterruptedException e) {
isFinished_ = true;
break;
}
}
}
if (null != url) {
// Запрос есть, загружаем изображение
Image image = download(url);
listener_.onImageLoaded(image);
}
}
}
public void stopThread() {
synchronized (this) {
isFinished_ = true;
notify();
}
}
}
Почему этот пример лучше первого? Во-первых, когда заданий нет, поток находится в режиме ожидания без вызовов sleep()
. Во-вторых, когда задание приходит, поток реагирует моментально просыпаясь.
Обратите внимание, что метод stopThread()
слегка изменился. Ввиду того, что поток не выйдет из состояния wait()
без вызова notify()
.
UPD: По совету друзей исправил метод run()
для корректного завершения потока.
В обоих примерах не учитывается, что изображения нам нужны пачкой сразу, то есть очередь запросов выкинута для упрощения кода.
P.S. Кстати, в C/C++ для тех же целей можно использовать pthread_cond_wait()
и pthread_cond_signal()
Хотелось бы заметить что второй вариант так же далек от идеала как и первый, тут есть несколько проблем:
1. использование одной переменной для запроса маловато, при частых запросах она будет переписываться, тут лучше заиспользовать очередь, при том блокирующую, например ArrayBlockingQueue
2. В текущей реализации неправильно работает условие выхода, его обязательно надо проверять под синхронайзом перед тем как заходишь в wait
В общем в современное время лучше все делать одним из двух вариантов:
1. Использовать Executors из пакета concurrent
2. Текущий пример переписать на использование ArrayBlockingQueue
Спасибо за столь подробный комментарий.
Действительно, вариант с Executors или ArrayBlockingQueue лучше, тут вы правы. Но я заядлый программист на C/C++ и поэтому стараюсь писать на Java С-style код. Мой косяк, да.
Немного смущает, что во время использования Executors происходит выделение памяти. А я стараюсь избегать создание новых объектов без необходимости. Таким образом, мой код использует меньше памяти, хотя он более громоздкий и сложнее в отладке по сравнению с Executors.
Да, это так, я тоже стараюсь создавать меньше объектов там где это действительно необходимо.
И если это узкое место, то можно исправить ошибки текущего варианта (пункт 2).
Но и преждевременной оптимизацией тоже не стоит заниматься, так как вероятность допустить ошибку сильно возрастает.
Если рассмотреть конкретно этот пример, то накладные расходы на создаваемые объекты в Executors будут невелики, по сравнению с другими участками кода, особенно если используются коллекции, особенно с обертакми над примитивами, где постоянно происходит Boxing, Unboxing.
Все конечно относительно и надо профилировать чтобы сказать точно, но в 99 процентах это так.
PS: заядлые java программисты стараются создавать еще меньше объектов :) — так как зачастую накладыные расходы GC очень велики
В ближайшее время напишу варианты с ArrayBlockingQueue и Executors, чтоб постараться охватить все возможные способы. Хотя заметка немного не про то. :)
Исправил функцию DownloadThread.run(), чтобы корректно завершалась.