AtomicInteger, AtomicLong и т.п. классы позволяют выполнять операции в многопоточной среде атомарно. Например, безопасно увеличивать счетчик.
Рассмотрим пример.
Некорректный код
Пусть есть класс, который увеличивает значение счетчика от 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.