Atomic classes

AtomicIntegerAtomicLong и т.п. классы позволяют выполнять операции в многопоточной среде атомарно. Например, безопасно увеличивать счетчик.

Рассмотрим пример.

Некорректный код

Пусть есть класс, который увеличивает значение счетчика от 0 до 500:

public class AtomicTest implements Runnable {
    Integer counter = 0;


    public void run() {
        for (int i = 0; i < 500; i++) {
            counter++;
        }
    }
}

Мы хотим запустить его в двух потоках:

public class AtomicTest implements Runnable {
    Integer counter = 0;


    public void run() {
        for (int i = 0; i < 500; i++) {
            counter++;
        }
    }


    public static void main(String[] args) throws InterruptedException {

        AtomicTest atomicTest = new AtomicTest();
        Thread thread1 = new Thread(atomicTest);
        Thread thread2 = new Thread(atomicTest);

        thread1.start();
        thread2.start();
        //ждем секунду, чтобы дождаться завершения потоков, а потом напечатать результат 
        Thread.sleep(1000);

        System.out.println(atomicTest.counter);
      }
}

Казалось бы, один поток увеличивает счетчик 500 раз, второй еще 500 раз, а значит результат должен быть 1000. Но нет.

Результат выполнения вышеприведенной программы непредсказуем. Он всегда меньше 1000, у меня выводятся такие значения при нескольких запусках:

685
611
849

Объясняется это тем, что count++ не является атомарной операцией. Она состоит из чтения, увеличения на 1 и записи. В псевдокоде это можно представить так:

int temp=counter; //1. чтение
counter = temp + 1;    // 2. добавление единицы 3. запись

И поскольку  count++ не синхронизирован, ничто не запрещает войти в этот участок двум потока одновременно. Они могут считать одно и то же значение,  а потом добавить к нему 1. Например, оба потока могут сначала считать значение 0, а потом увеличить его на 1, так что итоговым  результатом будет 1, а не 2.

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

Вариант c AtomicInteger

Чтобы сделать операцию атомарной, обернем счетчик counter в AtomicInteger:

public class AtomicTest implements Runnable {
    AtomicInteger counter = new AtomicInteger(0);


    public void run() {
        for (int i = 0; i < 500; i++) {
            counter.getAndIncrement();
            System.out.println(counter);
        }
    }


    public static void main(String[] args) throws InterruptedException {

        AtomicTest atomicTest = new AtomicTest();
        Thread thread1 = new Thread(atomicTest);
        Thread thread2 = new Thread(atomicTest);

        thread1.start();
        thread2.start();

        Thread.sleep(1000);

        System.out.println(atomicTest.counter);
        
    }
}

Обратите внимание, что увеличение счетчика делается с помощью метода:

counter.getAndIncrement();

Он имеет ту же функциональность, что и синхронизация:

synchronized (this) { 
    counter++; 
}

То есть все потоки по очереди увеличивают значение, два потока одновременно в этот участок не зайдут.

Теперь результат верный, 1000.

Но если добавить в цикл System.out.println(counter) можно заметить, что числа выводятся не по порядку (хоть итоговый результат и верный):

...
932
899
933
935
936
934
938
...
1000

Это происходит потому, что System.out.println(counter) уже не входит в синхронизированный участок кода. Синхронизируется только counter++.

Чтобы сделать вывод чисел строго последовательным, придется применить synchronized.

Вариант с synchronized

Здесь как увеличение счетчика, так и вывод в консоль чисел находится внутри блока synchronized:

public class AtomicTest implements Runnable {
    Integer counter = 0;


    public void run() {
        for (int i = 0; i < 500; i++) {
            synchronized (this) {
                counter++;
                System.out.println(counter);
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {

        AtomicTest atomicTest = new AtomicTest();
        Thread thread1 = new Thread(atomicTest);
        Thread thread2 = new Thread(atomicTest);

        thread1.start();
        thread2.start();

        Thread.sleep(1000);

        System.out.println(atomicTest.counter);

    }
}

В результате числа выводятся строго по порядку, результат тоже 1000.

1
2
...

1000

Итоги

Мы рассмотрели AtomicInteger. По функционалу его метод:

counter.getAndIncrement();

равносилен синхронизированному коду:

synchronized (object) { counter++; }

Код примера есть на GitHub.

Добавить комментарий

Ваш адрес email не будет опубликован.