Java Language
Атомные типы
Поиск…
Вступление
Java Atomic Types - это простые переменные типы, которые обеспечивают основные операции, которые являются потокобезопасными и атомными, не прибегая к блокировке. Они предназначены для использования в тех случаях, когда блокировка является узким местом параллелизма или существует риск взаимоблокировки или оживления.
параметры
параметр | Описание |
---|---|
задавать | Неустойчивый набор полей |
получить | Неустойчивое чтение поля |
lazySet | Это упорядоченная операция в полевых условиях |
compareAndSet | Если значение представляет собой значение expeed, оно отправляется на новое значение |
getAndSet | получить текущее значение и обновить |
замечания
Многие из них по существу сочетают волатильные чтения или записи и операции CAS . Лучший способ понять это - посмотреть исходный код напрямую. Например, AtomicInteger , Unsafe.getAndSet
Создание атомных типов
Для простого многопоточного кода приемлема синхронизация . Однако использование синхронизации имеет сильное влияние, и по мере того, как кодовая база становится более сложной, вероятность возрастает, и в конечном итоге вы столкнетесь с Deadlock , Starvation или Livelock .
В случаях более сложного параллелизма использование Atomic Variables часто является лучшей альтернативой, так как позволяет доступ к отдельной переменной поточно-безопасным образом без накладных расходов на использование синхронизированных методов или кодовых блоков.
Создание типа AtomicInteger
:
AtomicInteger aInt = new AtomicInteger() // Create with default value 0
AtomicInteger aInt = new AtomicInteger(1) // Create with initial value 1
Аналогично для других типов экземпляров.
AtomicIntegerArray aIntArray = new AtomicIntegerArray(10) // Create array of specific length
AtomicIntegerArray aIntArray = new AtomicIntegerArray(new int[] {1, 2, 3}) // Initialize array with another array
Аналогично для других типов атомов.
Есть заметное исключение, что нет типов float
и double
. Их можно моделировать с помощью Float.floatToIntBits(float)
и Float.intBitsToFloat(int)
для float
а также Double.doubleToLongBits(double)
и Double.longBitsToDouble(long)
для удвоений.
Если вы хотите использовать sun.misc.Unsafe
вы можете использовать любую примитивную переменную в качестве атома, используя атомную операцию в sun.misc.Unsafe
. Все примитивные типы должны быть преобразованы или закодированы в int или longs, чтобы таким образом использовать его. Подробнее об этом см .: sun.misc.Unsafe .
Мотивация для атомных типов
Простым способом реализации многопоточных приложений является использование встроенных синхронизирующих и блокирующих примитивов Java; например synchronized
ключевое слово. В следующем примере показано, как мы можем использовать synchronized
для накопления счетчиков.
public class Counters {
private final int[] counters;
public Counters(int nosCounters) {
counters = new int[nosCounters];
}
/**
* Increments the integer at the given index
*/
public synchronized void count(int number) {
if (number >= 0 && number < counters.length) {
counters[number]++;
}
}
/**
* Obtains the current count of the number at the given index,
* or if there is no number at that index, returns 0.
*/
public synchronized int getCount(int number) {
return (number >= 0 && number < counters.length) ? counters[number] : 0;
}
}
Эта реализация будет работать правильно. Однако, если у вас есть большое количество потоков, делающих много одновременных вызовов на одном и том же объекте Counters
, синхронизация может быть узким местом. В частности:
- Каждый вызов
synchronized
метода начинается с текущего потока, который получает блокировку для экземпляраCounters
. - Поток будет удерживать блокировку, пока он проверяет значение
number
и обновляет счетчик. - Наконец, он освободит блокировку, позволяя другим потокам получить доступ.
Если один поток пытается захватить блокировку, а другой удерживает ее, то попытка попытки будет заблокирована (остановлена) на шаге 1 до тех пор, пока блокировка не будет отпущена. Если несколько потоков ждут, один из них получит его, а остальные будут заблокированы.
Это может привести к возникновению нескольких проблем:
Если для блокировки много споров (т. Е. Много потоков пытаются ее приобрести), то некоторые потоки могут быть заблокированы в течение длительного времени.
Когда поток блокируется в ожидании блокировки, операционная система, как правило, пытается переключиться на другой поток. Такое переключение контекста оказывает относительно большое влияние на производительность процессора.
Когда есть несколько потоков, заблокированных на одном замке, нет никаких гарантий, что любой из них будет обрабатываться «честно» (т. Е. Каждый поток, как гарантируется, планируется запустить). Это может привести к голоданию нитей .
Как реализовать Atomic Types?
Начнем с перезаписи приведенного выше примера с AtomicInteger
счетчиков AtomicInteger
:
public class Counters {
private final AtomicInteger[] counters;
public Counters(int nosCounters) {
counters = new AtomicInteger[nosCounters];
for (int i = 0; i < nosCounters; i++) {
counters[i] = new AtomicInteger();
}
}
/**
* Increments the integer at the given index
*/
public void count(int number) {
if (number >= 0 && number < counters.length) {
counters[number].incrementAndGet();
}
}
/**
* Obtains the current count of the object at the given index,
* or if there is no number at that index, returns 0.
*/
public int getCount(int number) {
return (number >= 0 && number < counters.length) ?
counters[number].get() : 0;
}
}
Мы заменили int[]
на AtomicInteger[]
и инициализировали его экземпляром в каждом элементе. Мы также добавили вызовы incrementAndGet()
и get()
вместо операций над значениями int
.
Но самое главное, что мы можем удалить synchronized
ключевое слово, потому что блокировка больше не требуется. Это работает, потому что операции incrementAndGet()
и get()
являются атомарными и потокобезопасными . В этом контексте это означает, что:
Каждый счетчик в массиве будет наблюдаться только в состоянии «перед» для операции (например, «приращение») или в состоянии «после».
Предполагая, что операция происходит в момент времени
T
, нить не сможет увидеть состояние «раньше» после времениT
Кроме того, хотя два потока могут фактически попытаться обновить один и тот же экземпляр AtomicInteger
одновременно, реализации операций гарантируют, что только одно приращение происходит одновременно на данном экземпляре. Это делается без блокировки, что часто приводит к повышению производительности.
Как работают Atomic Types?
Атомные типы обычно полагаются на специализированные аппаратные команды в наборе команд целевой машины. Например, наборы инструкций на базе Intel предоставляют инструкцию CAS
( Compare and Swap ), которая будет выполнять определенную последовательность операций с памятью атомарно.
Эти низкоуровневые инструкции используются для реализации операций более высокого уровня в API соответствующих классов AtomicXxx
. Например, (опять же, в C-подобном псевдокоде):
private volatile num;
int increment() {
while (TRUE) {
int old = num;
int new = old + 1;
if (old == compare_and_swap(&num, old, new)) {
return new;
}
}
}
Если на AtomicXxxx
нет споров, тест if
будет успешным, и цикл завершится немедленно. Если есть конфликт, то if
будет терпеть неудачу для всех, кроме одного из потоков, и они будут «вращаться» в цикле для небольшого числа циклов цикла. На практике скорость вращения на несколько порядков (за исключением нереалистично высоких уровней конкуренции, когда синхронизация работает лучше, чем атомные классы, потому что, когда операция CAS завершается с ошибкой, повтор будет только увеличивать конкуренцию), чем приостановка потока и переход на другой один.
Кстати, инструкции CAS обычно используются JVM для реализации незащищенной блокировки . Если JVM может видеть, что блокировка в настоящий момент не заблокирована, она попытается использовать CAS для получения блокировки. Если CAS преуспевает, тогда нет необходимости выполнять дорогостоящее планирование потоков, переключение контекста и так далее. Дополнительные сведения об используемых методах см. В разделе Блокировка смещения в HotSpot .