Что, черт возьми, такое отказоустойчивое, что случайно ставит Java-разработку на яму?

Java

GitHub 2.1k ЗвездаПуть к тому, чтобы стать Java-инженером, почему бы тебе не прийти и не узнать?

GitHub 2.1k ЗвездаПуть к тому, чтобы стать Java-инженером, ты правда не хочешь узнать?

GitHub 2.1k ЗвездаПуть к тому, чтобы стать Java-инженером, ты действительно уверен, что не хочешь узнать?

Я представил отказоустойчивый механизм в Java в статье «Почему Alibaba запрещает операцию удаления/добавления элементов в цикле foreach», но я не стал вдаваться в подробности. В этой статье я расскажу о отказоустойчивости в глубина.

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

Во-первых, давайте посмотрим на объяснение отказоустойчивости в Википедии:

In systems design, a fail-fast system is one which immediately reports at its interface any condition that is likely to indicate a failure. Fail-fast systems are usually designed to stop normal operation rather than attempt to continue a possibly flawed process. Such designs often check the system's state at several points in an operation, so any failures can be detected early. The responsibility of a fail-fast module is detecting errors, then letting the next-highest level of the system handle them.

Грубо говоря: в системном дизайне система с быстрым отказом — это система, которая немедленно сообщает о любом состоянии, которое может указывать на отказ. Отказоустойчивые системы обычно предназначены для остановки нормальной работы, а не для попытки продолжить потенциально ошибочный процесс. Эта конструкция обычно проверяет состояние системы на нескольких этапах работы, поэтому любые сбои могут быть обнаружены на ранней стадии. Работа отказоустойчивого модуля состоит в том, чтобы обнаружить ошибки, а затем предоставить возможность их обработки следующему высшему уровню системы.

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

Возьмем простой отказоустойчивый пример:

public int divide(int divisor,int dividend){
    if(dividend == 0){
        throw new RuntimeException("dividend can't be null");
    }
    return divisor/dividend;
}

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

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

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

Поскольку отказоустойчивость — лучший механизм, почему в заголовке статьи говорится, что при отказоустойчивости будут ямки?

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

отказоустойчивость в классах коллекций

Обычно мы говорим о механизме отказоустойчивости в Java, который по умолчанию относится к механизму обнаружения ошибок для коллекций Java. Когда несколько потоков выполняют структурные изменения в частичной коллекции, может возникнуть отказоустойчивый механизм, и в это время будет выдано исключение ConcurrentModificationException (позже замененное CME).

CMException возникает, когда метод обнаруживает одновременную модификацию объекта, но такая модификация не разрешена.

Во многих случаях именно из-за того, что в коде выбрасывается CMException, многие программисты будут сильно сбиты с толку.Очевидно, что их код не выполняется в многопоточной среде.Зачем выбрасывать такое исключение, связанное с параллелизмом? При каких обстоятельствах это будет брошено? Давайте посмотрим поближе.

Аномальный рецидив

В Java, если операция удаления/добавления выполняется для некоторых элементов коллекции в цикле foreach, срабатывает отказоустойчивый механизм и генерируется исключение CMException.

Например, следующий код:

List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

for (String userName : userNames) {
    if (userName.equals("Hollis")) {
        userNames.remove(userName);
    }
}

System.out.println(userNames);

В приведенном выше коде используется расширенный цикл for для перебора элементов и попытки удалить из него строковый элемент Холлиса. Выполнение приведенного выше кода вызывает следующее исключение:

Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)
at java.util.ArrayList$Itr.next(ArrayList.java:859)
at com.hollis.ForEach.main(ForEach.java:22)

Точно так же читатели могут попытаться использовать метод add для добавления элементов в расширенный цикл for, и результат также вызовет это исключение.

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

Мы используемjadtool, декомпилируйте скомпилированный класс и получите следующий код:

public static void main(String[] args) {
    // 使用ImmutableList初始化一个List
    List<String> userNames = new ArrayList<String>() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    Iterator iterator = userNames.iterator();
    do
    {
        if(!iterator.hasNext())
            break;
        String userName = (String)iterator.next();
        if(userName.equals("Hollis"))
            userNames.remove(userName);
    } while(true);
    System.out.println(userNames);
}

Можно обнаружить, что foreach на самом деле реализуется с использованием цикла while и итератора.

Принцип аномалии

Через стек исключений приведенного выше кода мы можем отследить код, который фактически генерирует исключение:

java.util.ArrayList$Itr.checkForComodification(ArrayList.java:909)

Этот метод вызывается в методе iterator.next(). Посмотрим на реализацию этого метода:

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

Как и выше, в этом методе сравниваются значения modCount и expectModCount, и если они не хотят ждать, генерируется исключение CMException.

Итак, что такое modCount и ожидаемыйModCount? В чем причина того, что их ценности не хотят ждать?

modCount — это переменная-член в ArrayList. Он представляет собой количество фактических изменений коллекции.

List<String> userNames = new ArrayList<String>() {{
    add("Hollis");
    add("hollis");
    add("HollisChuang");
    add("H");
}};

Эта переменная доступна, когда коллекция инициализируется с помощью приведенного выше кода. Начальное значение равно 0.

ожидаемыйModCount — это внутренний класс в ArrayList — переменная-член в Itr.

Iterator iterator = userNames.iterator();

С помощью приведенного выше кода вы можете получить класс Itr, реализующий интерфейс Iterator.

ожидаемыйModCount представляет количество раз, когда этот итератор ожидает, что коллекция будет изменена. Его значение инициализируется при создании Itr. Это значение будет меняться только в том случае, если коллекция обрабатывается через итератор.

Затем давайте посмотрим, что делается в методе userNames.remove(userName); и почему значения ожидаемыхModCount и modCount отличаются.

Просматривая код, мы также можем обнаружить, что основная логика метода удаления выглядит следующим образом:

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 и ничего не делает с ожидаемым ModCount.

Просто нарисуйте картинку, чтобы описать описанный выше сценарий:

Если коротко, то причина, по которой выбрасывается CMException, заключается в том, что в нашем коде используется расширенный цикл for, а в расширенном цикле for обход коллекции осуществляется через итератор, но добавление/удаление элементов используется непосредственно собственными методами класса Collection . Это приводит к тому, что итератор обнаруживает, что элемент был удален/добавлен по незнанию, когда он проходит, и будет выдано исключение, чтобы напомнить пользователю, что могла произойти одновременная модификация!

Поэтому при использовании класса-коллекции Java, при возникновении CMException, приоритет должен отдаваться ситуации, связанной с отказоустойчивостью.На самом деле параллелизма здесь нет, но Итератор использует механизм защиты от отказоустойчивости, пока он находит определенный Если модификация не сделана сама по себе, будет выдано исключение.

О том, как решить эту проблему, мы рассказали в статье «Почему Alibaba запрещает операцию удаления/добавления элементов в цикле foreach», поэтому я не буду повторяться здесь.

fail-safe

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

Такой контейнер коллекции не имеет прямого доступа к содержимому коллекции во время обхода, а сначала копирует исходное содержимое коллекции и проходит по скопированной коллекции.

Контейнеры в пакете java.util.concurrent отказоустойчивы и могут использоваться и изменяться одновременно в нескольких потоках. Вы также можете добавить/удалить в foreach.

Проведем простой анализ отказоустойчивого класса коллекции CopyOnWriteArrayList.

public static void main(String[] args) {
    List<String> userNames = new CopyOnWriteArrayList<String>() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    userNames.iterator();

    for (String userName : userNames) {
        if (userName.equals("Hollis")) {
            userNames.remove(userName);
        }
    }

    System.out.println(userNames);
}

Приведенный выше код с использованием CopyOnWriteArrayList вместо ArrayList не вызовет исключения.

Все модификации коллекции отказоустойчивых коллекций выполняются путем копирования сначала копии, а затем коллекции копий, без непосредственного изменения исходной коллекции. И эти методы модификации, такие как добавление/удаление, контролируют параллелизм посредством блокировки.

Следовательно, итератору в CopyOnWriteArrayList не нужно выполнять быстрое обнаружение параллелизма во время процесса итерации. (Потому что основная цель отказоустойчивости — определить параллелизм, а затем уведомить пользователей через исключения)

Однако, несмотря на то, что преимущество контента на основе копирования заключается в том, чтобы избежать исключения ConcurrentModificationException, опять же, итератор не имеет доступа к измененному контенту. Например, следующий код:

public static void main(String[] args) {
    List<String> userNames = new CopyOnWriteArrayList<String>() {{
        add("Hollis");
        add("hollis");
        add("HollisChuang");
        add("H");
    }};

    Iterator it = userNames.iterator();

    for (String userName : userNames) {
        if (userName.equals("Hollis")) {
            userNames.remove(userName);
        }
    }

    System.out.println(userNames);

    while(it.hasNext()){
        System.out.println(it.next());
    }
}

После того, как мы получаем Iterator of CopyOnWriteArrayList, мы удаляем значения в исходном массиве прямо через цикл for и, наконец, выводим Iterator в конце, и мы обнаруживаем, что содержимое выглядит следующим образом:

[hollis, HollisChuang, H]
Hollis
hollis
HollisChuang
H

Итератор обходит копию коллекции, полученную в момент обхода, и итератор не знает о модификации исходной коллекции во время обхода.

Copy-On-Write

Узнав про CopyOnWriteArrayList, интересно, возникнет ли у вас такой вопрос: его add/remove и другие методы заблокированы, так зачем вам делать копию, а потом модифицировать? Перебор? Кроме того, это потокобезопасная коллекция. В чем разница между этой штукой и вектором?

Copy-On-Write, или COW, — это стратегия оптимизации, используемая в программировании. Основная идея заключается в том, что все с самого начала делятся одним и тем же контентом. Когда кто-то хочет изменить контент, он фактически копирует контент, чтобы сформировать новый контент, а затем изменить его. Это своего рода отсроченная лень. Стратегия.

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

Такие методы записи, как добавление/удаление в CopyOnWriteArrayList, должны быть заблокированы, чтобы избежать копирования N копий, что приводит к одновременной записи.

Однако метод чтения в CopyOnWriteArrayList не заблокирован.

public E get(int index) {
    return get(getArray(), index);
}

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

** Итак, контейнер CopyOnWrite — это своего рода идея разделения чтения и записи, чтение и запись разных контейнеров. ** И Vector использует один и тот же контейнер при чтении и записи, чтение и запись являются взаимоисключающими и могут выполнять только одно действие одновременно.