В компьютерной системе связь между ЦП и памятью осуществляется через шину.Когда поток занимает ЦП для выполнения инструкций, он попытается кэшировать некоторые переменные, к которым необходимо получить доступ из памяти, в свою собственную область кэша. .Модификации также не сразу сопоставляются с памятью.
В это время другие потоки не увидят никаких изменений переменной в памяти, что мы и называем проблемой видимости памяти. В последующих статьях мы предложили всего два решения.
Один из них — использовать ключевые словаvolatileИзменение общих глобальных переменных и принцип реализации volatile можно условно разделить на два этапа.Любая операция изменения переменной будет добавлена виртуальной машиной для немедленной записи значения в буферную область, где находится переменная. Буферы процессора. Это означает, что если другие процессоры захотят снова использовать переменную, в ней нет кеша, и они будут вынуждены обращаться к памяти для получения последних данных.
Во-вторых, использовать ключевые слова.synchronizedИ добиться согласованности данных с помощью встроенной блокировки объекта.Основная идея в том, что если поток заблокирован из-за неспособности конкурировать за блокировку, то он думает, что другие потоки работают и могут изменить данные некоторых общих переменных, тогда данные в памяти будут обновлены, как только будет получена блокировка, и в то же время данные будут синхронизированы с памятью до выхода потока из блока кода синхронизации.
На самом деле, мы редко используем второй метод для решения проблемы с видимостью памяти, он кажется немного излишним, чаще используется ключевое слово volatile. Но у volatile есть определенные применимые сценарии и свои ограничения, давайте посмотрим на это вместе.
Ограничения изменчивости
Без лишних слов давайте посмотрим на кусок кода:
public class MainTest {
private static volatile int count;
@Test
public void testVolatile() throws InterruptedException {
Thread1[] thread1s = new Thread1[100];
for (int i = 0; i < 100; i++){
thread1s[i] = new Thread1();
thread1s[i].start();
}
for (int j = 0; j < 100; j++){
thread1s[j].join();
}
System.out.println(count);
}
//每个线程随机自增 count
private class Thread1 extends Thread{
@Override
public void run(){
try {
Thread.sleep((long) (Math.random() * 500));
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
}
Мы украшаем переменную count с помощью volatile, а затем создаем сто потоков и запускаем их.Согласно нашему предыдущему пониманию, как только значение переменной count будет изменено, оно сразу же будет видно другим потокам и не будет кэшироваться в своем собственном рабочая память. Но оказалось, что это не так.
Запускайте несколько раз с разными результатами
94
96
98
....
На самом деле, причина очень проста, мы только сказали, что volatile будет записывать обратно память после изменения значения переменной и сделает недействительной ссылку на переменную в других кешах ЦП, заставив другие потоки повторно получать значение переменной из основная память.
Но операция count++ не является атомарной операцией, как мы уже говорили ранее, эта операция заставит ЦП выполнять следующие действия:
- Прочитайте значение переменной из кеша процессора и поместите его в регистр A.
- увеличить счетчик и сохранить значение в другом регистре B
- Записать данные из регистра B в кеш и записать обратно в память с блокировкой кеша
И если первый шаг только что выполнен, или второй шаг только что выполнен, но третий шаг не выполнен, какой-то другой поток изменяет значение переменной и делает недействительной ссылку на переменную в кеше в текущем ЦП, То третий шаг сначала считывает значение из памяти из-за аннулирования кеша, а затем перезаписывает кеш значением в регистре B и сбрасывает его в память.
Это означает, что изменения, сделанные другими потоками до этого, перезаписываются, и мы не получаем ожидаемых результатов. Вывод состоит в том, что ключевое слово volatile имеет видимость, но не атомарность.
Переменная атомарного типа
После JDK1.5 пакет java.util.concurrent.atomic, разработанный Дугом Ли, содержит все классы, связанные с атомарными типами.
в,
- AtomicBoolean: атомарный тип соответствующего логического типа.
- AtomicInteger: атомарный тип соответствующего целочисленного типа.
- AtomicLong: аналогично
- AtomicIntegerArray: соответствующий тип массива.
- AtomicLongArray: аналогично
- AtomicReference: атомарный тип соответствующего ссылочного типа
- AtomicIntegerFieldUpdater: Тип обновления поля
Функции остальных классов будут подробно описаны позже.
Для атомарного типа, соответствующего базовому типу, возьмем в качестве примера класс AtomicInteger, чтобы увидеть его реализацию в исходном коде.
Реализация, связанная с AtomicInteger
Значение переменной типа int определяется внутренне, и значение модифицируется как volatile, что указывает на то, что любое изменение значения значения поля немедленно видно другим потокам.
Конструктор позволяет передать начальное значение, в противном случае значение будет равно нулю.
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
Этот метод является атомарной операцией "i++", давайте проследим и посмотрим:
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 — это смещение поля, с помощью которого мы можем найти поле значения. var4 здесь установлен на единицу.
Логика кода тоже проста, вынуть значение внутреннего поля значения и временно сохранить его в переменной значение5, а потом снова судить, если значение поля значения по-прежнему равно значению5, тоатомарная операцияИзменение значения на значение4 + значение5, по сути, добавляет единицу.
В противном случае это означает, что другой поток изменил значение поля значения после последнего доступа текущего потока, поэтому мы повторно получаем значение этого поля, пока его никто не изменил, и увеличиваем его.
Этот метод compareAndSwapInt обычно называется "CAS". Нижний уровень поддерживается системными инструкциями. Это атомарная инструкция, которая сравнивает и изменяет. Если значение равно A, оно будет изменено на B, в противном случае оно будет возвращено.
Остальные методы в AtomicInteger примерно похожи и основаны на этом методе CAS.
- int getAndAdd(int delta): увеличить дельту и получить значение перед модификацией
- int incrementAndGet(): увеличить и получить измененное значение
- int decrementAndGet(): уменьшить и получить измененное значение
- int addAndGet(int delta): увеличить дельту и получить измененное значение
Исходя из этого, мы рефакторим приведенную выше демо-версию, небезопасную для потоков:
//构建一个原子类型变量 aCount
private static volatile AtomicInteger aCount = new AtomicInteger(0);
@Test
public void testAtomic() throws InterruptedException {
Thread2[] threads = new Thread2[100];
for (int i = 0; i < 100; i++){
threads[i] = new Thread2();
threads[i].start();
}
for (int i = 0; i < 100; i++){
threads[i].join();
}
System.out.println(aCount.get());
}
private class Thread2 extends Thread{
@Override
public void run(){
try {
Thread.sleep((long) (500 * Math.random()));
} catch (InterruptedException e) {
e.printStackTrace();
}
//原子自增
aCount.getAndIncrement();
}
}
Измененный код всегда будет иметь результат 100, независимо от того, сколько раз он будет запущен. Связанное содержимое AtomicLong и AtomicReference примерно одинаково, и все они основаны на нашем методе «CAS», поэтому я не буду повторять их здесь.
FieldUpdater основан на отражении для атомарного изменения значения переменных, я не буду здесь много говорить, давайте взглянем на некоторые проблемы "CAS".
Ограничения CAS
АВА-проблема
Типичной проблемой CAS является «проблема ABA».Мы знаем, что основной принцип работы CAS заключается в том, чтобы сначала прочитать значение целевой переменной, а затем вызвать атомарную инструкцию, чтобы определить, равно ли значение ожидаемому нами значению. .Если она равна, считается, что она не была изменена другими, в противном случае данные считаются грязными, и значение переменной считывается заново.
Но проблема в том, что если значение переменной a равно 100, наш CAS-метод тоже читает 100, то приходит поток, чтобы изменить переменную на 999, а затем другой поток меняет ее на 100. И настала очередь нашего основного потока обнаружить, что значение a по-прежнему равно 100. Он считает, что никто не конкурирует с ним в изменении переменной a, поэтому он изменяет значение a.
В этом случае, хотя CAS будет успешно обновлен, потенциальные проблемы будут, а операции добавленного в середине потока вообще не будут видны последнему потоку. В то время как общее решение заключается в добавлении метки времени к каждой операции, CAS не только обращает внимание на исходное значение переменной, но также обращает внимание на время последней модификации.
Длительное время цикла и высокие накладные расходы
Наши методы CAS обычно определяются в цикле, и цикл не завершится до тех пор, пока изменение не будет успешным.Если значение переменной всегда изменяется другими потоками в некоторых случаях с большим параллелизмом, поток всегда выносит решения в цикле. ценности менее эффективны.
Поэтому CAS подходит для случаев, когда количество параллелизма не очень велико, а эффективность гораздо выше, чем у механизма блокировки.
Гарантируются только атомарные операции над одной переменной
CAS может выполнять атомарные операции только над одной переменной, а механизм блокировки другой, после получения блокировки все общие переменные можно модифицировать без проблем, потому что другие не могут модифицировать эти общие переменные без блокировки.
Подводя итог, блокировка на самом деле является пессимистичной идеей: «Я думаю, что все будут конкурировать со мной за использование некоторых ресурсов, поэтому я блокирую его после того, как получу ресурс, а затем снимаю блокировку, когда он израсходован», и CAS — это оптимистичная мысль: «Я думал, что я единственный, кто использует эти ресурсы, и если кто-то другой использует их, я мог бы попробовать еще раз».
CAS является краеугольным камнем реализации различных параллельных контейнеров в будущем.Это оптимистичный и неблокирующий алгоритм, который поможет улучшить нашу параллельную производительность.
Весь код, изображения, файлы в статье хранятся в облаке на моем GitHub:
Добро пожаловать в официальную учетную запись WeChat: OneJavaCoder, все статьи будут синхронизированы в официальной учетной записи.