1. Атомные классы J.U.C.
1.1 Введение в атомарные классы
- Атомарный класс, неотделимый от манипулирования данными объекта. атомный
- Функция: Функция атомарного класса аналогична функции блокировки, чтобы обеспечить безопасность потока в случае параллелизма, но атомарный класс имеет определенные преимущества по сравнению с блокировкой:
- Более тонкая детализация: атомарные классы снижают конкуренцию до уровня переменных
- Более высокая эффективность: обычно выше, но менее эффективна в условиях жесткой конкуренции.
- Большинство атомарных классов в J.U.C реализованы CAS (в конце будет анализ исходного кода)
1.2 Atomic*атомарный класс базового типа
- Возьмите AtomicInteger в качестве примера
- Общий метод
-
int get()получить текущее значение -
int getAndSet(int)Получить текущее значение и установить новое значение -
int getAndIncrement()Получить текущее значение и увеличить его -
int incrementAndGet()Получить значение после автоинкремента (по сравнению с тем же методом get получает значение до автоинкремента перед get, get получает значение после автоинкремента после) -
int getAndDecrement()Получить текущее значение и уменьшить его -
int getAndAdd(int)Получить текущее значение и добавить значение -
boolean compareAndSet(int expect, int update)Определить, соответствует ли текущее значение ожидаемому значениюexpect, если совпадает, установить значение обновленияupdate.
-
- Пример использования
/**
* 使用AtomicInteger 对比 非原子类,演示线程安全问题
*
* @author yiren
*/
public class AtomicIntegerExample01 {
private static AtomicInteger atomicInteger = new AtomicInteger();
private static volatile Integer count = 0;
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) {
atomicInteger.getAndIncrement();
count++;
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("atomicInteger=" + atomicInteger);
System.out.println("count=" + count);
}
}
atomicInteger=20000
count=13409
Process finished with exit code 0
-
Мы видим, что в это время
AtomicIntegerрезультат правильный -
а также
IntegerРезультат неверный, если вы хотите обеспечить потокобезопасность, вам нужно заблокировать -
Пока он не вызывается несколько раз в потоке, безопасность потока может быть гарантирована, а атомарный класс с одним методом гарантирует безопасность потока.
-
Примечание. Атомарная операция + Атомарная операция! = Атомарная операция
1.3 Atomic*ArrayАнализ типа массива
-
к
AtomicIntegerArrayНапример -
Когда это тип массива, он гарантирует, что работа каждого элемента является потокобезопасной.
-
AtomicIntegerArrayметод иAtomicIntegerметоды похожи, ноAtomicIntegerArrayДля метода требуется указанный массивindex -
Демонстрация кода:
/**
* @author yiren
*/
public class AtomicIntegerArrayExample {
private static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
public static void main(String[] args) throws InterruptedException {
Runnable incrRunnable = () -> {
for (int i = 0; i < atomicIntegerArray.length(); i++) {
atomicIntegerArray.incrementAndGet(i);
}
};
Runnable decrRunnable = () -> {
for (int i = 0; i < atomicIntegerArray.length(); i++) {
atomicIntegerArray.decrementAndGet(i);
}
};
ExecutorService executorService = Executors.newFixedThreadPool(100);
for (int i = 0; i < 1000; i++) {
executorService.execute(incrRunnable);
}
for (int i = 0; i < 1000; i++) {
executorService.execute(decrRunnable);
}
TimeUnit.SECONDS.sleep(5);
for (int i = 0; i < atomicIntegerArray.length(); i++) {
System.out.print(atomicIntegerArray.get(i) + " ");
}
}
}
0 0 0 0 0 0 0 0 0 0
1.4 AtomicReferenceАнализ типа приложения
-
AtomicReferenceроль класса иAtomicIntegerРазницы особой нет, просто изменился объект действия,AtomicIntegerсостоит в том, чтобы гарантировать атомарность целого числа, иAtomicReferenceэто сделать объект гарантией атомарности - а также
AtomicReferenceбыло бы лучше, чемAtomicIntegerБолее мощный, потому что объект будет содержать много свойств, использование аналогично - метод объекта класса
- кейс
/**
* @author yiren
*/
public class SpinLock {
private static AtomicReference<Thread> sign = new AtomicReference<>();
private static void lock() {
Thread current = Thread.currentThread();
while (!sign.compareAndSet(null, current)) {
System.out.println("fail to set!");
}
}
private static void unlock() {
Thread thread = Thread.currentThread();
sign.compareAndSet(thread, null);
}
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println("start to get lock");
SpinLock.lock();
System.out.println("got lock successfully!");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
SpinLock.unlock();
}
};
Thread thread = new Thread(runnable);
Thread thread1 = new Thread(runnable);
thread.start();
thread1.start();
}
}
- мы используем
AtomicReferenceреализовать спин-блокировку с помощьюcompareAndSetметод для сравнения и последующего назначения, чтобы избежать использования блокировок
1.5 Инкапсуляция общих типов в атомарные классы
-
к
AtomicIntegerFieldUpdaterНапример -
AtomicIntegerFieldUpdater.newUpdater(Counter.class, "count");Когда мы создаем объект, нам нужно указать целевой класс и свойства. -
И при работе вам нужно передать объект операции
-
Почему это должно быть сделано именно так? без прямого изменения исходного объекта?
- Если мы редко используем атомарные операции в кодировании, использование атомарных классов непосредственно в исходном объекте будет пустой тратой производительности.
- Кроме того, когда мы используем классы, определенные другими, у нас есть такие требования, а у других таких требований нет, и мы не можем уничтожить чужие определения.
AtomicIntegerFieldUpdaterИспользование , не нанесет вред исходному классу.
-
Примечание. Этот класс не поддерживает
staticукрашенная переменная -
Дело в следующем:
/**
* @author yiren
*/
public class AtomicFieldUpdaterExample {
private static Counter one = new Counter();
private static Counter two = new Counter();
private static AtomicIntegerFieldUpdater<Counter> updater = AtomicIntegerFieldUpdater.newUpdater(Counter.class, "count");
public static void main(String[] args) throws InterruptedException {
Runnable runnable = () -> {
for (int i = 0; i < 10000; i++) {
one.count++;
updater.getAndIncrement(two);
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println("one.count = " + one.count);
System.out.println("two.count = " + two.count);
}
private static class Counter {
volatile int count;
}
}
one.count = 18417
two.count = 20000
Process finished with exit code 0
- Видно, что после обновления исходный поток операций с данными безопасен.
1.6 Adderаккумулятор
-
Возьмите LongAdder в качестве примера
-
LongAdderЭто новый класс, представленный в Java8, с высокой степенью параллелизма.LongAdderСравниватьAtomicLongЭффективность высока, и его способность использовать пространство для времени -
Он фактически использует технологию блокировки сегментов,
LongAdderИзменение разных потоков, соответствующих разным ячейкам, снижает вероятность конфликта и повышает производительность одновременного выполнения.
- Демонстрация кода: сравнение
AtomicLongа такжеLongAdder
/**
* @author yiren
*/
public class AtomicLongExample {
public static void main(String[] args) {
AtomicLong counter = new AtomicLong();
ExecutorService executorService = Executors.newFixedThreadPool(16);
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
counter.incrementAndGet();
}
};
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
executorService.execute(task);
}
executorService.shutdown();
while (!executorService.isTerminated()) {
}
long end = System.currentTimeMillis();
System.out.println("end-start=" + (end - start)+ "ms");
}
}
end-start=2140ms
Process finished with exit code 0
/**
* @author yiren
*/
public class LongAdderExample {
public static void main(String[] args) {
LongAdder counter = new LongAdder();
ExecutorService executorService = Executors.newFixedThreadPool(16);
Runnable task = () -> {
for (int i = 0; i < 10000; i++) {
counter.increment();
}
};
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
executorService.execute(task);
}
executorService.shutdown();
while (!executorService.isTerminated()) {
}
long end = System.currentTimeMillis();
System.out.println("end-start=" + (end - start)+ "ms");
}
}
end-start=157ms
Process finished with exit code 0
- Из приведенного выше видно, что моя локальная машина — это процессор i7, и единственная разница между двумя программами заключается в том, что используемые атомарные классы отличаются более чем в 10 раз.
LongAdderзначительно больше, чемAtomicLongБыстрее
- Почему такая большая разница?
-
AtomicLongПосле завершения работы каждого потока ему нужно будет сбросить данные из локальной памяти потока в основную память, а затем другой поток должен сбросить новые данные из основной памяти.- Примечание. Вам необходимо знать JMM здесь:Принцип и применение JMM (модель памяти Java) в параллелизме
- а также
LongAdderНе нужно этого делать,LongAdderКаждый поток будет иметь свой собственный счетчик, который используется для подсчета только внутри своего собственного потока, так что он не будет мешать счетчикам других потоков. -
LongAdderВводится понятие сегментарного накопления, и существует внутренняяbaseпеременная иCell[] cellsМассивы участвуют в вычислении вместе-
base: Если конкуренция не является жесткой, она будет напрямую накапливаться в этой переменной. -
cells: Когда конкуренция жесткая, каждая нить разбрасывается и накапливается по-своему.cells[i]середина
-
- Применимая сцена
-
AtomicLong: сумма в низкой конкуренцииLongAdderАналогичен, но имеет метод CAS, обеспечивающий больше функциональности. -
LongAdder: он имеет очевидные преимущества в случае высокой параллелизма, но применим только к сценам статистического суммирования и подсчета и имеет определенные ограничения.
1.7 Accumulatorаккумулятор
-
к
LongAccumulatorНапример -
Основное использование
/**
* @author yiren
*/
public class AccumulatorExample {
public static void main(String[] args) {
// 累加 :此处的(left, right) -> left + right 可以替换成 Long::sum
// left=3
LongAccumulator longAccumulator = new LongAccumulator((left, right) -> left + right, 3);
// left=3+right=3+2=5
longAccumulator.accumulate(2);
// left=5+right=5+3=8
longAccumulator.accumulate(3);
System.out.println(longAccumulator.getThenReset());
// left=3
LongAccumulator longAccumulator1 = new LongAccumulator((left, right) -> left - right, 3);
// left=3-right=3-2=1
longAccumulator1.accumulate(2);
// left=1-right=1-3=-2
longAccumulator1.accumulate(3);
System.out.println(longAccumulator1.getThenReset());
// 求最大值
LongAccumulator longAccumulator2 = new LongAccumulator(Math::max, -1);
longAccumulator2.accumulate(14);
longAccumulator2.accumulate(3);
System.out.println(longAccumulator2.getThenReset());
}
}
8
-2
14
Process finished with exit code 0
- Подробности смотрите в примечаниях
- Некоторые люди могут найти это хлопотным. Это всего лишь один поток, а атомарный класс гарантированно работает в нескольких потоках. То есть мы можем напрямую вызывать разные потоки
/**
* @author yiren
*/
public class AccumulatorExample01 {
public static void main(String[] args) {
LongAccumulator accumulator = new LongAccumulator((left, right) -> {
long y = left;
long x = right;
return x + y;
}, 0);
ExecutorService executorService = Executors.newFixedThreadPool(100);
IntStream.range(1, 100).forEach(item -> executorService.execute(() -> accumulator.accumulate(item)));
executorService.shutdown();
while (!executorService.isTerminated()) {
}
System.out.println(accumulator.get());
}
}
4950
Process finished with exit code 0
- используемые сцены:
- Требуются параллельные вычисления, большой объем данных
- заказ не требуется
2. Принцип КАС
2.1 Что такое КАС?
-
CAS, полное название - сравнение и обмен
-
CAS имеет три значения. Значение памяти Значение, ожидаемое значение Ожидаемое значение, которое нужно изменить Целевое, тогда и только тогда, когда Ожидаемое==Значение, значение памяти может быть изменено на Целевое, в противном случае ничего не делать. Наконец, верните текущее значение
-
В современных процессорах CAS может быть реализован со специальными инструкциями, а JVM при реализации также будет использовать инструкции по сборке: cmpxchg
2.2 Демонстрация случая
- Эквивалентный код для CAS:
/**
* @author yiren
*/
public class CasExample {
private static volatile int value;
public static synchronized int compareAndSwap(int expect, int target) {
int oldValue = value;
if (expect == oldValue) {
value = target;
}
return value;
}
public static void main(String[] args) throws InterruptedException {
value = 0;
Runnable runnable = () -> {
compareAndSwap(0, 1);
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(value);
}
}
2.3 Анализ исходного кода CAS
-
Возьмем в качестве примера исходный код атомарного класса AtomicInteger.
-
Ниже приведено определение свойства в AtomicInteger:
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
-
в
valueЭто атрибут, в котором мы в основном сохраняем данные; а valueOffset представляет адрес смещения текущего объекта в адресе памяти значения переменной value, потому что Unsafe получает исходное значение данных в соответствии с адресом смещения памяти, так что мы может внедрить CAS через небезопасный интервал -
Давайте взглянем
AtomicIntegerконкретный метод работы
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
-
Видно, что основными родственными методами являются
unsafeизcompareAndSwapInt,getAndAddIntИ каждый вызов метода передает текущий объект, адрес смещения значения и операнд -
Небезопасный класс: это основной класс реализации CAS. Java не может напрямую обращаться к базовой операционной системе, но через локальные нативные методы. Тем не менее, JVM по-прежнему предоставляет способ. Небезопасный класс в JDK предоставляет атомы на аппаратном уровне.
-
Давайте посмотрим
int getAndAddInt(Object var1, long var2, int var4)метод
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
- var1 — это объект текущего AtomicInteger, а var2 — адрес смещения значения.
UnsafeизgetIntVolatileПолучите значение текущего объекта AtomicInteger, а затем вызовитеUnsafeизcompareAndSwapIntспособ сделать CAS. - посмотри
compareAndSwapIntОпределение
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
- Это нативный метод, метод реализации JVM на C++, который фактически вызывается, а код C++ вызывается
Atomic::cmpxchg, а в современных процессорах это может фактически соответствовать инструкциям сравнения и обмена в наборе инструкций сборкиCMPXCHG
2.5 Недостатки CAS
- АВА-проблема
Например
- Начальное значение равно 0, поток 1 изменяет его на 1, а затем снова меняет на 0.
- Поток 2 получает 0 до того, как поток 1 изменится на 1, а затем сравнивает его после того, как поток 1 снова изменится на 0.
- В этот момент поток 2 будет успешно изменен, но поток 2 не знает, что поток 1 изменил номер в нем.
- Для этого можно использовать номера версий для решения, например 1A-> 2B-> 3A-4B, каждая операция имеет номер версии в качестве записи. При сравнении используйте номер версии для сравнения
- В Java есть AtomicStampedReference, который можно использовать для решения проблем ABA.
- В случае высокого параллелизма эффективность очень низкая, и может потребоваться много сравнений.
- Источник содержания статьи:
- «Искусство параллельного программирования на Java», исходный код версии JDK1.8, курс MOOC Wukong JUC
- Ставьте палец вверх, если считаете, что сможете 👍 Спасибо!
обо мне
- Координатор Ханчжоу, специализирующийся в области компьютерных наук и технологий в общеобразовательных колледжах и университетах.
- Окончил 20 лет, в основном занимается внутренней разработкой стека технологий Java.
- GitHub: github.com/imyiren
- Blog : imyi.ren