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