В этой статье подробно рассматриваются отказоустойчивые и отказоустойчивые механизмы (переход к исходному коду).

Java

Привет, я программная обезьяна, которая выращивает свиней. Обычно я люблю возиться с технологиями и выращивать «свиней». Время от времени я буду делиться некоторыми техническими статьями.
Следуй за мной, не теряйся~

В этой статье подробно рассматриваются отказоустойчивые и отказоустойчивые механизмы (переход к исходному коду).

Что такое отказоустойчивость?

Отказоустойчивый механизмКоллекции JavaОдин из (коллекция)механизм ошибки. При обходе объекта коллекции с помощью итератора, если структура объекта коллекции изменяется (добавляется, удаляется) в процессе обхода, он выдаетConcurrent Modification Exception(Исключение одновременной модификации).

Таким образом, в многопоточной среде легко создать исключение Concurrent Modification Exception, например, поток 1 просматривает коллекцию, а поток 2 изменяет (добавляет, удаляет) коллекцию. А не нить бы не кинул? Очевидно, что один поток будет иметь аналогичную ситуацию, например, когда основной поток модифицирует (добавляет, удаляет или изменяет) коллекцию во время обхода, основной поток выдает исключение Concurrent Modification Exception.

Принцип отказоустойчивости

Давайте сначала рассмотрим несколько вопросов, давайте подумаем над ними.

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

Q1:

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
    String tmp = iter.next();
    System.out.println(tmp);
    if (tmp.equals("1")) {
        list.remove("1");
    }
}

Q2:

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
    String tmp = iter.next();
    System.out.println(tmp);
    if (tmp.equals("3")) {
        list.remove("3");
    }
}

Q3:

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
    String tmp = iter.next();
    System.out.println(tmp);
    if (tmp.equals("4")) {
        list.remove("4");
    }
}

Q4:

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
for (String i : list) {
    if ("1".equals(i)) {
        list.remove("1");
    }
}

Q5:

List<String> list = Arrays.asList("1", "2", "3", "4");
for (String i : list) {
    if ("1".equals(i)) {
        list.remove("1");
    }
}

Q6:

List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
    String tmp = iter.next();
    System.out.println(tmp);
    if (tmp.equals("1")) {
        iter.remove("1");
    }
}

Ответы: 1, 3, 4.

Вы поняли это правильно?

Раз уж мы хотим поговорить о принципе, давайте перейдем к исходному коду!

Взгляните на исходный код ArrayList!

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

private class Itr implements Iterator<E> {
    int cursor;       // index of next element to return
    int lastRet = -1; // index of last element returned; -1 if no such
    int expectedModCount = modCount;

    public boolean hasNext() {
        return cursor != size;
    }

    @SuppressWarnings("unchecked")
    public E next() {
        checkForComodification();
        int i = cursor;
        if (i >= size)
            throw new NoSuchElementException();
        Object[] elementData = ArrayList.this.elementData;
        if (i >= elementData.length)
            throw new ConcurrentModificationException();
        cursor = i + 1;
        return (E) elementData[lastRet = i];
    }

    public void remove() {
        if (lastRet < 0)
            throw new IllegalStateException();
        checkForComodification();

        try {
            ArrayList.this.remove(lastRet);
            cursor = lastRet;
            lastRet = -1;
            expectedModCount = modCount;
        } catch (IndexOutOfBoundsException ex) {
            throw new ConcurrentModificationException();
        }
    }

    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

Видно, что виновником является метод checkForCommodification, который выдает проклятое исключение, когда modCount != expectModCount, а первое предложение в следующем методе — checkForCommodification, так что при обходе коллекции могут возникать исключения параллельных модификаций.

Кроме того, после создания итератора начальное значение expectModCount равно modCount. Изменение коллекции изменит только modCount, а expectModCount будет изменено только на modCount в методе удаления итератора. Метод итератора будет подробно объяснен позже. .

Взгляните на некоторые методы ArrayList!

Удалить:

public boolean remove(Object o) {
    if (o == null) {
        for (int index = 0; index < size; index++)
            if (elementData[index] == null) {
                fastRemove(index);
                return true;
            }
    } else {
        for (int index = 0; index < size; index++)
            if (o.equals(elementData[index])) {
                fastRemove(index);
                return true;
            }
    }
    return false;
}


private void fastRemove(int index) {
    modCount++;
    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work
}

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

добавлять:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

ModCount++ в методе sureCapacityInternal.

набор:

public E set(int index, E element) {
    rangeCheck(index);

    E oldValue = elementData(index);
    elementData[index] = element;
    return oldValue;
}

Видно, что метод set не modCount++, поэтому изменение элемента набора не будет быстрым.

Проанализировав волну исходного кода, давайте взглянем на Q1, Q2 и Q3 и обнаружим, что метод удаления списка был скорректирован, поэтому modCount не должен быть равен ожидаемомуModCount, так почему же Q2 не дал сбоев быстро? На самом деле такая ситуацияРуководство по Java-разработке на Alibaba(PDF-файл можно получить бесплатно, обратив внимание на мой публичный аккаунт «Program Ape Raising Pigs» и ответив на Руководство по разработке Java для Alibaba). Также упоминается:

image-20201003012527249

Противоположный пример удивителен,Исключение не выдается, когда "1",После изменения на "2" будет выброшено исключение! Почему? ? На самом деле эта ситуация похожа на Q2, который является предпоследним элементом удаления, но в этот раз не выбрасывается исключение, не волнуйтесь, давайте еще раз посмотрим на исходный код!

Класс итератора имеет эти две переменные:

int cursor;       // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such

курсор — это индекс следующей возвращаемой переменной, а lastRet — это индекс последней возвращаемой переменной.

public boolean hasNext() {
    return cursor != size;
}

Метод hasNext сообщает нам, что в коллекции есть следующий элемент, только если индекс следующей переменной не равен размеру.

но! ! Когда удалить, размер--, то для Q2, после удаления элемента "3", размер становится 3, и курсор в это время тоже 3, затем когда мы идем к hasNext, мы обнаруживаем, что курсор и размер равны, тогда It выйдет из обхода, "4" вообще не будет пройдена, давайте проверим текущий результат:

image-20201003014032974

Итак, Q2 не выдал исключение, потому что он завершился после удаления, и у меня не было времени перейти к следующему методу~

Что насчет Q3? Q3 - удалить «4», который является последним элементом.Разумеется, если последний элемент будет удален, он выйдет? Не можете перейти к следующему методу в следующий раз?

Неправильно, после удаления "4" вы не вышли напрямую! Подумайте об этом, после удаления размер становится равным 3, но курсор в это время равен 4, затем, когда вы перейдете к hasNext, вы найдете 4!=3, и вы снова войдете в цикл, тогда результат можно представить, перейти к следующему методу.закончил свою грешную жизнь...

Взгляните на беговые результаты:

image-20201003014948342

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

Сначала скомпилируйте следующий код с помощью javac:

import java.util.ArrayList;
import java.util.List;

public class Main{
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
        list.add("4");
        for (String i : list) {
            if ("1".equals(i)) {
                list.remove("1");
            }
        }
    }
}

чтобы получить файл .class:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import java.util.ArrayList;
import java.util.Iterator;

public class Main {
    public Main() {
    }

    public static void main(String[] var0) {
        ArrayList var1 = new ArrayList();
        var1.add("1");
        var1.add("2");
        var1.add("3");
        var1.add("4");
        Iterator var2 = var1.iterator();

        while(var2.hasNext()) {
            String var3 = (String)var2.next();
            if ("1".equals(var3)) {
                var1.remove("1");
            }
        }

    }
}

Обнаружение почти такое же, как и в Q1, поэтому использование расширенного цикла for для обхода коллекции по существу такое же, как использование итератора.

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

Сначала посмотрите на результаты работы:image-20201003020039543

Хм? Ты солгал мне, разве это не исключение?

... Посмотрите внимательно, выбрасывается исключение UnsupportedOperationException, а не наше сегодняшнее исключение параллельной модификации главного героя.

Почему?

Просто взгляните на исходный код Array.asList:

image-20201003020228893

Обнаружено, что ArrayList, сгенерированный asList, является статическим внутренним классом, а не java.util.ArrayList., давайте посмотрим, какие у него есть методы, о нет, у него нет методов добавления и удаления ArrayList, затем посмотрим, какие методы есть у его родительского класса AbstractList...

public void add(int index, E element) {
    throw new UnsupportedOperationException();
}

public E remove(int index) {
    throw new UnsupportedOperationException();
}

Таким образом, основной причиной является то, что ArrayList, сгенерированный asList, не переопределяет эти методы, поэтому вызов этих методов вызовет исключение UnsupportedOperationException.

Стучите по доске! ! Отсюда также видно, чтоМы не можем добавлять, удалять или изменять ArrayList, сгенерированный asList, а нижний слой других — это массив, украшенный final напрямую... Поэтому мы должны избегать этой ямы во время разработки..

Q6 является правильной операцией. Поскольку метод удаления итератора сбрасывает ожидаемыйModCount в modCount, неравенства не будет.

Как избежать исключения отказоустойчивого броска?

1. Если вам нужно изменить коллекцию во время обхода, рекомендуется использовать такие методы, как удаление итераторов, вместо таких методов, как удаление коллекций. (Честно соблюдайте спецификации разработки Java от Alibaba...)

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

3.CopyOnWriteArrayList (с использованием отказоустойчивости)

Что такое отказоустойчивость?

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

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

Наконец, представьте типичный отказоустойчивый контейнер — CopyOnWriteArrayList~.

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

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    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();
    }
}

final void setArray(Object[] a) {
    array = a;
}

Установлено, что при добавлении требуется блокировка, иначе при многопоточной записи будет скопировано N копий...

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

Сценарии применения CopyOnWrite: параллельный контейнер CopyOnWrite используется для параллельных сценариев с большим количеством операций чтения и меньшим количеством операций записи. Например, белый список, черный список, сценарии доступа и обновления товарных категорий.

Недостатки CopyOnWrite: Контейнер CopyOnWrite имеет много преимуществ, но есть и две проблемы, а именно использование памяти и согласованность данных. Итак, при разработке необходимо обратить внимание на:

проблема с использованием памяти. Из-за механизма копирования при записи CopyOnWrite, когда выполняется операция записи, память двух объектов будет находиться в памяти одновременно, старый объект и вновь записанный объект (примечание: при копировании только ссылка в контейнере копируется, просто новые объекты создаются и добавляются в новый контейнер при записи, а объекты старого контейнера все еще используются, поэтому есть две объектной памяти). Если память, занимаемая этими объектами, относительно велика, например, около 200 МБ, то при записи 100 МБ данных память будет занимать 300 МБ, тогда это время, вероятно, вызовет частые Yong GC и Full GC.

Чтобы решить проблему использования памяти, вы можете уменьшить потребление памяти большими объектами, сжав элементы в контейнере.Например, если все элементы представляют собой десятичные числа, вы можете сжать их до 36 или 64 шестнадцатеричных чисел. Или не используйте контейнер CopyOnWrite, а используйте другие параллельные контейнеры, такие как ConcurrentHashMap.

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

Суммировать

Безотказное поведение итераторов не может быть гарантировано (вопрос 2 выше, кровавый урок...), потому что в общем случае невозможно дать какие-либо жесткие гарантии относительно того, будут ли происходить несинхронизированные одновременные модификации. Отказоустойчивые итераторы сделают все возможное, чтобы вызвать исключение ConcurrentModificationException. Поэтому было бы неправильно писать программу, которая опирается на это исключение, чтобы улучшить корректность таких итераторов: безотказное поведение итераторов следует использовать только для обнаружения ошибок.