Параллельность, видимость объекта

Я пытаюсь выяснить, не страдает ли приведенный ниже код от возможных проблем с параллелизмом. В частности, проблема видимости связана с изменчивыми переменными. Volatile определяется следующим образом: значение этой переменной никогда не будет кэшироваться локально: все операции чтения и записи будут идти прямо в "основную память".

public static void main(String [] args)
{
    Test test = new Test();

    // This will always single threaded
    ExecutorService ex = Executors.newSingleThreadExecutor();

    for (int i=0; i<10; ++i)
        ex.execute(test);
}

private static class Test implements Runnable {
    // non volatile variable in question
    private int state = 0;

    @Override
    public void run() {
        // will we always see updated state value? Will updating state value
        // guarantee future run's see the value?
        if (this.state != -1)
            this.state++;
    }
}

Для вышеуказанного однопоточного исполнителя:

Можно ли сделать test.state энергонезависимым? Другими словами, будет ли каждый последующий Test.run() (который будет выполняться последовательно, а не одновременно, потому что опять-таки executor однопоточный) всегда видеть обновленное значение test.state? Если нет, то не гарантирует ли выход из Test.run() каких-либо изменений, внесенных локально, потоком, записываемым обратно в основную память? Иначе, когда изменения, сделанные потоком, локально записываются обратно в основную память, если нет при выходе из потока?

6 ответов

Решение

Да, это безопасно, даже если исполнитель заменил свою нить посередине. Начало / конец потока также являются точками синхронизации.

http://java.sun.com/docs/books/jls/third_edition/html/memory.html

Простой пример:

static int state;
static public void main(String... args) {
    state = 0;                   // (1)
    Thread t = new Thread() {
        public void run() {
            state = state + 1;   // (2) 
        }
    };
    t.start();
    t.join();
    System.out.println(state);  // (3)
}

Гарантируется, что (1), (2), (3) хорошо упорядочены и ведут себя как ожидалось.

Для однопотокового исполнителя "Задачи гарантированно выполняются последовательно", он должен каким-то образом определить завершение одной задачи перед началом следующей, что обязательно правильно синхронизирует различные задачи. run()"s

Пока это только один поток, нет необходимости делать его изменчивым. Если вы собираетесь использовать несколько потоков, вы должны не только использовать volatile, но и синхронизировать. Увеличение числа не является атомарной операцией - это распространенное заблуждение.

public void run() {
    synchronize (this) {
        if (this.state != -1)
            this.state++;
    }
}

Вместо синхронизации вы также можете использовать AtomicInteger # getAndIncrement () (если вам не нужно, если раньше).

private AtomicInteger state = new AtomicInteger();

public void run() {
    state.getAndIncrement()
}

Первоначально я думал так:

Если бы задача всегда выполнялась одним и тем же потоком, проблем не было бы. Но Excecutor произведено newSingleThreadExecutor() может создавать новые потоки, чтобы заменить те, которые убиты по любой причине. Нет гарантии того, когда будет создан замещающий поток или какой поток его создаст.

Если поток выполняет некоторые записи, то вызывает start() в новом потоке эти записи будут видны новому потоку. Но нет никакой гарантии, что это правило применимо в этом случае.

Но неоспоримый прав: создание правильного ExecutorService Без достаточных барьеров обеспечить видимость практически невозможно. Я забыл, что обнаружение смерти другого потока связано с синхронизацией. Механизм блокировки, используемый для простоя рабочих потоков, также требует барьера.

Ваш код, особенно этот бит

            if (this.state != -1)
                    this.state++;

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

Но ваш дизайн основан на утверждении, что всегда будет только один экземпляр Test, и что этот единственный экземпляр предоставляется только одному (одному) потоку. (Но обратите внимание, что один экземпляр на самом деле является общим состоянием между основным потоком и потоком исполнителя.)

Я думаю, что вы должны сделать эти предположения более явными (например, в коде используйте ThreadLocal и ThreadLocal.get()). Это необходимо для защиты как от будущих ошибок (когда какой-либо другой разработчик может небрежно нарушить предположения проекта), так и от принятия предположений о внутренней реализации используемого вами метода Executor, который в некоторых реализациях может просто обеспечить однопоточный исполнитель (т.е. последовательный и не обязательно один и тот же поток в каждом вызове execute(runnable).

Для состояния вполне нормально быть энергонезависимым в этом конкретном коде, потому что существует только один поток, и только этот поток обращается к полю. Отключение кэширования значения этого поля в единственном потоке, которое у вас есть, просто скажется на производительности.

Однако если вы хотите использовать значение состояния в основном потоке, который выполняет цикл, вы должны сделать поле volatile:

    for (int i=0; i<10; ++i) {
            ex.execute(test);
            System.out.println(test.getState());
    }

Однако даже это может работать неправильно с volatile, потому что между потоками нет синхронизации.

Поскольку поле является закрытым, возникает проблема, только если основной поток выполняет метод, который может получить доступ к этому полю.

Если ваш ExecutorService является однопоточным, то нет общего состояния, поэтому я не вижу, как могут возникнуть какие-либо проблемы вокруг этого.

Однако не будет ли больше смысла передавать новый экземпляр вашего Test класс для каждого звонка execute()? т.е.

for (int i=0; i<10; ++i)
    ex.execute(new Test());

Таким образом, не будет никакого общего состояния.

Другие вопросы по тегам