Почему Thread.sleep() — это прошлый век

Допустим, есть задача: создать поток, который скачивает что-то из интернета, а из основного потока ему валятся задания. Стандартный путь реализации такой:

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

4 комментария
  1. написал(а) Андрей (18 сентября 2013, 12:11)

    Хотелось бы заметить что второй вариант так же далек от идеала как и первый, тут есть несколько проблем:
    1. использование одной переменной для запроса маловато, при частых запросах она будет переписываться, тут лучше заиспользовать очередь, при том блокирующую, например ArrayBlockingQueue
    2. В текущей реализации неправильно работает условие выхода, его обязательно надо проверять под синхронайзом перед тем как заходишь в wait

    В общем в современное время лучше все делать одним из двух вариантов:
    1. Использовать Executors из пакета concurrent
    2. Текущий пример переписать на использование ArrayBlockingQueue

    1. написал(а) eJ (18 сентября 2013, 12:39)

      Спасибо за столь подробный комментарий.

      Действительно, вариант с Executors или ArrayBlockingQueue лучше, тут вы правы. Но я заядлый программист на C/C++ и поэтому стараюсь писать на Java С-style код. Мой косяк, да.

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

      1. написал(а) Аноним (18 сентября 2013, 14:46)

        Да, это так, я тоже стараюсь создавать меньше объектов там где это действительно необходимо.
        И если это узкое место, то можно исправить ошибки текущего варианта (пункт 2).

        Но и преждевременной оптимизацией тоже не стоит заниматься, так как вероятность допустить ошибку сильно возрастает.

        Если рассмотреть конкретно этот пример, то накладные расходы на создаваемые объекты в Executors будут невелики, по сравнению с другими участками кода, особенно если используются коллекции, особенно с обертакми над примитивами, где постоянно происходит Boxing, Unboxing.

        Все конечно относительно и надо профилировать чтобы сказать точно, но в 99 процентах это так.

        PS: заядлые java программисты стараются создавать еще меньше объектов :) — так как зачастую накладыные расходы GC очень велики

        1. написал(а) eJ (18 сентября 2013, 22:43)

          В ближайшее время напишу варианты с ArrayBlockingQueue и Executors, чтоб постараться охватить все возможные способы. Хотя заметка немного не про то. :)

          Исправил функцию DownloadThread.run(), чтобы корректно завершалась.

Добавить комментарий

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

Блог Евгения Жирнова