Оригинальная статья, краткое изложение опыта и жизненные перипетии на всем пути от набора в школу до фабрики А
Нажмите, чтобы узнать подробностиwww.codercc.com
1. Введение в финал
финал можно изменитьПеременные, методы и классы, используемый для указания того, что измененное содержимое не будет изменено после его назначения, например, класс String является окончательным классом типа. Даже если я могу знать конкретное использование final, я хотел быОкончательная проблема переупорядочения в многопоточностиЭто также легко игнорировать, я надеюсь, что мы сможем обсудить это вместе.
2. Конкретные сценарии использования final
Final может модифицировать переменные, методы и классы, то есть сфера использования final в основном охватывает все места в java.Следующие позиции изменяются блокировками: переменные, методы и классы.
2.1 Переменные
Переменные в java можно разделить наПеременные-членыи методлокальная переменная. Следовательно, это также говорится таким образом, чтобы не пропустить ни одного мертвого угла.
2.1.1 окончательные переменные-члены
Обычно переменные-члены в каждом классе можно разделить наПеременные класса (статические измененные переменные) и переменные экземпляра. Время присвоения начальных значений этим двум типам переменных отличается.Переменным класса могут быть присвоены начальные значения непосредственно при объявлении переменных или начальные значения могут быть присвоены переменным класса в статических блоках кода. Переменным экземпляра можно присваивать начальные значения при объявлении переменных, в нестатических блоках инициализации и в конструкторах. переменная класса имеетДва случая присвоить начальные значения, в то время как переменные экземпляра могут иметьТри времени для назначения начальных значений. Когда конечная переменная не инициализирована, система не будет выполнять неявную инициализацию, и произойдет ошибка. Это все еще относительно абстрактно, и для демонстрации используется следующий конкретный код. (Код охватывает все возможные случаи окончательных измененных переменных, и будет полезно терпеливо наблюдать за ним :))
Глядя на картинку выше, каждая ситуация была разобрана, способ снятия скриншотов здесь также чувствуется, что красная метка ошибки в IDE может объяснить ситуацию более четко. Теперь подытожим эти ситуации:
- переменная класса: должен быть встатический блок инициализацииУкажите начальное значение в илиПри объявлении переменной классаУкажите начальное значение, и только здесьдва местаодин из назначенных;
- переменная экземпляра: необходимонестатический блок инициализатора,объявить переменную экземпляраили вв конструктореУкажите начальное значение, и только здесьтри местауказать.
2.2.2 конечные локальные переменные
Конечная локальная переменная явно инициализируется программистом.Если конечная локальная переменная была инициализирована, ее нельзя изменить снова.Если конечная переменная не инициализирована, ее можно присвоить.если и только один разНазначение, назначенное один раз, а затем назначенное снова, приведет к ошибке. В следующем коде используется специальный код для демонстрации ситуации с конечными локальными переменными:
А теперь давайте посмотрим на это под другим углом: есть ли разница между базовыми типами данных и ссылочными типами, модифицированными final?
окончательный примитивный тип данных и окончательный ссылочный тип данных
Из вышеприведенного примера мы видели, что если окончательная модификация является данными базового типа данных, после того, как она будет присвоена, она не может быть изменена снова.Тогда, если окончательная модификация является ссылочным типом данных? Можно ли изменить этот ссылочный объект? Давайте также посмотрим на кусок кода.
public class FinalExample {
//在声明final实例成员变量时进行赋值
private final static Person person = new Person(24, 170);
public static void main(String[] args) {
//对final引用数据类型person进行更改
person.age = 22;
System.out.println(person.toString());
}
static class Person {
private int age;
private int height;
public Person(int age, int height) {
this.age = age;
this.height = height;
}
@Override
public String toString() {
return "Person{" +
"age=" + age +
", height=" + height +
'}';
}
}
}
Когда мы изменим атрибут окончательной модифицированной переменной типа ссылочных данных person на 22, с ним можно будет успешно работать. Благодаря этому эксперименту мы можем видеть, чтоКогда final изменяет переменную базового типа данных, переменная базового типа данных не может быть переназначена, поэтому переменная базового типа данных не может быть изменена. Для переменных ссылочного типа сохраняет только ссылку, final только гарантирует, что адрес, на который ссылается переменная ссылочного типа, не изменится, то есть на объект ссылаются всегда, но свойства объекта могут быть изменены.
макропеременная
Используя неизменяемость конечных переменных, при выполнении следующих трех условий переменная станет «макропеременной», то есть константой.
- Модифицируется модификатором final;
- Начальное значение указывается при определении конечной переменной;
- Это начальное значение может быть однозначно указано во время компиляции.
Примечание. Когда макропеременная используется в другом месте программы, компилятор напрямую заменяет ее значением переменной.
2.2 Методы
переписать?
Когда метод родительского класса изменяется с помощью final, дочерний класс не может переопределить метод родительского класса.Например, в Object метод getClass() является окончательным, поэтому мы не можем переопределить метод, но hashCode() метод не изменен окончательным, мы можем переопределить метод hashCode(). Давайте напишем пример, чтобы углубить наше понимание: Сначала определите родительский класс с окончательным модифицированным методом test();
public class FinalExampleParent {
public final void test() {
}
}
Затем FinalExample наследует родительский класс, и возникает ошибка при переопределении метода test(), как показано ниже:
Из этого явления мы можем видеть, чтоМетод, измененный final, не может быть переопределен подклассами..
Перегрузка?
public class FinalExampleParent {
public final void test() {
}
public final void test(String str) {
}
}
Видно, что метод, модифицированный final, может быть перегружен. После нашего анализа мы можем сделать следующие выводы:
1. Конечный метод родительского класса не может быть переопределен дочерним классом.
2. окончательные методы могут быть перегружены
2.3 Класс
Когда класс завершен, имя таблицы не может быть унаследовано подклассами.. Наследование подкласса часто может переопределить методы родительского класса и изменить свойства родительского класса, что повлечет за собой определенные риски безопасности, поэтому, когда класс не хочет наследоваться, можно использовать окончательную модификацию. Или написать небольшой пример:
public final class FinalExampleParent {
public final void test() {
}
}
Родительский класс будет изменен final.Когда дочерний класс наследует родительский класс, будет сообщено об ошибке, как показано ниже:
3. Последний пример
final часто используется в неизменяемых классах, чтобы воспользоваться неизменностью final. Давайте сначала посмотрим, что такое неизменяемый класс.
неизменяемый класс
Неизменяемый класс означает, что после создания экземпляра класса переменные экземпляра экземпляра не могут быть изменены. Класс может быть неизменяемым, если он удовлетворяет следующим условиям:
- Используйте модификаторы private и final для изменения переменных-членов класса.
- Предоставьте конструктору параметры для инициализации переменных-членов класса;
- Предоставляйте только методы получения для переменных-членов этого класса и не предоставляйте методы установки, поскольку обычные методы не могут изменять переменные-члены, измененные с помощью fina;
- При необходимости перепишите методы hashCode() и equals() класса Object и убедитесь, что значения Hashcode тех же двух объектов, оцениваемые с помощью equals(), также равны.
Все восемь классов-оболочек и классы String, представленные в JDK, являются неизменяемыми классами, давайте взглянем на реализацию String.
/** The value is used for character storage. */
private final char value[];
Можно видеть, что значение String изменяется с помощью final, и другие свойства, упомянутые выше, также непротиворечивы.
4. Вы действительно понимаете final в многопоточности?
Последнее использование, о котором мы говорили выше, должно принадлежатьБазовый уровень JavaДа, когда мы это поймем, сможем ли мы действительно освоить финал? Рассматривали ли вы final в случае многопоточного параллелизма? существуетJava-модель памятиМы знаем, что модель памяти Java имеет очень мало ограничений на нижний уровень, чтобы позволить нижнему слою процессора и компилятора проявить свои наибольшие преимущества, то есть для нижнего уровня модель памяти Java является слабая модель данных памяти. При этом процессор и компилятор могут изменять последовательность инструкций для оптимизации производительности.Переупорядочивание компилятора и процессора. Итак, в случае многопоточности, как будет изменен порядок final? Не вызовет ли это проблем с безопасностью потоков? Далее, давайте взглянем на окончательное изменение порядка.
4.1 Окончательные правила переупорядочения полей
4.1.1 final поля являются примитивными типами
Давайте сначала посмотрим на пример кода:
public class FinalDemo {
private int a; //普通域
private final int b; //final域
private static FinalDemo finalDemo;
public FinalDemo() {
a = 1; // 1. 写普通域
b = 2; // 2. 写final域
}
public static void writer() {
finalDemo = new FinalDemo();
}
public static void reader() {
FinalDemo demo = finalDemo; // 3.读对象引用
int a = demo.a; //4.读普通域
int b = demo.b; //5.读final域
}
}
Предположим, что поток A выполняет метод write(), а поток B выполняет метод reader().
Напишите окончательные правила переупорядочения полей
Напишите правила переупорядочивания для конечных полейЗапретить изменение порядка записи в конечные поля вне конструктора, реализация этого правила в основном включает два аспекта:
- JMM запрещает компилятору изменять порядок записи конечного поля вне конструктора;
- Компилятор вставит барьер хранилища после записи последнего поля и до возврата конструктора (о барьерах памяти см.эта статья). Этот барьер не позволяет процессору переупорядочивать записи конечного поля из конструктора.
Давайте еще раз проанализируем метод записи, хотя он состоит всего из одной строки кода, на самом деле он делает две вещи:
- Создан объект FinalDemo;
- Назначьте этот объект переменной-члену finalDemo.
Давайте нарисуем возможную диаграмму последовательности выполнения, которая существует, следующим образом:
Поскольку между a и b нет зависимости данных, общее поле (обычная переменная) a может быть переупорядочено вне конструктора, и поток B может прочитать значение (нулевое значение) до инициализации общей переменной a. Это может привести к ошибки. Конечная доменная переменная b, в соответствии с правилами переупорядочивания, запрещает переупорядочивать конечную измененную переменную b вне конструктора, чтобы b можно было правильно присвоить, а поток B мог прочитать инициализированное значение конечной переменной.
Таким образом, написание правил переупорядочивания для полей final гарантирует, что:Прежде чем ссылка на объект станет видимой для любого потока, конечное поле объекта будет должным образом инициализировано, а обычные поля не имеют этой гарантии.. Например, в приведенном выше примере поток B может быть неправильно инициализированным объектом finalDemo.
Прочтите окончательные правила переупорядочения полей
Прочтите окончательные правила переупорядочения полей как:В потоке при первом чтении ссылки на объект и при первом чтении последнего поля, содержащегося в объекте, JMM запрещает изменение порядка этих двух операций.(Обратите внимание, что это правило применяется только к процессору.) Процессор вставляет барьер LoadLoad перед операцией чтения конечного поля. На самом деле существует косвенная зависимость между чтением ссылки объекта и чтением конечного поля объекта, и общий процессор не будет переупорядочивать эти две операции. Но есть некоторые процессоры, которые выполняют переупорядочение, поэтому это запрещающее правило переупорядочения предназначено для этих процессоров.
Метод read() в основном включает в себя три операции:
- Первая ссылочная переменная finalDemo;
- Сначала прочитайте общее поле a, ссылающееся на переменную finalDemo;
- Сначала прочитайте final и b ссылочной переменной finalDemo;
Если предположить, что процесс записи потока A не переупорядочивается, то возможное время выполнения потока A и потока B следующее:
Если обычное поле прочитанного объекта переупорядочивается в начале ссылки на прочитанный объект, поток B будет читать переменную нормального поля объекта перед чтением ссылки на объект, что, очевидно, является неправильной операцией. Операция чтения конечного поля "уточняет", что ссылка на объект была прочитана до того, как будет прочитана конечная переменная поля, так что этой ситуации можно избежать.
Чтение правил переупорядочивания для конечных полей гарантирует, что:Прежде чем читать финальное поле объекта, он должен сначала прочитать ссылку на объект, содержащий финальное поле.
4.1.2 Последнее поле является ссылочным типом
Мы уже знаем, каковы правила переупорядочивания, когда последнее поле является примитивным типом данных? еслиссылочный тип данныхуже? Продолжаем обсуждать.
Операции записи поля для окончательно измененных объектов
Для эталонных типов данных окончательные записи полей переупорядочены для компиляторов и процессоров.Добавлены такие ограничения: внутри конструктораЗапись в поле окончательно оформленного объекта с последующим присвоением ссылки на сконструированный объект ссылочной переменной вне конструктора, эти две операции не могут быть переупорядочены. Обратите внимание, что «увеличение» здесь означает, что здесь все еще используются предыдущие правила переупорядочивания для окончательных базовых типов данных. Это предложение немного сложное, поэтому давайте рассмотрим его на примере.
public class FinalReferenceDemo {
final int[] arrays;
private FinalReferenceDemo finalReferenceDemo;
public FinalReferenceDemo() {
arrays = new int[1]; //1
arrays[0] = 1; //2
}
public void writerOne() {
finalReferenceDemo = new FinalReferenceDemo(); //3
}
public void writerTwo() {
arrays[0] = 2; //4
}
public void reader() {
if (finalReferenceDemo != null) { //5
int temp = finalReferenceDemo.arrays[0]; //6
}
}
}
В приведенном выше примере программы поток A выполняет метод WriterOne, после выполнения поток B выполняет метод WriterTwo, а затем поток C выполняет метод чтения. На следующем рисунке обсуждается ситуация, в которой возникает эта последовательность выполнения (вы можете только извлечь выгоду, терпеливо прочитав ее).
1 и 3 нельзя изменить порядок, так как запись в поля final запрещает изменение порядка вне конструктора. 2 и 3 нельзя переупорядочить, потому что запись поля ссылочного объекта в конечное поле не может быть переупорядочена с последующим присвоением сконструированного объекта ссылочной переменной.
Операции чтения полей для окончательно измененных объектов
JMM может гарантировать, что поток C может, по крайней мере, видеть запись записывающего потока A в поле члена объекта, на который ссылается final, то есть он может видеть arrays[0] = 1, в то время как запись потока B в элементы массива может видеть и не видеть. JMM не гарантирует, что запись потока B будет видна потоку C. Между потоком B и потоком C происходит гонка данных, и результат в это время непредсказуем. Используйте блокировки или volatile, если они видны.
Резюме по окончательному изменению порядка
Классифицируется в соответствии с окончательным измененным типом данных:
Основные типы данных:
- последнее поле написать: запрещенопоследнее поле написатьиМетод строительстваПереупорядочивание, то есть запрет записи и переупорядочивания конечных полей вне конструктора, чтобы гарантировать, что, когда объект виден всем потокам, все конечные поля объекта были инициализированы.
- финальное поле читается: запретить в первый разпрочитать ссылку на объектиПрочитайте последние поля, содержащиеся в объектеизменение порядка.
Тип справочных данных:
Дополнительные ограничения: запрещено для пар конструкторовЗапись в поле окончательно измененного объектас последующимСсылка на построенный объект присваивается ссылочной переменнойизменение порядка
5. Принцип реализации конечного
Выше мы упоминали, что для записи конечных полей компилятор должен вставить барьер StoreStore после того, как последнее поле будет записано и до возврата из конструктора. Правила переупорядочивания, которые считывают окончательные поля, требуют, чтобы компилятор вставил барьер LoadLoad перед операциями, считывающими окончательные поля.
Интересно, что если взять в качестве примера обработку x86, x86 не переупорядочивает запись-запись, поэтомуБарьеры StoreStore можно не указывать. так какНе переупорядочивает операции с косвенными зависимостями, поэтому в процессорах X86 чтение последнего поля требуетБарьеры LoadLoad также опущены. Это,Взяв в качестве примера X86, барьер чтения/записи памяти для конечного поля будет опущен.! Подключен ли он, зависит от процессора
6. Почему окончательные ссылки не могут «переполняться» из конструкторов
Здесь есть еще одна интересная проблема: приведенные выше правила переупорядочивания полей final могут гарантировать, что когда мы используем ссылку на объект, конечное поле объекта будет инициализировано в конструкторе. Но на самом деле здесь есть предварительное условие, а именно:В конструкторе сконструированный объект нельзя сделать видимым для других потоков, то есть ссылка на объект не может «ускользнуть» в конструкторе.. Возьмем следующий пример:
public class FinalReferenceEscapeDemo {
private final int a;
private FinalReferenceEscapeDemo referenceDemo;
public FinalReferenceEscapeDemo() {
a = 1; //1
referenceDemo = this; //2
}
public void writer() {
new FinalReferenceEscapeDemo();
}
public void reader() {
if (referenceDemo != null) { //3
int temp = referenceDemo.a; //4
}
}
}
Возможная последовательность выполнения показана на рисунке:
Предположим, что один поток A выполняет метод записи, а другой поток выполняет метод чтения. Поскольку между операциями 1 и 2 в конструкторе нет зависимости данных, 1 и 2 можно переупорядочить, и первой выполняется 2. В это время ссылочный объект referenceDemo является объектом, который не полностью инициализирован, и когда поток B читает объект, он пойдет не так. Хотя правила переупорядочения записи конечного поля все еще выполняются: когда объект, на который указывает ссылка, виден всем потокам, его конечное поле было полностью успешно инициализировано. Однако ссылочный объект "this" экранируется, а код по-прежнему имеет проблему безопасности потоков.
см. литературу
Искусство параллельного программирования на Java
«Безумные лекции по Java»