Итак, если эти три инструкции выполняются десятью потоками, существует 4.38679733629e+24 возможных путей выполнения. Так как в данном случае возможен только один результат, различия в порядке выполнения несущественны. Так уж вышло, что одинаковый результат гарантирован в этой ситуации и для long. Почему? Потому что все десять потоков присваивают одну и ту же константу. Даже если их выполнение будет чередоваться, результат не изменится.
Но с операцией ++ в методе getNextId возникают проблемы. Допустим, в начале метода поле lastId содержит значение 42. Байт-код нового метода выглядит так.
Мнемоника | Описание | Состояние стека операндов после выполнения |
---|---|---|
ALOAD 0 | Загрузка this в стек операндов | this |
DUP | Копирование вершины стека. Теперь в стеке операндов хранятся две копии this | this, this |
GETFIELD lastID | Загрузка значения поля lastId объекта, ссылка на который хранится в вершине стека (this), и занесение загруженного значения в стек | this, 42 |
ICONST_1 | Занесение константы 1 в стек | this, 42, 1 |
IADD | Целочисленное сложение двух верхних значений в стеке операндов. Результат сложения также сохраняется в стеке операндов | this, 43 |
DUP_X1 | Копирование значения 43 и сохранение копии в стеке перед this | 43, this, 43 |
PUTFIELD value | Сохранение верхнего значения из стека (43) в поле value текущего объекта, который задается ссылкой, хранящейся на один элемент ниже вершины стека (this) | 43 |
IRETURN | Возвращение верхнего (и единственного) элемента стека | <пусто> |
Представьте, что первый поток выполняет первые три инструкции (до GETFIELD включительно), а потом прерывается. Второй поток получает управление и выполняет весь метод, увеличивая lastId на 1; он получает значение 43. Затем первый поток продолжает работу с того места, на котором она была прервана; значение 42 все еще хранится в стеке операндов, потому что поле lastId в момент выполнения GETFIELD содержало именно это число. Поток увеличивает его на 1, снова получает 43 и сохраняет результат. В итоге первый поток также получает значение 43. В результате одно из двух увеличений теряется, так как первый поток «перекрыл» результат второго потока (после того как второй поток прервал выполнение первого потока).
Проблема решается объявлением метода getNexId() с ключевым словом synchronized.
Заключение
Чтобы понять, как потоки могут «перебегать дорогу» друг другу, не обязательно разбираться во всех тонкостях байт-кода. Приведенный пример наглядно показывает, что программные потоки могут вмешиваться в работу друг друга, и этого вполне достаточно.
Впрочем, даже этот тривиальный пример убеждает в необходимости хорошего понимания модели памяти, чтобы вы знали, какие операции безопасны, а какие – нет. Скажем, существует распространенное заблуждение по поводу атомарности оператора ++ (в префиксной или постфиксной форме), тогда как этот оператор атомарным не является. Следовательно, вы должны знать:
• где присутствуют общие объекты/значения;
• какой код может создать проблемы многопоточного чтения/обновления;
• как защититься от возможных проблем многопоточности.
Знайте свои библиотеки
Executor Framework
Как демонстрирует пример ExecutorClientScheduler.java на с. 361, представленная в Java 5 библиотека Executor предоставляет расширенные средства управления выполнением программ с использованием пулов программных потоков. Библиотека реализована в виде класса в пакете
Если вы создаете потоки, не используя пулы,
Инфраструктура Executor создает пул потоков с автоматическим изменением размера и повторным созданием потоков при необходимости. Также поддерживаются