Статьи

Неизменность действительно означает Потокобезопасность?

Я часто читал статьи, в которых говорилось: «Если объект неизменен, он безопасен для потоков». На самом деле, я никогда не нашел статью, которая убедила бы меня, что неизменность означает безопасность потоков. Даже книга Brian Goetz Java Concurrency на практике с ее главой об неизменности не полностью удовлетворила меня. В этой книге мы можем прочитать слово в слово в рамке: неизменяемые объекты всегда поточно-ориентированы . Я думаю, что это предложение заслуживает большего объяснения.

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

Определения неизменяемости

Мое определение таково: «Неизменяемый объект — это объект, состояние которого не изменяется после его создания». Я намеренно расплывчато, так как никто точно не согласен с точными определениями.

Поток безопасности

Вы можете найти много разных определений «безопасных потоков» в Интернете. Это на самом деле очень сложно определить. Я бы сказал, что потокобезопасный код — это код, который имеет ожидаемое поведение в многопоточной среде. Я позволю вам определить «ожидаемое поведение» …

Пример строки

Давайте посмотрим на код 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 .