December
2008
Бывает, что иногда почитываю разные интересные книжки.
Вот например, для собственного развития всякие JSR читаю :).
Из недавнего - просматривал JSR-133 Java Memory Model and Thread Specification (Модель памяти Java).
В данной статье хочу в который раз показать, насколько важна синхронизация потоков, на примере такого понятия как атомарность (Atomicity) операций.
Рассмотрим такой программный код:
public class Atomicity extends Thread {
volatile static int i;
boolean b;
public void run() {
while (true) {
if (b = !b) i++;
else i--;
}
}
public static void main(String[] args) {
// new Atomicity().start();
new Atomicity().start();
while (true)
System.out.println(i);
}
}
При запуске на экран будут выводится числа 0 и 1.
Естественно, так как значение переменной i в потоке попеременно инкрементируется и декрементируется.
Можно сделать предположение, что если раскомментировать первую строку метода main, то значение i будет принимать максимум 4 разных значения.
На самом деле в System.out будет выводится что-то типа:
... 472 ... 97472 ... 115920 ...
Так в чем же дело?
А все дело в том, что операции инкремента и декремента не являются атомарными.
Происходит считывание, увеличение/уменшение и далее запись значения.
Исправить ошибку в коде помогает synchronized блок:
public void run() {
while (true) {
synchronized (Atomicity.class) {
if (b = !b) i++;
else i--;
}
}
}
Синхронизация по классу Atomicity говорит о том, что в один момент времени в synchronized блоке может находится не больше одного потока.
Остальные потоки будут ждать, для того чтобы захватить монитор класса Atomicity. При чем случится это только после того, как активный поток отпустит этот монитор.
Атомарность говорит о том, что некоторое действие (или их последовательность) должно происходить “все и сразу”.
Осутствие синхронизации может привести к катострофическим последствиям. Это далеко не NullPointerException, который можно обнаружить сразу.
Программа может работать достаточно долго и визаульно никаких неполадок обнаружено не будет.
При написании многопоточных приложений необходимо аккуратно следить за всеми возможными случаями проявления ошибок неатомарности.
В Java Memory Model рассказано много еще чего интересного, например о видимости (Visibility) и упорядоченности (Ordering).
Но это уже совсем другая история.
Надеюсь, статья была вам интересна. В любом случае, жду ваших комментариев.
7 Comments »
RSS feed for comments on this post.
А разве этот пример доказывает неатомарность операции инкремента, ведь в программном фрагменте присутствует еще и условный переход? Это я к тому написал, что, понятно, в машинном коде операция инкремента будет состоять из нескольких инструкций, но насколько я помню джава все-таки гарантирует атомарность таких операций.
Атаморность поля b не имеет смысла в данном контексте, так как это поле экземпляра класса и у каждого потока есть свое собственное значение этого поля.
Как раз Java и не гарантирует атомрность инкремента, что собственно и показывает данный пример.
в приведённом примере вообще не гарантируется, что значение свойства i будет в потоке 2 будет такое же, какое оно будет записано в потоке 1. Опять же согласно Java Memory Model. Чтобы избежать этот нюанс св-во i должно иметь модификатор volatile, но даже это не решит вопрос атомарности инкремента/дикремента
синхронизация через synchronized для соблюдения атомарности подобных операций - overhead, не мало обзоров на тему производительности - чтобы не потерять производительность, и в то же время иметь атомарность операций примитивов есть атомарные классы обёртки, н-р. java.util.concurrent.atomic.AtomicInteger.
Да, спасибо, забыл volatile поставить, чтобы гарантировать, что все потоки будут видеть одно и то же значение переменной i. Здесь имеет место понятие видимость (Visibility).
Ключевое слово volatile говорит (в частности интерпретатору байт-кода) о том, что не нужно использовать регистровые оптимизации для этой переменной.
Да, согласен, не обратил внимания на характер b.
На http://java.sun.com/docs/books/tutorial/essential/concurrency/atomic.html есть такие строчки
”
* Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).
* Reads and writes are atomic for all variables declared volatile (including long and double variables).
”
- volatile для типа int не нужен, хотя, от него и хуже наверное не будет.
А использование обёртки java.util.concurrent.atomic.AtomicInteger, как заметил Vladimir Dolzhenko, быда бы кстати.
Но цель автора была, на сколько я понял, показать неатомарность операций “–” и “++”, поэтому и synchronized подойдёт ![]()
@c0nst
Ключевое слово volatile говорит (в частности интерпретатору байт-кода) о том, что не нужно использовать регистровые оптимизации для этой переменной.
да, но_ так было и в 1.3, и в 1.4. И только в 1.5 JMM даёт гарантию что volatile не будет “reorder-иться” с другими переменными на “multithread-овых” участках.
class A {
private int i;
private volatile j;
void a() {
i=1;
j=2;
}
}
JMM-ское правило относительно volatile гарантирует что i выполнится перед j в любом thread-е.