Почему паттерн Prototype в Java не является потокобезопасным по умолчанию?

«Почему паттерн Prototype в Java не является потокобезопасным по умолчанию?» — вопрос из категории Паттерны, который задают на 10% собеседований Java Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Паттерн Prototype не является потокобезопасным, потому что операция клонирования (clone()) и последующая модификация состояния объекта-прототипа могут выполняться конкурентно разными потоками, что приводит к состоянию гонки (race condition).

Основные причины:

  1. Разделяемое состояние: Если прототип содержит изменяемые (mutable) поля, они разделяются между оригиналом и клонами или между разными клонами.
  2. Неатомарность clone(): Метод Object.clone() выполняет поверхностное копирование (shallow copy). Для сложных объектов требуется глубокое копирование (deep copy), реализация которого также должна быть синхронизирована.
  3. Отсутствие синхронизации: Стандартная реализация не использует блокировки (synchronized) или другие механизмы координации потоков.

Пример проблемы:

public class DocumentPrototype implements Cloneable {
    private StringBuilder content; // Изменяемый объект!

    public DocumentPrototype(String text) {
        this.content = new StringBuilder(text);
    }

    public void append(String text) {
        this.content.append(text); // Небезопасно для потоков
    }

    @Override
    public DocumentPrototype clone() {
        try {
            // Поверхностное копирование! Оба объекта ссылаются на один StringBuilder.
            return (DocumentPrototype) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}
// Поток 1: prototype.append("A");
// Поток 2: DocumentPrototype copy = prototype.clone(); // copy.content может быть в несогласованном состоянии

Решение: Для потокобезопасности необходимо:

  • Использовать глубокое копирование неизменяемых (immutable) объектов.
  • Синхронизировать методы, изменяющие состояние прототипа, или использовать ThreadLocal.
  • Рассмотреть использование фабрики для создания копий с полной инициализацией.