Допустим, есть задача: создать поток, который скачивает что-то из интернета, а из основного потока ему валятся задания. Стандартный путь реализации такой:
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(), чтобы корректно завершалась.