【问题标题】:Instance-controlled classes and multithreading实例控制的类和多线程
【发布时间】:2014-10-10 19:46:19
【问题描述】:

Effective Java 第 2 章第 1 项中,Bloch 建议考虑静态工厂方法而不是构造函数来初始化对象。他提到的好处之一是这种模式允许类从重复调用中返回相同的对象:

静态工厂方法从重复调用返回相同对象的能力允许类在任何时候保持对存在的实例的严格控制。执行此操作的类被称为实例控制。编写实例控制类有几个原因。实例控制允许一个类保证它是单例(第 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() 会发生什么?我怎样才能使这个类线程安全?

【问题讨论】:

  • 您在谈论Multiton 模式。有一个出色的 Java 示例 here
  • 不要忘记,如果实体不再从内部映射中删除,这可能会导致内存泄漏!根据您的用例,也可能需要销毁实体!
  • @isnot2bad 更好的是,保留WeakReferences,它仍然保证每个guid 最多存在1 个实时Entity 实例,但垃圾收集很快。

标签: java multithreading multiton


【解决方案1】:

如果您运行此代码,可能会导致不可预知的结果,因为两个线程可以同时调用 newInstance 方法,两者都会将 entity 字段视为 null,并且都会创建 new Entity。在这种情况下,这两个线程将具有此类的不同实例。

您的类中应该有一个静态私有字段 Entity 实体,而不是从地图中获取它。 这就是为什么你应该使用同步。你可以像这样同步整个方法:

public synchronized static Entity newInstance(UUID entityId)

作为替代方案,您可以使用更好的双重检查锁定,但必须小心操作 - 看看下面的 cmets。

至于这个类的线程安全还有另一件事——你使用的映射。它使类 Mutable,因为 Entity 对象的状态在映射更改时会更改。在这种情况下,Final 是不够的。 您应该将地图存储在 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.");
    }
}
}

【讨论】:

  • 谢谢 - 我不知道这一点。我已经从我的回答中删除了 DCL。
  • 是的,我是。至少我的编译器允许我这样做;)同步静态方法时,您正在使用类进行锁定。我再次编辑了我的帖子,以便更详细地回答问题。我已经将 volatile 用于 DCL - 感谢您提醒我。如果您认为扩展答案有问题,请告诉我。
  • 使用这个过于复杂的 DCL 修复程序而不是简单的同步块有什么好处?如果您认为它更快,请运行性能测试 - 我怀疑它在常用硬件架构上更快。
  • 好的,我试试。有人向我建议它更快并且对我来说有点道理,所以我没有检查它,但现在我会做。然而,我认为如果该类中有其他同步方法会很有用。感谢您的评论。
【解决方案2】:

使newInstance同步

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

所以一个线程进入新的实例方法,除非第一个线程完成,否则没有其他线程可以调用此方法。基本上发生的事情是第一个线程获得了整个类的锁。在第一个线程为该类持有锁的时间内,没有其他线程可以进入该类的同步静态方法。

【讨论】:

    猜你喜欢
    • 2015-01-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多