Итак, я собираюсь попытаться определить неизменность и его связь с безопасностью потока.
Определения неизменяемости
Мое определение таково: «Неизменяемый объект — это объект, состояние которого не изменяется после его создания». Я намеренно расплывчато, так как никто точно не согласен с точными определениями.
Поток безопасности
Вы можете найти много разных определений «безопасных потоков» в Интернете. Это на самом деле очень сложно определить. Я бы сказал, что потокобезопасный код — это код, который имеет ожидаемое поведение в многопоточной среде. Я позволю вам определить «ожидаемое поведение» …
Пример строки
Давайте посмотрим на код String
(на самом деле просто часть кода …):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public class String { private final char value[]; /** Cache the hash code for the string */ private int hash; // Default to 0 public String( char [] value) { this .value = Arrays.copyOf(value, value.length); } public int hashCode() { int h = hash; if (h == 0 && value.length > 0 ) { char val[] = value; for ( int i = 0 ; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; } } |
String
считается неизменной. Рассматривая его реализацию, мы можем вывести одну вещь: неизменяемый может изменить свое внутреннее состояние (в данном случае хэш-код, который загружается ленивым образом), пока он не виден снаружи.
Теперь я собираюсь переписать метод хеш-кода не потокобезопасным способом:
01
02
03
04
05
06
07
08
09
10
|
public int hashCode() { if (hash == 0 && value.length > 0 ) { char val[] = value; for ( int i = 0 ; i < value.length; i++) { hash = 31 * hash + val[i]; } } return hash; } |
Как видите, я удалил локальную переменную h
и вместо этого непосредственно затронул hash
переменной. Эта реализация НЕ является поточно-ориентированной! Если несколько потоков вызывают hashcode
одновременно, возвращаемое значение может отличаться для каждого потока. Вопрос в том, является ли этот класс неизменным? Поскольку два разных потока могут видеть разные хеш-коды, с внешней точки зрения у нас есть изменение состояния, и поэтому оно не является неизменным.
Таким образом, мы можем сделать вывод, что String
неизменен, потому что он потокобезопасен, а не наоборот. Итак … Какой смысл говорить: «Сделай какой-нибудь неизменный объект, он потокобезопасен! Но будьте осторожны, вы должны сделать свой неизменный объект потокобезопасным! » ?
Пример ImmutableSimpleDateFormat
Ниже я написал класс, похожий на SimpleDateFormat.
1
2
3
4
5
6
7
8
|
public class VerySimpleDateFormat { private final DateFormat formatter = SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT); public String format(Date d){ return formatter.format(d); } } |
Этот код не является потокобезопасным, потому что SimpleDateFormat.format
нет.
Является ли этот объект неизменным? Хороший вопрос! Мы сделали все возможное, чтобы все поля не модифицировались, мы не используем какой-либо метод установки или методы, позволяющие предположить, что состояние объекта изменится. На самом деле, SimpleDateFormat
внутренне меняет свое состояние, и это делает его не защищенным от потоков. Поскольку в графе объектов что-то меняется, я бы сказал, что оно не является неизменным, даже если оно выглядит так… Проблема даже не в том, что SimpleDateFormat
меняет свое внутреннее состояние, а в том, что он делает это не потокобезопасным способом.
Заключение этого примера не так просто сделать неизменным классом. Последнее ключевое слово недостаточно, вы должны убедиться, что поля объекта вашего объекта не изменяют свое состояние, что иногда невозможно.
Неизменяемые объекты могут иметь не поточнобезопасные методы (без магии!)
Давайте посмотрим на следующий код.
01
02
03
04
05
06
07
08
09
10
11
12
|
public class HelloAppender { private final String greeting; public HelloAppender(String name) { this .greeting = 'hello ' + name + '!\n' ; } public void appendTo(Appendable app) throws IOException { app.append(greeting); } } |
Класс HelloAppender
определенно неизменен. Метод appendTo принимает метод Appendable
. Поскольку Appendable
не гарантирует Appendable
(например, StringBuilder
), добавление к этому Appendable
вызовет проблемы в многопоточной среде.
Вывод
Создание неизменяемых объектов, безусловно, является хорошей практикой в некоторых случаях, и это очень помогает для создания поточно-ориентированного кода. Но меня беспокоит, когда я читаю везде Неизменяемые объекты являются поточно-ориентированными и отображаются в виде аксиомы. Я понимаю, но я думаю, что всегда хорошо подумать об этом, чтобы понять, что вызывает не поточно-безопасные коды.
Благодаря комментарию Хосе я заканчиваю эту статью другим выводом. Все дело в определении неизменного. Это нуждается в разъяснениях!
Объект является неизменным, если:
- Все его поля инициализируются перед использованием (что означает, что вы можете выполнить отложенную инициализацию)
- Состояния поля не меняются после их инициализации (не меняет означает, что граф объектов не меняется, даже внутреннее состояние дочерних элементов)
Неизменяемый объект всегда будет потокобезопасным, если он не имеет дело с тем, что он должен манипулировать объектами, не поддерживающими поток
Справка: действительно ли неизменность означает безопасность потоков? от нашего партнера по JCG Тибо Делора в блоге InvalidCodeException .