← Назад к вопросам

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

2.0 Middle🔥 141 комментариев
#Другое

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему Prototype не является потокобезопасным

Этот вопрос о Design Pattern Prototype в контексте многопоточности. Prototype паттерн не потокобезопасен по умолчанию из-за того, как происходит клонирование объектов и управление состоянием. Давайте разберёмся в деталях.

Что такое Prototype Pattern

Prototype Pattern — это паттерн создания объектов, который позволяет создавать новые объекты путём клонирования существующего объекта (прототипа):

// Интерфейс с методом clone
public interface Prototype {
    Prototype clone();
}

// Конкретная реализация
public class Document implements Prototype, Cloneable {
    private String title;
    private String content;
    private List<Page> pages;
    
    @Override
    public Document clone() {
        try {
            return (Document) super.clone();  // Shallow copy
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

// Использование
Document original = new Document("Report", "...");
Document copy1 = original.clone();
Document copy2 = original.clone();

Проблема 1: Shallow Copy vs Deep Copy

Shallow Copy (поверхностное копирование)

По умолчанию clone() создаёт shallow copy:

public class Document implements Cloneable {
    private String title;           // Примитив — копируется
    private List<String> lines;     // Ссылка — общая! ← ПРОБЛЕМА
    
    @Override
    public Document clone() {
        try {
            return (Document) super.clone();  // Shallow copy
            // Новый Document, но lines указывает на ТУ ЖЕ List!
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

// Проблема в многопоточности:

// Поток 1
Document doc = new Document("Report", lines);
Document copy = doc.clone();

copy.lines.add("New line");  // Модифицирует ОБЩИЙ список

// Поток 2  
System.out.println(doc.lines);  // Видит "New line"! ← Гонка данных

Deep Copy (глубокое копирование)

public class Document implements Cloneable {
    private String title;
    private List<String> lines;
    
    @Override
    public Document clone() {
        try {
            Document cloned = (Document) super.clone();
            // Важно: создаём новый список!
            cloned.lines = new ArrayList<>(this.lines);
            return cloned;
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

Проблема 2: Отсутствие синхронизации

Сценарий гонки данных (Race Condition)

public class UserProfile implements Cloneable {
    private String name;
    private int age;
    private List<String> roles;  // Collections.emptyList() или обычный List
    
    @Override
    public UserProfile clone() {
        try {
            return (UserProfile) super.clone();  // Не синхронизировано
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

// Без синхронизации в многопоточной среде:

public class PrototypeRegistry {
    private UserProfile masterProfile;
    
    // Поток 1: Читает и клонирует
    public UserProfile getUser() {
        // Между read и clone может произойти изменение!
        UserProfile copy = masterProfile.clone();
        return copy;
    }
    
    // Поток 2: Изменяет
    public void updateMasterProfile(UserProfile newProfile) {
        this.masterProfile = newProfile;
    }
}

// Проблема:
// Поток 1: читает masterProfile (reference)
// Поток 2: заменяет masterProfile
// Поток 1: клонирует старую версию?
// Гонка данных!

Проблема 3: Состояние объекта может измениться

public class ConfigurationTemplate implements Cloneable {
    private String database;
    private String apiKey;
    private boolean isConnected;  // Состояние может измениться
    
    @Override
    public ConfigurationTemplate clone() {
        try {
            // Clone копирует текущее состояние
            return (ConfigurationTemplate) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

// Проблема:

// Поток 1
ConfigurationTemplate template = new ConfigurationTemplate("db1", "key1", false);
ConfigurationTemplate copy = template.clone();  // Copy: isConnected = false

// Между clone() и использованием copy...
// Поток 2
template.setConnected(true);  // Изменили оригинал

// Поток 1
System.out.println(copy.isConnected());  // Может быть true!
// Если clone выполнился в момент, когда setConnected выполняется

Пример реальной проблемы

public class DataCache implements Cloneable {
    private Map<String, Object> cache;
    
    public DataCache(Map<String, Object> cache) {
        this.cache = cache;
    }
    
    @Override
    public DataCache clone() {
        try {
            DataCache cloned = (DataCache) super.clone();
            // Ошибка! Map не скопирован, обе ссылки на один Map
            return cloned;
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

public class PrototypeFactory {
    private DataCache template;
    
    public PrototypeFactory(DataCache template) {
        this.template = template;
    }
    
    public DataCache create() {
        return template.clone();  // Возвращает объект с общим Map!
    }
}

// В многопоточной среде:

public class MultiThreadedTest {
    public static void main(String[] args) throws InterruptedException {
        Map<String, Object> sharedMap = new HashMap<>();
        sharedMap.put("value", 0);
        
        DataCache template = new DataCache(sharedMap);
        PrototypeFactory factory = new PrototypeFactory(template);
        
        // Множество потоков создают и используют копии
        ExecutorService executor = Executors.newFixedThreadPool(10);
        
        for (int t = 0; t < 10; t++) {
            executor.submit(() -> {
                for (int i = 0; i < 1000; i++) {
                    DataCache copy = factory.create();
                    
                    // Все копии используют ОДИН cache!
                    Integer current = (Integer) copy.cache.get("value");
                    copy.cache.put("value", current + 1);
                }
            });
        }
        
        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
        
        // Ожидаем 10000 (10 потоков * 1000 инкрементов)
        System.out.println(sharedMap.get("value"));
        // Результат: 3847 или 5234 или что-то другое!
        // Правильно: 10000
    }
}

Решение 1: Синхронизация при клонировании

public class SafeDocument implements Cloneable {
    private String title;
    private List<String> content;
    
    // Синхронизируем доступ к prototype
    @Override
    public synchronized SafeDocument clone() {
        try {
            SafeDocument cloned = (SafeDocument) super.clone();
            // Deep copy для mutable объектов
            cloned.content = new ArrayList<>(this.content);
            return cloned;
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

public class ThreadSafePrototypeRegistry {
    private SafeDocument masterDocument;
    private final Object lock = new Object();
    
    public SafeDocument getDocument() {
        synchronized (lock) {
            return masterDocument.clone();  // Синхронизируем доступ
        }
    }
    
    public void updateMaster(SafeDocument newDoc) {
        synchronized (lock) {
            this.masterDocument = newDoc;
        }
    }
}

Решение 2: Copy Constructor (предпочтительнее)

public class Document {
    private String title;
    private List<String> content;
    
    // Обычный конструктор
    public Document(String title, List<String> content) {
        this.title = title;
        this.content = new ArrayList<>(content);  // Deep copy
    }
    
    // Copy constructor (более явный)
    public Document(Document original) {
        this.title = original.title;
        // Deep copy
        this.content = new ArrayList<>(original.content);
    }
}

// Использование
Document original = new Document("Report", lines);
Document copy = new Document(original);  // Явно показывает копирование

Решение 3: Builder Pattern

public class Configuration {
    private String host;
    private int port;
    private boolean ssl;
    
    // Builder создаёт новый объект явно
    public static class Builder {
        private String host;
        private int port;
        private boolean ssl;
        
        public Builder(Configuration original) {
            this.host = original.host;
            this.port = original.port;
            this.ssl = original.ssl;
        }
        
        public Builder host(String host) {
            this.host = host;
            return this;
        }
        
        public Configuration build() {
            return new Configuration(this);
        }
    }
    
    private Configuration(Builder builder) {
        this.host = builder.host;
        this.port = builder.port;
        this.ssl = builder.ssl;
    }
}

// Использование
Configuration original = new Configuration("localhost", 8080, false);
Configuration copy = new Configuration.Builder(original)
    .host("example.com")
    .build();

Решение 4: Immutable Objects

// Неизменяемый объект — всегда потокобезопасен!
public final class ImmutableDocument {
    private final String title;
    private final List<String> content;  // final
    
    public ImmutableDocument(String title, List<String> content) {
        this.title = title;
        // Создаём неизменяемую копию
        this.content = Collections.unmodifiableList(
            new ArrayList<>(content)
        );
    }
    
    // Нет setter методов
    
    // clone() не нужен — всегда можно вернуть this
    public ImmutableDocument copy() {
        return new ImmutableDocument(this.title, this.content);
    }
}

Сравнение подходов

ПодходПотокобезопасностьПроизводительностьСложность
Prototype + sync✓ ДаХорошоВысокая
Copy constructor✓ ДаОтличнаяСредняя
Builder✓ ДаХорошоСредняя
Immutable✓ ДаОтличнаяНизкая

Best Practices

// ❌ ПЛОХО: Unsafe Prototype
public class UnsafeUser implements Cloneable {
    private List<String> permissions;  // Shared reference!
    
    @Override
    public Object clone() {
        return super.clone();  // Shallow copy
    }
}

// ✓ ХОРОШО: Copy Constructor
public class SafeUser {
    private final String name;
    private final List<String> permissions;
    
    public SafeUser(SafeUser original) {
        this.name = original.name;
        this.permissions = new ArrayList<>(original.permissions);  // Deep copy
    }
}

// ✓ ЛУЧШЕ: Immutable
public final class ImmutableUser {
    private final String name;
    private final List<String> permissions;
    
    public ImmutableUser(String name, List<String> permissions) {
        this.name = name;
        this.permissions = Collections.unmodifiableList(
            new ArrayList<>(permissions)
        );
    }
}

Заключение

Prototype паттерн не потокобезопасен потому что:

  1. Shallow copy — shared references между оригиналом и копией
  2. Отсутствие синхронизации — race conditions при одновременном доступе
  3. Изменяемое состояние — состояние может измениться между clone() и использованием

В многопоточной среде лучше использовать:

  • Copy Constructor
  • Builder Pattern
  • Immutable Objects
  • Или явную синхронизацию с deep copy

Главное правило: никогда не делай shallow copy mutable объектов в многопоточной среде без синхронизации.

Почему Prototype не является потокобезопасным? | PrepBro