Контролируемые экземпляром классы и многопоточность

В главе 2 " Эффективная Java", пункт 1, Блох предлагает рассмотреть статические фабричные методы вместо конструкторов для инициализации объекта. Одно из преимуществ, о которых он упоминает, состоит в том, что этот шаблон позволяет классам возвращать один и тот же объект от повторных вызовов:

Способность статических фабричных методов возвращать один и тот же объект из повторяющихся вызовов позволяет классам в любой момент поддерживать строгий контроль над тем, какие экземпляры существуют. Классы, которые делают это, называются контролируемыми экземплярами. Есть несколько причин для написания управляемых экземпляром классов. Контроль экземпляра позволяет классу гарантировать, что он является одноэлементным (элемент 3) или нереализуемым (элемент 4). Кроме того, он позволяет неизменному классу (элемент 15) гарантировать, что не существует двух одинаковых экземпляров: a.equals(b) тогда и только тогда, когда a==b.

Как этот шаблон будет работать в многопоточной среде? Например, у меня есть неизменный класс, который должен управляться экземпляром, потому что одновременно может существовать только одна сущность с данным ID:

public class Entity {

    private final UUID entityId;

    private static final Map<UUID, Entity> entities = new HashMap<UUID, Entity>();

    private Entity(UUID entityId) {
        this.entityId = entityId;
    }

    public static Entity newInstance(UUID entityId) {
        Entity entity = entities.get(entityId);
        if (entity == null) {
            entity = new Entity(entityId);
            entities.put(entityId, entity);
        }
        return entity;
    }

}

Что будет, если я позвоню newInstance() из отделенных тем? Как я могу сделать этот класс потокобезопасным?

2 ответа

Решение

Сделайте синхронизацию newInstance, то есть

public static synchronized Entity newInstance(UUID entityId){
     ...
}

так что один поток входит в новый метод экземпляра, никакой другой поток не может вызвать этот метод, пока не завершится первый поток. По сути, первый поток получает блокировку для всего класса. Если первый поток удерживает блокировку для класса, никакой другой поток не может ввести синхронизированный статический метод для этого класса.

Если вы запустите этот код, это может привести к непредсказуемым результатам, поскольку два потока могут одновременно вызывать метод newInstance, оба будут видеть entity поле как ноль, и оба создадут new Entity, В этом случае эти два потока будут иметь разные экземпляры этого класса.

Вы должны иметь в своем классе статическую сущность Entity Entity вместо того, чтобы получать ее с карты. Вот почему вы должны использовать синхронизацию. Вы можете синхронизировать весь метод так:

public synchronized static Entity newInstance(UUID entityId)

В качестве альтернативы вы можете использовать двойную проверку блокировки, которая лучше, но должна быть сделана осторожно - посмотрите на комментарии ниже.

Что касается безопасности потоков этого класса, то есть еще один вопрос - карта, которую вы используете. Это делает класс Mutable, потому что состояние объекта Entity изменяется при изменении карты. Финал не достаточен в этом случае. Вы должны хранить карту в каком-то другом классе, например EntityManager.

Я думаю, что ваша сущность должна быть простой и не должна интересоваться вопросом "Я уникален" - это должна быть обязанность кого-то другого. Вот почему я хотел бы предположить, что сущность выглядит так:

public class Entity {

    private final UUID entityId;

    public Entity(UUID entityId) {
        this.entityId = entityId;
    }

    public UUID getEntityId() {
        return entityId;
    }
}

Теперь он неизменен и останется таким, потому что его поле является окончательным и неизменным. Если вы хотите добавить некоторые поля, убедитесь, что они также неизменны.

Что касается хранения, я бы предложил некоторый класс держателя:

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class EntityHolder {
    private static Map<UUID, Entity> entities;

    private static volatile EntityHolder singleton;

    public EntityHolder() {
        entities = new ConcurrentHashMap<UUID, Entity>();
    }

    public Entity getEntity(final UUID id) {
        return entities.get(id);
    }

    public boolean addEntity(final UUID id, final Entity entity) {
        synchronized (entities) {
            if (entities.containsKey(id)) {
                return false;
            } else {
                entities.put(id, entity);
                return true;
            }
        }
    }

    public void removeEntity(final UUID id) {
        entities.remove(id);
    }

    public static EntityHolder getInstance() {
        if (singleton == null) {
            synchronized (EntityHolder.class) {
                if (singleton == null) {
                    singleton = new EntityHolder(); 
                }
            }
        }
        return singleton;
    }
}

Таким образом, вы можете отделить его от других классов. А что касается создания, я бы использовал создатель (фабрика), как это:

import java.util.UUID;

public class EntityCreator {

public static void createEntity(final UUID id) {
    boolean entityAdded = EntityHolder.getInstance().addEntity(id, new Entity(id));
    if (entityAdded) {
        System.out.println("Entity added.");
    } else {
        System.out.println("Entity already exists.");
    }
}
}
Другие вопросы по тегам