что такое атомарность в java

Введение в атомарные переменные в Java

Узнайте, как использовать атомарные переменные для решения проблем параллелизма.

1. введение

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

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

2. Замки

Давайте взглянем на класс:

В случае однопоточной среды это работает идеально; однако, как только мы разрешаем писать более одного потока, мы начинаем получать противоречивые результаты.

Это происходит из-за простой операции приращения ( counter++ ), которая может выглядеть как атомарная операция, но на самом деле представляет собой комбинацию трех операций: получение значения, приращение и запись обновленного значения обратно.

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

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

Процесс приостановки и последующего возобновления потока очень дорог и влияет на общую эффективность системы.

3. Атомарные операции

Существует раздел исследований, посвященный созданию неблокирующих алгоритмов для параллельных сред. Эти алгоритмы используют низкоуровневые атомарные машинные инструкции, такие как compare-and-swap (CAS), для обеспечения целостности данных.

Типичная операция CAS работает с тремя операндами:

Операция CAS атомарно обновляет значение в M до B, но только в том случае, если существующее значение в M совпадает с A, в противном случае никаких действий не предпринимается.

В обоих случаях возвращается существующее значение в M. Это объединяет три этапа – получение значения, сравнение значения и обновление значения – в одну операцию на уровне машины.

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

Еще одним следствием является то, что логика основной программы становится более сложной. Это связано с тем, что мы должны обрабатывать сценарий, когда операция CAS не удалась. Мы можем повторять его снова и снова, пока он не увенчается успехом, или мы ничего не можем сделать и двигаться дальше в зависимости от варианта использования.

4. Атомарные переменные в Java

Как вы можете видеть, мы повторяем операцию compareAndSet и снова при сбое, так как мы хотим гарантировать, что вызов метода increment всегда увеличивает значение на 1.

5. Заключение

В этом кратком руководстве мы описали альтернативный способ обработки параллелизма, при котором можно избежать недостатков, связанных с блокировкой. Мы также рассмотрели основные методы, предоставляемые классами атомарных переменных в Java.

Источник

Атомарные и неатомарные операции (java)

Как понять, какие операции являются атомарными, а какие неатомарными?

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

Один шаг == одной машинной операции? Или чему-то другому? Как определить точно, какие операции относятся к атомарным, а какие к неатомарным?

P.S.: Я нашла похожий вопрос, но там речь идёт о C#.

что такое атомарность в java. Смотреть фото что такое атомарность в java. Смотреть картинку что такое атомарность в java. Картинка про что такое атомарность в java. Фото что такое атомарность в java

3 ответа 3

Как можно определить атомарность?

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

Почему это важно?

Атомарность зачастую проистекает из бизнес-требований приложений: банковские транзакции должны применяться целиком, билеты на концерты заказываться сразу в том количестве, в котором были указаны, и т.д. Конкретно в том контексте, который разбирается (многопоточность в java), задачи более примитивны, но произрастают из тех же требований: например, если пишется веб-приложение, то разбирающий HTTP-запросы сервер должен иметь очередь входящих запросов с атомарным добавлением, иначе есть риск потери входящих запросов, а, следовательно, и деградация качества сервиса. Атомарные операции предоставляют гарантии (неделимости), и к ним нужно прибегать, когда эти гарантии необходимы.

Почему примитивные операции не являются атомарными сами по себе? Так же было бы проще для всех.

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

Это относится только к операциям связанным с установкой переменных и прочей процессорной сфере деятельности?

Любая ли операция может быть атомарной?

У меня операция с двумя и более сайд-эффектами. Могу ли я все-таки что-нибудь с этим сделать?

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

Это обеспечивает прогресс алгоритма, но снимает все обязательства по временным рамкам (с которыми, формально говоря, и без того не все в порядке). В случае, если операции идемпотентны, подобная система будет рано или поздно приходить к требуемому состоянию без каких-либо заметных отличий от ожидаемого (за исключением времени выполнения).

Как все-таки определить атомарность операций в java?

Источник

Что такое атомарность в java? [дубликат]

Например метод compareAndSet из java.util.concurrent.atomic пакета.

что такое атомарность в java. Смотреть фото что такое атомарность в java. Смотреть картинку что такое атомарность в java. Картинка про что такое атомарность в java. Фото что такое атомарность в java

1 ответ 1

Необходимость координации межпроцессного взаимодействия возникает из-за того, что некоторая область памяти является общей.

Мы также знаем, что все потоки процесса используют одни и те же данные:

Простой пример общей переменной x управляют два потока: A и B :

Это недетерминированный код.

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

Некоторые возможные результаты:

Обратите внимание, что мы не можем распечатать 7 и получить окончательное значение 5.

Нам даже не нужно иметь несколько потоков, чтобы иметь проблему с параллелизмом. Достаточно иметь прерывания в системе. Рассмотрим приложение, которое используется для подсчета наступления какого-либо события.

Мы будем хранить количество в переменной count.

Мы предоставим пользователю возможность сброса счетчика (кнопка сброса).

count++; разбит на ряд более мелких операций. Если count равно 4, и мы увеличиваем его:

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

Переменная count равна 5, но она должна быть 0 (или 1). Пользователь нажал кнопку сброса, но счет не был сброшен!

Действие сброса «потеряно».

Эта проблема возникает из-за того, что инструкция count++ на самом деле состоит из трех вещей (чтение, добавление, запись) и может быть прервана в любой момент.

Когда мы выполняем операцию, которая не может быть прервана, мы говорим, что она atomic : атомарная.

Т.е. мы хотим для определенных операций жестко установить последовательность их выполнения = атомарность.

Потоково безопасный пример со счетчиком AtomicInteger:

Источник

Многопоточность в Java. Лекция 5: атомарные переменные и многопоточные коллекции

что такое атомарность в java. Смотреть фото что такое атомарность в java. Смотреть картинку что такое атомарность в java. Картинка про что такое атомарность в java. Фото что такое атомарность в java

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

5.1 Атомарные переменные

Рассмотрим ситуацию, когда два или более потоков пытаются изменить общий разделяемый ресурс: одновременно выполнять операции чтения или записи. Для избежания ситуации race condition нужно использовать synchronized-методы, synchronized-блоки или соответствующие блокировки. Если этого не сделать, разделяемый ресурс будет в неконсистентном состоянии, и значение не будет иметь никакого смысла. Однако использование методов synchronized или synchronized-блоков кода — очень дорогостоящая операция, потому что получение и блокировка потока обходятся недешево. Также этот способ является блокирующим, он сильно уменьшает производительность системы в целом.

Для решения этой проблемы придумали так называемые неблокирующие алгоритмы — non blocking thread safe algorithms. Эти алгоритмы называются compare and swap(CAS) и базируются на том, что современные процессоры поддерживают такие операции на уровне машинных инструкций. С выходом Java 1.5 появились классы атомарных переменных: AtomicInteger AtomicLong, AtomicBoolean, AtomicReference. Они находятся в пакете java.util.concurrent.atomic. Алгоритм compare and swap работает следующим образом: есть ячейка памяти, текущее значение в ней и то значение, которое хотим записать в эту ячейку. Сначала ячейка памяти читается и сохраняется текущее значение, затем прочитанное значение сравнивается с тем, которое уже есть в ячейке памяти, и если значение прочитанное ранее совпадает с текущим, происходит запись нового значения. Следует упомянуть, что значение переменной после чтения может быть изменено другим потоком, потому что CAS не является блокирующей операцией.

5.2 Проблема ABA или lost update

Есть два потока. Поток А прочитал значение из памяти, после чего предположим, планировщик потока прервал выполнение потока А. Затем значение из памяти читает поток B, а потом меняет его на ДРУГОЕ несколько раз. Предположим, что изначально значение атомарной переменной было 5, потом 4, в конце оно снова стало равно 5. Получилось так, что поток B в последний раз записал то же значение, что было изначально, т. е. значение, которое было прочитано потоком А. Затем планировщик возобновляет работу потока А, тот сравнивает значение, которое изначально прочитал ( 5) с тем,что есть в памяти сейчас (тоже 5). Эти значения равны — выполняется операция CAS. Возникает такая ситуация, когда новое для системы значение, установленное потоком B (которые, кстати, было равно изначальному значению, которое мы наблюдали до начала работы потоков А и В), затирается старым значением, которое должен был установить поток А. Возникает закономерный вопрос: почему значение теряется? Ведь поток А не записал в память значение, которые он пытался записать, а вот когда планировщик потоков возобновил работу потока А, значение наконец записалось?

Предположим, что ячейка памяти отображает какое-то глобальное состояние системы, или, например,какой-то циклический адрес или повторяющуюся сущность. В этом случае такое поведение просто недопустимо. Решить CAS-проблему может счетчик количества изменений. В первой операции при чтении значения из памяти происходит также чтение счетчика. При выполнении CAS-операции сравнивается значение памяти на текущий момент со старым значением, прочитанным ранее, и производится сравнение текущего значения счетчика со значением счетчика, которое было прочитано на предыдущем шаге. Если в обеих операциях сравнения получен результат true, выполняется CAS-операция и записывается новое значение. Также стоит отметить, что при записи нового значения с помощью CAS-операции значение счетчика увеличивается. Причем значение счетчика увеличивается только при записи!

Возвращаясь к примеру про lost update: когда поток А получит управление и попытается выполнить CAS-операцию, значения памяти будут равны, а значения счетчиков равны не будут. Поэтому поток А опять прочитает значение из памяти и значение счетчика — и опять попытается выполнить CAS-операцию. Это будет происходить до тех пор пока CAS-операция не выполниться успешно. Однако и при использовании счетчика возникают определенные проблемы, рассмотрение которых выходит за рамки нашего цикла статей. На практике сейчас используется алгоритм safe memory reclamation (SMR).

Рассмотрим, как работает класс AtomicLong (Листинги 1, 2 и 3).

Листинг 1: класс AtomicLong:

public final long getAndAdd(long delta) <
return unsafe.getAndAddLong(this, valueOffset, delta);
>

Здесь видно, что выполнение делегируется классу Unsafe.

Листинг 2: класс Unsafe:

public final long getAndAddLong(Object object, long offset, long newValue) <
long oldValue;
do <
oldValue = this.getLongVolatile(object, offset);
> while(!this
.compareAndSwapLong(object, offset, oldValue, oldValue + newValue));
return oldValue;
>

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

Листинг 3: класс AtomicLong. Получение смещения поля value в объекте AtomicLong.

static <
try <
valueOffset = unsafe
.objectFieldOffset(AtomicLong.class.getDeclaredField(«value»));
> catch (Exception ex) <
throw new Error(ex);
>
>

5.3 Многопоточные коллекции

Обычные коллекции в Java не синхронизированы и не могут быть безопасно использованы в многопоточной среде. За исключением случаев, когда обращение к этим коллекциям происходит из синхронизированных блоков кода. Неправильное использование коллекций может привести к неконсистентности данных или ConcurrentModificationException.

В Java есть коллекции, которые предназначены для использования в многопоточной среде. Они реализуют разные механизмы синхронизации данных. До выхода Java 1.5 в наличии были следующие многопоточные коллекции: Stack, Vector, HashTable. Все методы этих классов являются synchronized. Это означает, что при вызове любого метода этих классов другой поток будет заблокирован, даже если вызванный метод — метод чтения. Эти коллекции появились еще с версией Java 1 и в современной разработке их не стоит использовать. C выходом Java 1.2 появился утилитный класс Collections, который предоставляет статические методы для оборачивания стандартных коллекций в их синхронизированные представления. Это сделано для совместимости с Java версией 1.

После релиза Java 1.5, 1.6 и 1.7 появилась возможность использовать следующие классы коллекций, которые предназначены и оптимизированы для работы в многопоточной среде:

Первые две коллекции — copy on write структуры. Третья коллекция — skip list структура. Следующие две коллекции — классы Map, предназначенные для использования в многопоточных программах. Использование классов этих коллекций позволяет увеличить производительность программы по сравнению с использованием устаревших классов из Java 1. Рассмотрим статические методы утилитарного класса Collections.

5.4 Статические методы из класса Collections

Статические методы для работы с многопоточностью в классе Collections:

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

5.5 Copy On Write структуры данных

CopyOnWriteArrayList используется, когда есть много потоков, которые читают элементы из коллекции, и несколько потоков, которые редко записывают данные в коллекцию. Copy on write структура создает новую копию данных при записи в эту структуру. Это позволяет нескольким потокам одновременно читать данные, и одному потоку записывать элементы в коллекцию в каждый конкретный момент времени. Внутри структура данных содержит volatile массив элементов и при КАЖДОМ изменении коллекции: добавлении, удалении или замене элементов, создается новая локальная копия массива для изменений. После модификации измененная копия массива становится текущей. Массив элементов используется с ключевым словом volatile для того, чтобы все потоки сразу увидели изменения в массиве. Такой алгоритм работы с данными гарантирует, что читающие потоки будут читать не изменяющиеся во времени данные, и не будет сгенерирован ConcurrentModificationException при параллельном изменение массива. Если для чтения коллекции используется Iterator, при попытке вызвать remove() при обходе коллекции будет сгенерировано UnsupportedOperationException, потому что текущая копия коллекции не подлежит изменению. Для операций записи внутри класса CopyOnWriteArrayList создается блокировка, чтобы в конкретный момент времени только один поток мог изменять copy on write структуру данных (Листинг 1). Пример добавления элемента представлен в Листинге 4.

Листинг 4:

final ReentrantLock lock = new ReentrantLock();
public boolean add(E e) <
lock.lock();
try <
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
> finally <
lock.unlock();
>
>

Без блокировки наличие нескольких потоков, изменяющих структуру данных, могут привести к потере данных, записываемых потоками. Несколько потоков делают копию исходного массива данных, вносят изменения и записывают их. Какой-то из потоков завершает работу быстрее, какой-то — медленнее. Поток, который завершит свою работу последним, удалит изменения, сделанные другими потоками. С использованием блокировки ReentrantLock такая проблема исчезает. Рассмотрим программу из Листинга 5, которая показывает, что чтение элементов из CopyOnWriteArrayList происходит быстрее, чем чтение из полностью синхронизированной коллекции, либо коллекции, которая обернута соответствующим статическим методом из класса Collections.

Листинг 5:

import java.util.*;
public class PerformanceComparision <
public static void main(String[] args)
throws ExecutionException, InterruptedException <
List syncList = Collections.synchronizedList(new ArrayList<>());
List cowLst = new CopyOnWriteArrayList<>();
fillLst(syncList);
fillLst(cowLst);
System.out.println(«List synchronized:»);
checkList(syncList);
System.out.println(«CopyOnWriteArrayList:»);
checkList(cowLst);
>
private static void fillLst(List list) <
IntStream.rangeClosed(1, 100).forEach(list::add);
>
private static void checkList(List list)
throws ExecutionException, InterruptedException <
CountDownLatch latch = new CountDownLatch(1);
ExecutorService executors = Executors.newFixedThreadPool(2);
Future f1 = executors.submit(new ListRunner(latch, 0, 49, list));
Future f2 = executors.submit(new ListRunner(latch, 50, 99, list));
executors.shutdown();
latch.countDown();
System.out.println(«Thread1: » + f1.get()/1000);
System.out.println(«Thread2: » + f2.get()/1000);
>
>

public class ListRunner implements Callable <
private CountDownLatch latch;
private int start;
private int end;
private List list;
public ListRunner(CountDownLatch latch, int start, int end, List list) <
this.latch = latch;
this.start = start;
this.end = end;
this.list = list;
>
@Override
public Long call() throws Exception <
latch.await();
Integer integer;
long startTime = System.nanoTime();
for (int i = start; i что такое атомарность в java. Смотреть фото что такое атомарность в java. Смотреть картинку что такое атомарность в java. Картинка про что такое атомарность в java. Фото что такое атомарность в java

Реализация ConcurrentSkipListSet базируется на ConcurrentSkipListMap, структура ConcurrentSkipListSet похожа на структуру LinkedHashMap. Каждый элемент skip list структуры, кроме значения, содержит ссылку на соседние элементы. Также есть ссылки высших порядков, которые указывают на элементы, находящиеся впереди текущего, на произвольное число в определенном диапазоне, заданном для этого уровня ссылок. Для следующего уровня ссылок это число больше, чем для предыдущего.

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

5.7 Интерфейс BlockingQueue

BlockingQueue — интерфейс потокобезопасных очередей, в которую несколько потоков могут записывать данные и читать их оттуда. Это достигается за счет способности очереди блокировать поток, который добавляет или читает элементы из очереди. Например, когда поток пытается получить элемент из очереди, но очередь пустая — поток блокируется. Или когда поток пытается положить объект в очередь, которая уже заполнена, поток тоже блокируется.

Блокирующие очереди используются, когда одни потоки добавляют элементы в очередь, а другие читают их оттуда. Этот паттерн известен как producer consumer. Потоки, которые добавляют объекты в очередь, называются producer (Производитель). Добавлять элементы в очередь можно до тех пор, пока она не заполнится. Потоки которые читают значения, называются consumer (Потребитель). Читать из очереди можно только в случаях, когда в ней есть элементы.

BlockingQueue хранит свои элементы по принципу FIFO. Элементы отсортированы по времени нахождения в очереди, а голова находится там дольше всех остальных.

BlockingQueue не поддерживает значение null, при попытке вставить его генерируется NullPointerException.

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

Источник

Модель памяти Java и атомарность операций (java memory model)

В данной статье хочу в который раз показать, насколько важна синхронизация потоков, на примере такого понятия как атомарность (Atomicity) операций.

Рассмотрим такой программный код:

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

При запуске на экран будут выводится числа 0 и 1. Естественно, так как значение переменной i в потоке попеременно инкрементируется и декрементируется.

Можно сделать предположение, что если раскомментировать первую строку метода main, то значение i будет принимать максимум 3 разных значения, например 0, 1 и 2.

На самом деле в System.out будет выводится что-то типа такого:

А все дело в том, что операции инкремента и декремента не являются атомарными. Происходит последовательно считывание, увеличение/уменшение и далее запись значения.

Исправить ошибку в коде помогает synchronized блок:

Синхронизация по классу Atomicity говорит о том, что в один момент времени в synchronized блоке может находится не больше одного потока. Остальные потоки будут ждать, для того чтобы захватить монитор класса Atomicity. При чем случится это только после того, как активный поток отпустит этот монитор.

После исправления и запуска класса в консоли видим следующее:

Атомарность говорит о том, что некоторое действие (или их последовательность) должно происходить «все и сразу». Осутствие синхронизации может привести к катострофическим последствиям. Это далеко не NullPointerException, который можно обнаружить сразу. Программа может работать достаточно долго и визаульно никаких неполадок обнаружено не будет.

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

Чтобы гарантировать атомарность записи в long и double необходимо объявлять их как volatile.

Кстати, запись ссылки на объект (reference) всегда атомарна, не зависимо от того, имеем мы дело с 32-х или 64-х битной реализацией JVM.

В Java Memory Model рассказано много еще чего интересного, например о видимости (Visibility) и упорядоченности (Ordering). Но это уже совсем другая история.

Надеюсь, статья была вам интересна. В любом случае, жду ваших комментариев.

Источник

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *