ОШИБКА, написанная Дугом Ли в сумке J.U.C, снова была обнаружена пользователями сети.

Java

Вот почему 69-я оригинальная статья

Описание ошибки

ОШИБКА JDK под номером 8073704 будет ссылаться на эту мою статью.

Это ссылка ниже.

https://bugs.openjdk.java.net/browse/JDK-8073704

Эта ошибка была исправлена ​​в выпуске JDK 9. То есть, если вы используете JDK 8, вы можете столкнуться с такой проблемой.

Давайте посмотрим, в чем проблема:

Этот баг говорит:Метод FutureTask.isDone возвращает значение true, если задача еще не завершена.

Как видите, это ОШИБКА уровня P4 (низкий приоритет), которая также назначена Дугу Ли, потому что класс FutureTask был написан им:

Реакция на национальную политику: кто загрязняет, тот и контролирует.

Брат Мартин, автор этой ошибки, описал ее следующим образом:

Ниже я дам вам перевод того, что он хочет выразить.

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

Если вы хотите понять, о чем он говорит, то я должен показать вам еще одну картинку, это описание документации FutureTask:

При просмотре описания ОШИБКИ, представленного братом Мартином, вы должны смотреть на диаграмму состояний и числа, соответствующие состоянию.

Он говорит, что метод FutureTask#isDone теперь записывается так:

Он считает, что из исходного кода, пока текущее состояние не равно NEW (т.е. не равно 0), он вернет true, указывая на то, что задача выполнена.

Он считает, что это должно быть написано так:

Цель этого письма заключается в том, чтобы помимо суждения о НОВОМ состоянии,Также оцениваются два промежуточных состояния: ЗАВЕРШЕНИЕ и ПРЕРЫВАНИЕ.

Таким образом, в дополнение к вышеуказанным трем состояниям осталось всего четыре состояния:

Эти четыре состояния могут представлять конечное состояние задачи.

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

state>COMPLETING, этому условию удовлетворяют только следующие состояния:

Из них только ПРЕРЫВАНИЕ является промежуточным состоянием, поэтому он исключил его следующим !=.

Таким образом, код становится кратким, но для его понимания требуется немного больше времени. Но смысл этих двух кодов абсолютно одинаков.

Ну описание этого бага такое.

Суммируя одно предложение, этот брат Мартин думает:

Метод FutureTask.isDone вернет значение true, если задача не была завершена, например COMPLETING и INTERRUPTING, что неверно. Это ошибка.

Только из кода статуса != NEW в исходном коде isDone, я думаю, что действительно нет проблем с тем, что сказал этот старый Мартин. Поскольку действительно есть два промежуточных состояния, этот исходный код не рассматривается.

Далее мы сосредоточимся на этом вопросе и взглянем на дискуссии великих богов.

открытое обсуждение

Во-первых, первым заговорил Пардип, через 13 дней после того, как был задан этот вопрос:

Я действительно не понимаю, в чем смысл ответа этого чувака.

Он сказал: «Нужно посмотреть описание метода isDone.

В описании сказано:Если задача завершена, вызов этого метода возвращает значение true. Помимо нормального завершения, завершение также может быть вызвано аномальной задачей или отменой задачи.Это считается завершением.

Я думаю, что его ответ немного несовместим с вопросом, и кажется, что ответ неверен.

Просто когда он выкидывает очко знаний о методе isDone.

Три дня спустя второго приятеля, который говорил, звали Пол, и он хотел сказать следующее:

Во-первых, он сказал, что нам не нужно проверять промежуточное состояние INTERRUPING.

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

Давайте посмотрим на метод isCancelled и метод get.

Затем давайте взглянем на метод isCancelled:

Непосредственно оценивается, является ли состояние большим или равным ОТМЕНЕННОМУ, то есть оценивается, является ли состояние одним из этих трех типов:

При оценке того, отменена ли задача (isCancelled), промежуточное состояние ПРЕРЫВАНИЯ не обрабатывается особым образом.

Согласно этой логике, при оценке того, завершена ли задача (isDone), нет необходимости выполнять специальную обработку для промежуточного состояния ПРЕРЫВАНИЕ.

Далее рассмотрим метод get.

Метод get в конечном итоге вызовет метод отчета:

Если переменная s (то есть состояние) имеет значение INTERRUPING (значение равно 5), а затем больше, чем состояние CANCELED (значение равно 4), создается исключение CancellationException (CE).

Поэтому он считает, что нет необходимости обнаруживать состояние INTERRUPING.

Потому что если вы вызовете метод isCancelled, он сообщит вам, что задача отменена.

Если вы вызовете метод get, будет выдано исключение CE.

Итак, подводя итог, я думаю, что логика приятеля Пола такова:

Как потребители, мы все заканчиваем тем, что вызываем метод get для получения результата, предполагая, что перед вызовом метода get. Мы используем isCancelled или isDone, чтобы судить о статусе задачи.

Если текущее состояние достаточно хорошее, чтобы умереть, это ПРЕРЫВАНИЕ. Тогда вызов isCancelled возвращает true, тогда по нормальной логике метод get дальше вызываться не будет.

Если вызов isDone, верните true и вызовите метод get.

Таким образом, в методе get гарантируется, что даже если он в настоящее время находится в промежуточном состоянии INTERRUPING, программа может сгенерировать исключение CE.

Поэтому Пол считает, что если нет необходимости определять состояние INTERRUPING, то мы можем изменить код с:

Упрощается до:

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

Он сказал: Если только я не пропустил что-то тонкое во взаимодействиях.

Разве что я не замечаю каких-то очень мелких деталей. Понимаете, искусство говорить. Все слова были закончены им.

Хорошо, Павел закончил свою речь. Через 42 минуты трубку взял маленький парень по имени Крис и сказал:

Я думаю, то, что сказал Пол, имеет смысл, и я согласен с его предложением.

Но да, я также думаю, что мы говорим об очень подробной, очень маленькой проблеме, я не знаю, даже если она будет написана так, это вызовет какие-то проблемы?

Написав здесь, позвольте мне дать вам краткое изложение:

  • Брат Мартин предложил ошибку и сказал, что метод FutureTask#isDone не оценивает два промежуточных состояния ПРЕРЫВАНИЕ и ЗАВЕРШЕНИЕ.
  • Пол сказал, что для состояния INTERRUPING вы можете обратиться к методу isCancelled, и никаких специальных суждений не требуется.
  • Брат Крис сказал, что то, что сказал Пол, было правильным.

Поэтому они подумали, что метод isDone следует изменить, чтобы он выглядел так:

Итак, теперь остается спорным только одно промежуточное состояние: ЗАВЕРШЕНИЕ.

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

Он придумал критику и прямо сказал: я думаю, что здесь все фигня.

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

На самом деле он сказал следующее: я не думаю, что какие-либо изменения необходимы.

Состояние ЗАВЕРШЕНИЕ — это эфемерное переходное состояние, которое означает, что у нас уже есть конечное состояние, но есть мгновенное состояние в промежутке времени между началом и концом конечного состояния, и это состояние ЗАВЕРШЕНИЕ.

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

Потому что переходное состояние COMPLETING не обнаруживается программой.

Ответ Дэвида Лянцзая был подтвержден боссом через два с половиной часа:

Дуг Ли сказал: Теперь исходный код написан таким образом намеренно, причина в том, что сказал Дэвид, и я думал об этом, когда писал его.

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

На самом деле, смысл Дага таков: если вы говорите, что есть проблема с этой частью, вы можете привести мне пример, не просто закончить теорию, вы можете показать мне код.

Через полчаса отправитель этой ошибки ответил:

Вы знаете, что означает преднамеренное?

Черт, мне снова приходится учить английский на полставки:

Он сказал: О, это было намеренно.

Это предложение, вы можете прочитать разные значения в разных тонах голоса.

Я склоняюсь здесь к тому, что он думает, что, поскольку Дуг учел это, когда впервые написал этот код, после анализа он почувствовал, что нет никакой проблемы в написании его таким образом, поэтому он написал его вот так.

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

Тогда не надо играть.

Вещи теперь кажутся качественными, то есть никаких доработок не требуется.

Но в этот момент одноклассник Павел убил карабин.Это должно быть предыдущее обсуждение, которое вдохновило его на размышления.Вы не говорите, что его нельзя обнаружить?Вы не говорите, что метод get может получить окончательный правильный результат ?

Тогда посмотрите, как выглядит мой код:

Код такой, вы можете вставить его напрямую и запустить в среде JDK 8/9:

public static void main(String[] args) throws Exception {

        AtomicReference<FutureTask<Integer>> a = new AtomicReference<>();
        Runnable task = () -> {
            while (true) {
                FutureTask<Integer> f = new FutureTask<>(() -> 1);
                a.set(f);
                f.run();
            }
        };

        Supplier<Runnable> observe = () -> () -> {
            while (a.get() == null);

            int c = 0;
            int ic = 0;
            while (true) {
                c++;
                FutureTask<Integer> f = a.get();
                while (!f.isDone()) {}
                try {
                    /*
                    Set the interrupt flag of this thread.
                    The future reports it is done but in some cases a call to
                    "get" will result in an underlying call to "awaitDone" if
                    the state is observed to be completing.
                    "awaitDone" checks if the thread is interrupted and if so
                    throws an InterruptedException.
                     */
                    Thread.currentThread().interrupt();
                    f.get();
                }
                catch (ExecutionException e) {
                    throw new RuntimeException(e);
                }
                catch (InterruptedException e) {
                    ic ++;
                    System.out.println("InterruptedException observed when isDone() == true " + c + " " + ic + " " + Thread.currentThread());
                }
            }
        };

        CompletableFuture.runAsync(task);
        Stream.generate(observe::get)
                .limit(Runtime.getRuntime().availableProcessors() - 1)
                .forEach(CompletableFuture::runAsync);

        Thread.sleep(1000);
        System.exit(0);
    }

Давайте сначала посмотрим на основную логику этого кода:

Первое место, обозначенное ①, — это два счетчика, c представляет количество раз первого цикла while, а ic представляет количество раз, когда было выдано InterruptedException (IE).

Место, помеченное ②, предназначено для определения того, находится ли текущая задача в завершенном состоянии, и если да, то продолжить вниз.

Место, помеченное ③, должно сначала прервать текущий поток, а затем вызвать метод get, чтобы получить результат задачи.

Место, отмеченное ④, означает, что если метод get выдает исключение IE, оно будет записано здесь, и журнал будет распечатан.

Следует отметить, что если журнал печатается, это указывает на проблему:

Очевидно, что метод isDone возвращает true, указывая на то, что выполнение метода завершено. Но когда я вызываю метод get, он выдает исключение IE?

Боюсь, вы не понимаете!

Позвольте мне сделать скриншот рабочего результата JDK 8.

Где выбрасывается это исключение?

На входе метод awaitDone сначала проверяет, не прерван ли текущий поток, и если он прерван, выбрасывается исключение IE:

И как код может выполняться в методе awaitDone?

Когда статус задачи меньше или равен COMPLETING.

В примере кода метод isDone в предыдущем цикле while вернул значение true, указывая на то, что текущее состояние определенно не является НОВЫМ.

Так что осталось?

Есть только одно состояние COMPLETING.

Демо, разве это не просто мониторинг?

Через 8 часов после того, как этот пример кода вышел, красавчик Дэвид снова заговорил:

То, что он хотел выразить, я понял так:

В пакете j.u.c очень часто сначала проверяется состояние прерывания потока, потому что, условно говоря, очень мало мест, которые вызывают прерывание потока.

Но этого не может быть из-за меньшего, мы не проверяем.

Нам еще предстоит выполнить на нем проверку приоритета, чтобы сообщить программе, был ли текущий поток прерван, то есть имеет ли смысл продолжать выполнение.

Однако в этом сценарии текущий поток прерывается, но это не указывает на завершение задачи задачи в Будущем. Это две не связанные вещи.

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

Поэтому его предложения по решению:

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

Пять дней спустя предыдущий отправитель BUG, ​​Мартин, снова пришел:

Он сказал, что передумал.

Передумать? Какова была его предыдущая идея?

После того, как Дуг сказал, что сделал это намеренно, Мартин сказал:

Это намеренно. О, это было намеренно.

На тот момент его идея была такова: большие ребята уже сказали, и это считалось так писать, и проблем точно нет.

Теперь его идея такова:Если метод isDone возвращает значение true, то метод get должен явно возвращать значение результата без создания исключения IE.

Следует отметить, что на данный момент описание ошибки изменилось.

от"Метод FutureTask.isDone вернет true, если задача не завершена."стал"Если метод isDone возвращает значение true, то метод get должен явно возвращать значение результата без создания исключения IE.".

Тогда Дэвид Лянзи дал самое простое решение:

Самое простое решение — сначала проверить статус, а затем проверить, не прерван ли текущий поток.

Затем эту ошибку исправил Мартин:

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

Пока он чинил, он также осторожно попросил Дуга благословить его и поддержать его.

Наконец, Мартин сказал, что отправил его на jsr166, который, как ожидается, будет исправлен в версии JDK 9.

Из любопытства я поискал имя Мартина в исходном коде JDK. Я думал, что это бронза, но я не ожидал, что это будет король. Неуважительно:

сравнение кода

Поскольку сказано, что ошибка исправлена ​​в JDK 9, давайте сравним код JDK 9/8.

java.util.concurrent.FutureTask#awaitDone:

Как видите, JDK 9 задерживает проверку прерывания на шаг.

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

Поскольку операция оценки COMPLETING в исходном коде выполняется перед оценкой флага прерывания потока:

Я не думаю, что мне нужно объяснять больше.

Затем скажите еще одно предложение в текущей FutureTask#awaitDone JDK 9. Там есть такая строка комментария:

В нем говорится: метод isDone уже сообщает пользователю, что задача выполнена, поэтому мы не должны ничего возвращать или выдавать исключение IE при вызове метода get.

Эта строка комментариев хочет выразить то, что мы обсуждаем в ОШИБКЕ в предыдущем разделе. Человек, написавший эту строку комментариев, — одноклассник Мартин.

Когда я разобрался в подноготной этого бага, и вдруг увидел этот комментарий в исходниках JDK 9, у меня возникло очень волшебное чувство.

Это своего рода исходный код, говорящий: ты меня чувствуешь?

Я сразу понял: я тебя понял.

Хорошо.

ложное пробуждение

Этот термин также есть в комментариях JDK 9:

ложное пробуждение, ложное пробуждение.

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

Если вам не нужно использовать такие методы, как ожидание и уведомление в вашем коде.

О, нет, это может быть использовано во время интервью.

Что такое «ложное пробуждение», позвольте мне показать вам пример:

Метод java.lang.Thread#join(длинный):

Зачем использовать здесь цикл while вместо прямого использования if?

Потому что в теле цикла есть вызов метода ожидания.

Почему я должен использовать цикл while при вызове метода ожидания?

Не спрашивайте, спрашивайте, чтобы предотвратить ложные пробуждения.

Взгляните на javadoc метода ожидания:

Поток может проснуться без уведомления, прерывания или тайм-аута, так называемые «ложные пробуждения». не Продолжать ждать, если удовлетворено, чтобы предотвратить ложные пробуждения.

Поэтому рекомендуется писать так:

В методе соединения метод isAlive является условием, которое здесь не выполняется.

В книге "Эффективная Java" также есть место, где упоминается "ложное пробуждение":

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

Вот еще один вопрос для интервью:Почему метод ожидания должен выполняться внутри цикла while?

Теперь вы можете ответить на этот вопрос.

Так много было сказано о «ложном пробуждении», и заинтересованные студенты могут пойти и узнать больше об этом.

Яма Нетти

Говоря о FutureTask JDK, почему он вдруг обратился к Netty?

Потому что в Netty была допущена базовая логическая ошибка в реализации его основного интерфейса Future, что нарушало контракт JDK при реализации методов cancel и isDone.

Эта ошибка также удивила авторов Netty.

Сначала посмотрите на описание метода отмены в интерфейсе JDK Future:

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html

В описании метода документации говорится:Если вызывается метод отмены, то вызов isDone всегда будет возвращать значение true.

Взгляните на этот тестовый код:

Видно, что после вызова метода отмены вызов метода isDone снова возвращает false.

Я видел этот момент в этой статье на Zhihu давно, и он был немного связан с содержанием, обсуждаемым в этой статье, поэтому я снова перевернул его и сказал больше.

Кому интересно, могут посмотреть:«Ошибка, которая удивила авторов Нетти»

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

Спасибо за прочтение, настаиваю на оригинальности, очень приветствую и благодарю за внимание.