Запуск и остановка фоновых потоков в Java

Written by elwood

Потокобезопасный вариант остановки потока по требованию. Паттерн, который мы повсеместно применяли в .net. В java-мире аналогичное поведение можно более просто реализовать с помощью механизма interruptions, но этот способ более гибок, поскольку во-первых не зависит от тонкостей обработки InterruptException (а это довольно хитрая штука), а во-вторых, более расширяем – например, по сигналу в condition object можно не только завершать выполнение потока, но и передавать какие-то события внутрь потока – например, некоторое действие нужно выполнять либо по таймеру, либо по сигналу. И мы внутри функции потока всегда будем делать нужные операции сразу же по мере необходимости. В отличие от традиционного механизма wait-notify (которое не дает инфы о причине завершения ожидания – таймауте или внешнем сигнале), в этом API мы всегда увидим, когда прошел таймаут. Мы не всегда сможем определить, был ли вызван signal() из-за одного пограничного случая (если и эта информация нужна, то можно её получить просто добавив флаг), но факт прохождения таймаута мы будем знать достоверно.

    private ReentrantLock reentrantLock = new ReentrantLock(  );
    private Condition condition= reentrantLock.newCondition();
    private volatile boolean running = false;
    private static final long PERIOD_MILLISECONDS = 5000;
 
    private Thread calculationThread = new Thread( new Runnable() {
        public void run() {
            log.info( "Calculation thread started." );
            for (;;) {
 
                // todo : здесь собственно идёт полезная работа потока
 
                reentrantLock.lock();
                try {
                    boolean awaitResult = false;
 
                    // в этом месте мы проверяем running для ситуации, когда stopThread()
                    // был вызван в момент, когда этот поток не находился в ожидании, и вызов
                    // signal() не привел ни к чему
                    if ( !running )
                        break;
                    try {
                        // При вызове await() занятая нами блокировка будет освобождена
                        // Но при возврате управления метод await() снова должен будет взять блокировку
                        // Интересный механизм, позволяющий нам точно знать, в какой последовательности
                        // будут выполнены инструкции после вызова signal() и собственно пробуждение потока
                        awaitResult = condition.await(PERIOD_MILLISECONDS, TimeUnit.MILLISECONDS );
                    } catch ( InterruptedException e ) {
                        log.error( "Calculation thread interrupted", e );
                        break;
                    }
                    // running проверять обязательно, поскольку awaitResult может быть false
                    // даже в случае вызова signal (если к этому времени подошел таймаут) - проверено
                    // Это и есть тот самый пограничный случай
                    if (awaitResult || !running )
                        break;
                } finally {
                    reentrantLock.unlock();
                }
            }
            log.info( "Calculation thread stopped." );
        }
    } );
 
    private void startCalculationThread() {
        Assert.assertTrue( !running );
        log.info( "Starting calculation thread.." );
        running = true;
        calculationThread.start();
    }
 
    private void stopCalculationThread() {
        Assert.assertTrue( running );
        log.info( "Stopping calculation thread.." );
        reentrantLock.lock();
        try {
            running = false;
            // Посылаем сигнал в наш condition object
            // Если в это время поток ждет на вызове await(), то он возобновит выполнение
            // НО только после того, как await() получит блокировку
            // Таким образом, сначала будет выполнен весь код после вызова signal() - 
            // в нашем случае это запись в лог сообщения, и только потом await() получит блокировку и вернёт управление
            condition.signal();
            log.info("Calculation thread signalled to stop." );
        } finally {
            reentrantLock.unlock();
        }
        // ожидаем завершения потока
        calculationThread.join();
    }