Использование логической переменной для остановки потоков

У меня есть книга по Java, по которой я изучаю, и в одном из примеров я увидел что-то подозрительное.

public class ThreadExample extends MIDlet {
    boolean threadsRunning = true; // Flag stopping the threads

    ThreadTest thr1;
    ThreadTest thr2;

    private class ThreadTest extends Thread {
        int loops;

        public ThreadTest(int waitingTime) {
            loops = waitTime;
        }

        public void run() {
            for (int i = 1; i <= loops; i++) {
                if (threadsRunning != true) { // here threadsRunning is tested
                    return;
                }

                try {
                    Thread.sleep(1000);
                } catch(InterruptedException e) {
                    System.out.println(e);
                }
            }
        }
    }

    public ThreadExample() {
        thr1 = new ThreadTest(2);
        thr2 = new ThreadTest(6);
    }

    public void startApp() throws MIDletStateChangeException {
        thr1.start();
        thr2.start();

        try {
            Thread.sleep(4000); // we wait 4 secs before stopping the threads - 
                                // this way one of the threads is supposed to finish by itself
        } catch(InterruptedException e) {
            System.out.println(e);
        }

        destroyApp();
    }

    public void destroyApp() {    
        threadsRunning = false;

        try {
            thr1.join();
            thr2.join();
        } catch(InterruptedException e) {
            System.out.println(e);
        }

        notifyDestroyed();
    }
}

Поскольку это приложение MIDlet, при его запуске выполняется метод startApp. Для простоты сам метод startApp вызывает destroyApp, поэтому программа уничтожается, останавливая потоки и уведомляя об уничтожении.

Вопрос в том, безопасно ли использовать эту переменную threadsRunning и не вызовет ли ее использование внутри обоих потоков и в методе destroyApp какие-либо проблемы в какой-то момент? Поможет ли ключевое слово volatile перед объявлением синхронизировать его?


person closer    schedule 09.11.2011    source источник
comment
Кроме того, пока вы только что прочитали эту переменную внутри потоков, ее следует сохранить.   -  person Matthias Bruns    schedule 09.11.2011
comment
Спасибо. Итак, только если я пишу в переменную более чем в один поток, это опасно без использования механизма синхронизации, верно?   -  person closer    schedule 09.11.2011
comment
Есть случаи, когда запись в переменную из более чем одного потока безопасна, но в большинстве случаев это не так. Также бывают случаи, когда запись в переменную только из одного потока и чтение ее из других безопасны, и случаи, когда это не так. Это становится настолько сложным, что я обычно просто синхронизирую доступ к любой общей переменной.   -  person Joe Daley    schedule 09.11.2011
comment
просто примечание: если поток получает InterruptedException, он должен остановиться, то есть, по крайней мере, выйти из цикла в приведенном выше коде.   -  person user85421    schedule 09.11.2011


Ответы (1)


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

Однако переменная должна быть хотя бы помечена как volatile.

Пометка переменной как volatile не синхронизирует доступ к ней потоков; он гарантирует, что поток не пропустит обновление переменной другим потоком из-за оптимизации кода или кэширования значений. Например, без volatile код внутри run() может прочитать значение threadsRunning только один раз в начале, кэшировать значение, а затем каждый раз использовать это кэшированное значение в операторе if вместо повторного чтения переменной из основной памяти. Если значение threadsRunning будет изменено другим потоком, оно может не получиться.

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

person Joe Daley    schedule 09.11.2011
comment
Обновил свой ответ, потому что передумал. Хотя в этом конкретном примере это может не понадобиться по техническим причинам, при таком использовании переменной она должна быть помечена как volatile. - person Joe Daley; 09.11.2011
comment
Я удалил часть о барьере памяти, потому что JLS 17.9 говорит следующее: It is important to note that neither Thread.sleep nor Thread.yield have any synchronization semantics. In particular, the compiler does not have to flush writes cached in registers out to shared memory before a call to Thread.sleep or Thread.yield, nor does the compiler have to reload values cached in registers after a call to Thread.sleep or Thread.yield. - person Voo; 09.11.2011
comment
@Voo Хорошо! Я перепроверил свой источник, в котором говорилось, что Thread.sleep был барьером памяти, и речь шла о .NET, а не о Java. Спасибо за исправление. - person Joe Daley; 09.11.2011
comment
Дополнительный вопрос @JoeDaley. Что, если внутри run() будет создан новый runnable. как этот новый поток может прочитать threadsRunning, если runnable не является вложенным классом. Могу ли я просто передать threadsRunning в конструктор нового runnable? Я вижу, что это работает, но целесообразно ли это? - person Erik; 27.11.2011