Как сделать иммутабельный класс java

Добавил пользователь Дмитрий К.
Обновлено: 02.09.2024

Immutable.js и его практика в проекте React + Redux

Введение автора: Цзоу Гунъи (Zou Gongyi), интерфейсный инженер Meituan Dianping, имеет 5-летний опыт веб-разработки, теперь является членом команды заказов Meituan Dianping.

Предисловие

В этой статье в основном рассказывается о библиотеке классов immutable.js, запущенной facebook, и о том, как интегрировать immutable.js в существующий мобильный проект нашей команды с архитектурой react + redux.

Эта статья относительно длинная (около 5000 слов), рекомендуемое время чтения:20 min

Прочитав эту статью, вы сможете узнать:

  • Что такое immutable.js и какие проблемы он может решить?
  • Особенности immutable.js и использование api
  • Что можно улучшить в проекте redux + react, добавив immutable.js
  • Как интегрировать immutable.js в React + Redux
  • Сравнение данных до и после интеграции
  • Некоторые примечания по использованию immutabe.js

оглавление

  • 1. immutable.js
    • 1.1 Основы исходных ссылочных типов js
    • 1.2 Введение в immutable.js
      • 1.2.1 Постоянная структура данных (постоянная структура данных)
      • 1.2.2 структурное разделение
      • 1.2.3 поддержка ленивых операций (ленивых операций)
      • 2.1 Статус заказа проекта H5 до внедрения immutable.js
      • 2.2 Как интегрировать immutableJS в проект React + Redux
        • 2.2.1 Четкий план интеграции, определение границ
        • 2.2.2 Конкретный метод реализации кода интеграции

        1.1 Основы исходных ссылочных типов js

        Сначала рассмотрим следующие два сценария:

        Я считаю, что два вышеупомянутых сценария очень распространены в процессе разработки в будние дни. Я считаю, что всем известны конкретные причины. Я не буду здесь вдаваться в подробности. Обычно решение проблемы этого типа состоит в том, чтобы скопировать новый объект путем поверхностного или глубокого копирования. Это делает новый объект и старый адрес ссылки на объект разными.
        В js преимущество данных ссылочного типа заключается в том, что частые манипуляции с данными модифицируются на основе исходного объекта, и новые объекты не будут создаваться, так что память может эффективно использоваться без потери памяти. Эта функция называется изменяемой (может Change), но его преимущества являются также его недостатками. Он слишком гибкий и изменчивый, что также приводит к его неконтролируемости в сложных сценариях данных. Предположим, объект используется в нескольких местах и случайно изменен в одном месте. Данные. Трудно предсказать, как данные будут меняться в других местах. Решение этой проблемы, как правило, аналогично приведенному сейчас в примере. Вам нужно скопировать новый объект, а затем изменить новый объект. Это, несомненно, вызовет дополнительные Проблемы с производительностью и трата памяти.
        Для решения этой проблемы появились неизменяемые объекты: каждый раз, когда неизменяемый объект изменяется, создается новый неизменяемый объект, а старый объект не изменяется.

        1.2 Введение в immutable.js

        В настоящее время существует множество js-библиотек, реализующих неизменяемую структуру данных, и immutable.js является одной из наиболее распространенных библиотек.

        Immutable.js создан Facebook и является одной из самых популярных реализаций неизменяемых структур данных. Он реализует полностью постоянную структуру данных с нуля и использует передовые технологии, такие как попытки достичь разделения структуры. Все операции обновления будут возвращать новые значения, но внутренняя структура является общей, чтобы уменьшить использование памяти (и сделать сборку мусора недействительной).

        Есть три основных особенности immutable.js:

        • Постоянная структура данных (постоянная структура данных)
        • структурное разделение
        • поддерживать ленивую операцию (ленивую операцию)

        Давайте подробно рассмотрим эти три функции по порядку:

        1.2.1 Постоянная структура данных (постоянная структура данных)

        В общем, говоря о настойчивости, первая реакция в программировании должна заключаться в том, что данные где-то существуют и могут быть использованы непосредственно из этого места, когда это необходимо.
        Но упомянутая здесь настойчивость - это еще одно значение, используемое для описания структуры данных, которая очень распространена в общем функциональном программировании. Она относится к данным, которые все еще могут поддерживать состояние до модификации при ее изменении. По сути, это Этот тип данных является неизменным типом, который является неизменным.
        Immutable.js предоставляет более десяти неизменяемых типов (List, Map, Set, Seq, Collection, Range и т. Д.)
        На этом этапе некоторые студенты могут почувствовать, что это отличается от копии, упомянутой ранее, и что это также затраты на создание нового объекта каждый раз. Хорошо, тогда вторая функция откроет ваши сомнения.

        1.2.2 структурное разделение

        (Изображение из Интернета)

        Immutable использует расширенную технологию попыток (дерево словаря) для достижения структурного разделения для решения проблем с производительностью. Когда мы работаем с неизменяемым объектом, ImmutableJS будет клонировать только узел и его узел-предок, а остальные останутся неизменными, так что то же самое можно разделить Деталь значительно повышает производительность.

        Давайте посмотрим на испытания (дерево словарей), давайте рассмотрим пример




        (Картинка из интернета)
        На рисунке 1 показан объект объекта древовидной структуры словаря, вершина - корневой узел, а каждый дочерний узел имеет уникальный идентификатор (в immutable.js это хэш-код).
        Предполагая, что теперь мы берем значение data.in в соответствии с путем, отмеченным i и n, мы можем найти узел, содержащий 5. Можно видеть, что data.in = 5, нет необходимости проходить весь объект
        Итак, теперь нам нужно изменить data.tea с 3 на 14, как это сделать?
        Вы можете видеть зеленую часть рисунка 2. Вам не нужно обходить все дерево, просто начните с корня.
        При фактическом использовании вы можете создать новую ссылку, как показано на рисунке 3. Data.tea создает новый узел, а другие узлы совместно используют старый объект, но старый объект остается неизменным.
        Благодаря этой функции при сравнении двух объектов, если их хэш-код одинаков, их значение одинаково, что позволяет избежать глубокого обхода

        1.2.3 поддержка ленивых операций (ленивых операций)

        • Ленивая операция Seq
        • Особенность 1: неизменяемый (неизменяемый)
        • Особенность 2: ленивый (ленивый, отложенный)

        Это очень интересная функция. Что здесь подразумевается под ленивым? Это сложно описать словами, смотрим демку, вы поймете, прочитав

        Смысл этого кода в том, что массив сначала принимает нечетное число, затем возводится в квадрат основание, а затем второе число в console.log. Тот же код реализован с неизменяемым объектом seq. Фильтр выполняется только 3 раза, но исходный Был исполнен 8 раз.
        Фактически, принцип заключается в том, что блок кода объекта, созданного с помощью seq, не выполняется, а объявляется. Код будет фактически выполнен, когда get (1). После того, как число index = 1, следующее не будет выполнено. Он выполняется снова, поэтому в фильтре требуемое число выбирается в третий раз, и оно не будет выполняться повторно с 4-8
        Подумайте об этом, если в реальном бизнесе объем данных очень велик. Например, в нашем бизнесе заказов список меню продавца может содержать сотни блюд, а длина массива - сотни. Для работы с таким массивом, если приложение ленивое Характеристики работы значительно сэкономят производительность

        1.3 Введение в часто используемые API

        Выше перечислены только некоторые распространенные методы, см.Официальный api сайта:facebook.github.io/immutable-j…
        immutablejs также содержит много синтаксического сахара, похожего на подчеркивание.После использования immutable.js вы можете полностью удалить lodash или подчеркивание из проекта.

        1.4 Преимущества и недостатки immutable.js

        преимущество:

        • Уменьшите сложность, вызванную изменяемым
        • Экономить память
        • Историческая прослеживаемость (путешествие во времени): путешествие во времени означает, что значение каждого момента сохраняется. На какой шаг вы хотите вернуться, вы можете просто извлечь данные. Подумайте об этом, если на текущей странице есть операция отмены. Данные до отзыва сохраняются, просто удалите их. Эта функция особенно полезна при сокращении или изменении
        • Примите функциональное программирование: неизменность - это концепция функционального программирования. Характерной чертой чистого функционального программирования является то, что до тех пор, пока ввод согласован, вывод должен быть согласованным. По сравнению с объектно-ориентированным, удобнее разрабатывать компоненты и отлаживать

        Недостатки:

        • Нужно переучивать api
        • Увеличенный размер пакета ресурсов (около 5000 строк исходного кода)
        • Легко спутать с нативными объектами: поскольку API-интерфейсы отличаются от нативных, легко ошибиться, если они смешаны.

        Я уже столько рассказывал раньше, но на самом деле хочу подчеркнуть этот важный момент. В этой главеВ сочетании с практикой группы проверки порядка в реальном проекте дается улучшение производительности проекта react + redux до и после использования immutable.js.

        2.1 Статус заказа проекта H5 до внедрения immutable.js

        В настоящее время в проекте используется react + redux. Из-за непрерывной итерации проекта и увеличения сложности требований структура состояний, поддерживаемая в redux, становится все больше и больше, и это уже не простые мозаичные данные. Например, в состоянии страницы меню будет три или четыре уровня объектов. Что касается вложенности массивов, мы знаем, что объект и массив в JS являются ссылочными типами.В процессе непрерывной работы после изменения состояния нескольких действий исходное сложное состояние стало неконтролируемым, что привело к изменению состояния. Многие компоненты, которые не изменили свое состояние, подвергаются повторной визуализации. Как показано ниже

        Порекомендуйте здесь response-addons-perf, инструмент индикатора производительности для реакции
        Если вы раньше не использовали этот инструмент для просмотра, кто-то спросит вас, какие компоненты будут вызваны изменением простой кнопки питания / извлечения на изображении для повторного рендеринга, вы можете ответить, что есть только компонент метода обеда.
        Но когда вы на самом деле используете для проверки response-addons-perf, вы обнаружите, что WTF? ? ! Одна операция вызвала повторный рендеринг стольких нерелевантных компонентов? ?
        По какой причине? ?

        shouldComponentUpdate

        Здесь мы определенно не можем использовать глубокое сравнение каждый раз, когда мы сравниваем, для обхода всех структур, затраты на производительность огромны, только сейчас мы сказали, что в immutable.js есть функция сравнения ссылок (хэш-код), эта функция Идеально подходит для сцены

        2.2 Как интегрировать immutableJS в проект React + Redux

        2.2.1 Четкий план интеграции, определение границ

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

        Из вышеизложенного видно, что почти весь проект должен использовать неизменяемые, и только несколько мест, которые взаимодействуют с внешними зависимостями, используют собственные js.
        Цель этого на самом деле состоит в том, чтобы предотвратить смешивание нативных js и неизменяемых в больших проектах, в результате чего сам кодер не знает, какой тип данных хранится в переменной.
        Кто-то может подумать, что это выполнимо в совершенно новом проекте, но в существующем зрелом проекте, чтобы изменить все переменные на immutablejs, количество изменений кода и инвазивность очень велики, а риск также высок. . Затем они подумали бы об изменении состояния в редукторе с fromJS () на неизменяемое для операций с состоянием, а затем с помощью toJS () преобразовать его обратно в собственный js, чтобы состояние можно было отслеживать без изменений. Стоимость кода, отличного от редуктора, очень мала.

        Два вопроса:

        1. fromJS () и toJS () - это углубленное взаимное преобразование неизменяемых объектов и собственных объектов, которые дороги в производительности, поэтому старайтесь не использовать их (см. конкретное сравнение в следующей главе)
        2. Свойства и состояние в компоненте по-прежнему являются родными js, shouldComponentUpdate по-прежнему не может проводить глубокие сравнения, используя преимущества immutablejs.

        2.2.2 Конкретный метод реализации кода интеграции

        redux-immutable

        В redux первым шагом должно быть использование combReducers для слияния редукторов и инициализации состояния. CombineReducers, который поставляется с redux, поддерживает только собственную форму js, поэтому здесь нам нужно использовать combReducers, предоставляемые redux-immutable, чтобы заменить исходный метод

        InitialState в reducer также должен быть инициализирован неизменным типом

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

        Исходные собственные переменные js на странице необходимо преобразовать в неизменяемые типы, а не перечислять их все.

        Пакет ajax для взаимодействия на стороне сервера

        Во внешнем коде используется неизменяемый, но данные, отправленные сервером, по-прежнему являются json, поэтому их необходимо инкапсулировать в ajax, а данные, возвращаемые сервером, преобразуются в неизменяемые.

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

        shouldComponentUpdate

        Высший приоритет!Я уже много рассказывал о том, почему мы должны использовать неизменяемый объект для преобразования shouldComponentUpdate. Я не буду говорить об этом здесь. Давайте посмотрим, как его преобразовать.
        Есть много способов инкапсулировать shouldComponentUpdate. Здесь мы выбрали базовый класс, который инкапсулирует слой компонента. Базовый класс обрабатывает shouldComponentUpdate единообразно, а компонент напрямую наследует базовый класс.

        Если вам нужно использовать унифицированный пакет shouldComponentUpdate в компоненте, наследуйте напрямую базовый класс

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

        2.3 Сравнение до и после оптимизации проекта H5

        1. FromJS и toJS будут глубоко преобразовывать данные, и накладные расходы будут большими. По возможности избегайте их использования. Используйте Map () и List () для однослойного преобразования данных.

        (Проведено простое сравнение производительности fromJS и Map. В одних и тех же условиях используются два метода для обработки 1 000 000 частей данных соответственно. Видно, что стоимость fromJS в 4 раза выше стоимости Map)

        2.js - слабый тип, но ключ типа Map должен быть строкой! (См. Описание официального сайта ниже)

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

        4. После введения immutablejs код для копирования массива объектов больше не должен появляться (например)

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

        6. Неизменяемый объект можно напрямую передать в JSON.stringify (), без необходимости вручную вызывать toJS () для собственного

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

        8. Чтобы увидеть реальное значение неизменяемой переменной во время отладки, вы можете добавить точки останова в Chrome и использовать метод .toJS () в консоли для просмотра

        В общем, появление immutable.js решило многие болевые точки нативного js, и было сделано много оптимизаций с точки зрения производительности, и immuable.js, как продукт, запущенный одновременно с react, идеально подходит для response + redux Обработка потока состояний, цель redux - единый поток данных, который можно отследить. Эти две точки как раз и являются преимуществами immutable.js. Естественно, почему бы не сделать это.
        Конечно, не во всех сценариях, в которых используется react + redux, необходимо использовать immutable.js. Рекомендуется соблюдать принцип, согласно которому проект достаточно велик, а структура состояний достаточно сложна. Небольшие проекты могут обрабатывать shouldComponentUpdate вручную. Не рекомендуется использовать его.

        Интеллектуальная рекомендация

        UIWebView-OC и взаимодействие JS

        1. Перехватить указанный URL-адрес в прокси-методе webView. 2. По перехваченному URL-адресу определите специальное поле, указанное в URL-адресе, для обработки соответствующего события. 3. Передайте ис.

        Как можно сделать класс Java неизменным, какова потребность в неизменности и есть ли какое-то преимущество в использовании этого?

        Что такое неизменяемый объект?

        Неизменяемый объект - это тот, который не изменит состояние после его создания.

        Как сделать объект неизменным?

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

        Следующий класс создаст неизменяемый объект:

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

        Однако необходимо следить за тем, чтобы все объекты, на которые ссылается объект, также должны быть неизменными, или можно было бы изменить состояние объекта.

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

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

        Один из способов обойти эту проблему - вернуть копию массива или коллекции при вызове из получателя:

        В чем преимущество неизменности?

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

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

        почему необходим Шаг 3? Почему я должен отмечать класс final ?

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

        теперь предположим, что я сделаю следующее:

        обратите внимание, что в моем Mutable подкласс, я переопределил поведение getValue для чтения нового изменяемого поля, объявленного в моем подклассе. В результате ваш класс, который изначально выглядит неизменным, на самом деле не является неизменным. Я могу пройти это. Mutable объект, где Immutable ожидается объект, который может сделать очень плохие вещи для кода, предполагая, что объект действительно неизменен. Маркировка базового класса final предотвращает это.

        надеюсь, что это помогает!

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

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

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

        соответствие контракту вашего суперкласса не является чем-то, что может или должно всегда применяться компилятором. компилятор может применять определенные аспекты вашего контракта (например: минимальный набор методов и их сигнатур типа), но есть много частей типичных контрактов, которые не могут быть применены компилятором.

        неизменяемость является частью контракта класса. Это немного отличается от некоторых вещей, к которым люди больше привыкли, потому что в нем говорится, что класс (и все подклассы) не могу do, в то время как я думаю, что большинство программистов Java (и вообще ООП) склонны думать о контрактах как о том, что класс можете сделать, не то, что он не могу do.

        неизменяемость также влияет не только на один метод - она влияет на весь экземпляр - но это не сильно отличается от способа equals и hashCode в работе Java. Эти два метода имеют конкретный контракт, изложенный в Object . Этот контракт очень тщательно излагает вещи, которые эти методы не может do. Этот контракт делается более конкретным в подклассах. Это очень легко переопределить equals или hashCode таким образом, что нарушает договор. Фактически, если вы переопределяете только один из этих двух методов без другого, есть вероятность, что вы нарушаете контракт. Так должно быть equals и hashCode были объявлены final на Object чтобы избежать этого? Думаю, большинство согласится, что не должны. Точно так же нет необходимости делать неизменяемые классы final .

        тем не менее, большинство ваших классов, неизменных или нет, вероятно должны be final . См. Эффективное Второе Издание Java пункт 17: "дизайн и документ для наследования или запретить его".

        таким образом, правильной версией вашего шага 3 будет: "Сделайте класс окончательным или, при проектировании для подклассы, четко документируют, что все подклассы должны оставаться неизменными."

        не отмечайте весь финал класса.

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

        лучше отметить свои свойства частными и окончательными, и если вы хотите защитить "контракт", отметьте своих геттеров как окончательные.

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

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

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

        Это ограничивает другие классы, расширяющие ваш класс.

        Final класс не может быть расширен другими классами.

        Если класс расширяет класс, который вы хотите сделать как неизменяемый, он может изменить состояние класса из-за принципов наследования.

        просто уточните "это может измениться". Подкласс может переопределить поведение суперкласса, например, с помощью переопределения метода (например, templatetypedef/ TED Hop answer)

        для создания неизменяемого класса не обязательно отмечать класс как окончательный.

        позвольте мне взять один из таких примеров из классов java сам класс "BigInteger" является неизменяемым, но не окончательным.

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

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

        неизменяемость означает, что нет никакого способа изменить состояние объекта после его создания, и это достигается тремя правилами thumb, которые заставляют компилятор распознавать, что класс является неизменяемым, и они следующие: -

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

        для получения дополнительной информации см. ниже URL

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

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

        предположим, что следующий класс не был final :

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

        теперь объект, который присваивается переменной типа Foo больше не гарантируется mmutable. Это может вызвать проблемы с такими вещами, как хеширование, равенство, параллелизм и т. д.

        дизайн сам по себе не имеет никакой ценности. Дизайн всегда используется для достижения цели. Какова здесь цель? Хотим ли мы уменьшить количество сюрпризов в коде? Хотим ли мы предотвратить ошибки? Мы слепо следуем правилам?

        имея это в виду, вам нужно найти ответы на эти вопросы:

        1. сколько очевидных ошибок это предотвратит?
        2. сколько тонких ошибок это предотвратит?
        3. как часто это сделает другой код более сложным (=более подверженным ошибкам)?
        4. это делает тестирование проще или сложнее?
        5. насколько хороши разработчики в вашем проекте? Сколько руководства с кувалдой им нужно?

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

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

        Если вы правильно используете интерфейсы, вы можете избежать "I нужно сделать эту изменяемую " проблему, всегда используя интерфейс, а затем добавляя изменяемую реализацию, когда возникает необходимость.

        вывод: для этого ответа нет" лучшего " решения. Это зависит от того, какую цену вы готовы и которую вы должны заплатить.

        значение по умолчанию equals () совпадает с ссылочным равенством. Для неизменяемых типов данных, это почти всегда неправильно. Таким образом, вы должны переопределить метод equals (), заменив его собственной реализацией. ссылке


        Мне часто приходится объяснять почему тот или иной код иногда может не работать. Чтобы доходчиво объяснить почему это происходит, и что надо сделать, чтобы он заработал, желательно понимать основные принципы модели памяти Java. Когда заходит речь о Java Memory Model (JMM), я считаю бесчеловечным отправлять человека читать спецификацию, так как там черт ногу сломит. На самом деле JMM очень хорошо описана в книге Brian Goetz "Java Concurrency in Practice", но, во-первых, её надо иметь, а, во-вторых, там тоже нужно прочитать не одну страничку. Поэтому я и решил описать вкратце JMM, чтобы всегда можно было сюда сослаться.


        Две основные вещи, которые вводят энтропию в многопоточный код - это reordering и visibility.

        Видимость (visibility)

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

        Reordering

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

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

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

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

        Happend-before

        В Java Memory Model введена такая абстракция как happens-before. Она обозначает, что если операция X связана отношением happens-before с операцией Y, то весь код следуемый за операцией Y, выполняемый в одном потоке, видит все изменения, сделанные другим потоком, до операции X.

        • В рамках одного поток любая операция happens-before любой операцией следующей за ней в исходном коде
        • Освобождение лока (unlock) happens-before захват того же лока (lock)
        • Выход из synhronized блока/метода happens-before вход в synhronized блок/метод на том же мониторе
        • Запись volatile поля happens-before чтение того же самого volatile поля
        • Завершение метода run экземпляра класса Thread happens-before выход из метода join() или возвращение false методом isAlive() экземпляром того же треда
        • Вызов метода start() экземпляра класса Thread happens-before начало метода run() экземпляра того же треда
        • Завершение конструктора happens-before начало метода finalize() этого класса
        • Вызов метода interrupt() на потоке happens-before когда поток обнаружил, что данный метод был вызван либо путем выбрасывания исключения InterruptedException, либо с помощью методов isInterrupted() или interrupted()

        Очень важный момент: как освобождение/захват монитора, так и записать/чтение в volatile переменную связаны отношением happens-before, только если операции проводятся над одним и тем же экземпляром объекта.

        Так же важно понимать, что в отношении happens-before участвуют только два потока, о видимости и reordering остальных потоков ничего сказать нельзя, пока в каждом из них не наступит отношение happens-before с к другим потоком.

        Еще в отношении happens-before есть очень большой дополнительный бонус: данное отношение дает не только видимость volatile полей или результатов операций защищенных монитором или локом, но и видимость вообще всего-всего, что делалось до события hapens-before.


        Так на рисунке выше ThreadB гарантированно увидит изменение в поле y, сделанное ThreadA, хотя он не является volatile и запись в него происходит вне synchronized блока.

        Вышеописанные свойства могут достигаться JVM следующим образом: в момент отпускания монитора (записи в volatile переменную и дальше по списку) все регистры и локальные кэши процессора синхронизируются с основной памятью, а в момент последующего захвата лока (чтения volatile переменной и т.д.) процессор на котором выполняется второй поток инвалидирует свой кэш и зачитывает все последние данные из основной памяти. Почему же тогда надо обязательно синхронизироваться на один и тот же монитор, спросите вы? Да потому, что только в этом случае будет гарантироваться строгий порядок, т.е. второй поток гарантировано сбросит свой кэш, только после того как первый синхронизирует свой с основной памятью.

        Отношение happens-before так же накладывает сильные ограничения на reordering. С точки зрения потока Y все операцие произошедшие до точки happens-before в потоке X он может рассматривать как операции свершившиеся в своем собственном потоке. Т.е. никакого логического reordering по сравнению с прямым порядком в исходном коде с точки зрения потока Y быть не может.

        Если взглянуть внимательнее на границу happens-before с точки зрения reordering для потока Y, то никакие операции располагающиеся выше границы happens-before в потоке X, не могут выполнится ниже границы happens-before в результате reordering, однако, операциям, находящимся ниже границы, разрешено выполнение до неё. Более наглядно это изображено на рисунке.


        Публикация объектов

        Публикацией объектов называется явление, когда один поток создает объект и присваивает на него ссылку какому-нибудь полю, которое может увидеть второй поток. Если запись в это поле первым потоком, разделена со чтением этого поля вторым потоком отношением happens-before, то публикация называется безопасной, т.е. второй поток увидит все поля опубликованного объекта, инициализированные первым потоком.

        Однако, есть еще один способ добиться безопасной публикации объектов: если ссылка на объект, все поля которого являются final, становится видимой любому потоку, то данный поток видит все final поля, инициализированные во время создания объекта. Более того он будет видеть все значения достижимые из final полей. Рассмотрим пример ниже:
        Если кроме как ссылок final на ваши объекты никто не ссылается, то не зависимо от уровня вложенности, поток, который видит ссылку на опубликованный объект, увидит все значения достижимые через final поля, которые были выставлены в конструкторе. Так в примере выше, любой поток, успешно зашедший в метод number (а это значит, что он увидел ссылку на опубликованный объект), то он всегда вернет значение 2. Конечно при условии, что после конструктора содержимое всех объектов больше не модифицируется.

        Данное замечательное свойство делает антипаттерн double-check locking работоспособным, если singleton в конструкторе инициализирует только final поля.

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

        Еще очень важный момент, который стоит упомянуть, это то, что вышеописанное верно только для объектов, во время конструирования которых, ссылка на объект не покидет конструктор, прежде чем он завершен. Обычно это происходит, когда вы передаете ссылку на экземпляр вашего класса в какой-нибудь listener прямо в конструкторе объекта. Или ваш объект является экземпляром Thread и вы вызываете метод start в конструкторе.

        Как же данное свойство может быть реализовано JVM? Я думаю, что JVM гарантирует, что в момент записи ссылки на объект в основную память, все значения, достижимые из final полей выставленных во время конструирования объектов, уже синхронизированы с основной памятью. Таким образом, любой поток, читая ссылку на объект обязательно прочитает и все значения выставленные описанным способом во время конструирования объекта.

        Reflection

        На сколько я слышал от одного из сотрудников Oracle на стенде JavaSE на прошедшей javaone в Москве, изменение final поля через reflection и последующем его последующее чтение происходит через барьер памяти, поэтому и в этом случае можно не беспокоится о безопасном доступе к final полям из других потоках. На самом деле для меня это звучит немного странно и непонятно. Руслан, который слышал об этом эффекте вместе со мной, похоже тоже об этом долго у думал и в результате родил следующий пост.

        Статическая инициализация

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

        Атомарность записи-чтения полей

        JMM гарантирует атомарность записи-чтения всех не long/double полей. А volatile - абсолютно всех полей. Поля, представляющие ссылки на объекты, тоже всегда пишутся-читаются атомарно. Понятно дело, вместе с этим спецификация не запрещает иметь атомарность записи чтения long\double полей для 64-битной виртуальной машины. Данная атомарность гарантирует, что любой поток в любой момент времени зачитает из поля либо значение по умолчанию, либо полное значение, записанное туда в некий момент времени, и никогда не найдет там какого-то мусора.

        Читайте также: