如何使用HashMap从Cache中为相同的键返回相同的Object?

时间:2017-04-21 00:54:13

标签: java junit junit4

我有以下一组课程(以及失败的单元测试):

链轮:

public class Sprocket {
    private int serialNumber;

    public Sprocket(int serialNumber) {
        this.serialNumber = serialNumber;
    }

    @Override
    public String toString() {
        return "sprocket number " + serialNumber;
    }
}

SlowSprocketFactory:

public class SlowSprocketFactory {
    private final AtomicInteger maxSerialNumber = new AtomicInteger();

    public Sprocket createSprocket() {
        // clang, click, whistle, pop and other expensive onomatopoeic operations
        int serialNumber = maxSerialNumber.incrementAndGet();
        return new Sprocket(serialNumber);
    }

    public int getMaxSerialNumber() {
        return maxSerialNumber.get();
    }
}

SprocketCache:

public class SprocketCache {

    private SlowSprocketFactory sprocketFactory;
    private Sprocket sprocket;

    public SprocketCache(SlowSprocketFactory sprocketFactory) {
        this.sprocketFactory = sprocketFactory;
    }

    public Sprocket get(Object key) {
        if (sprocket == null) {
            sprocket = sprocketFactory.createSprocket();
        }

        return sprocket;
    }
}

TestSprocketCache单元测试:

public class TestSprocketCache {

    private SlowSprocketFactory sprocketFactory = new SlowSprocketFactory();

    @Test
    public void testCacheReturnsASprocket() {
        SprocketCache cache = new SprocketCache(sprocketFactory);
        Sprocket sprocket = cache.get("key");
        assertNotNull(sprocket);
    }

    @Test
    public void testCacheReturnsSameObjectForSameKey() {
        SprocketCache cache = new SprocketCache(sprocketFactory);

        Sprocket sprocket1 = cache.get("key");
        Sprocket sprocket2 = cache.get("key");

        assertEquals("cache should return the same object for the same key", sprocket1, sprocket2);
        assertEquals("factory's create method should be called once only", 1, sprocketFactory.getMaxSerialNumber());
    }
}

即使我按如下方式更改以下内容,TestSprocketCache单元测试也始终返回绿色条:

Sprocket sprocket1 = cache.get("key");
Sprocket sprocket2 = cache.get("pizza");

我猜我必须在SprocketCache.get()方法中使用HashMap.contains(key),但似乎无法弄清楚逻辑。

2 个答案:

答案 0 :(得分:0)

您遇到的问题是您的get(Object)实施只允许创建一个实例:

    public Sprocket get(Object key) {
        // Creates object if it doesn't exist yet
        if (sprocket == null) { 
            sprocket = sprocketFactory.createSprocket();
        }

        return sprocket;
    }

这是典型的延迟加载实例化 singleton 模式。如果再次调用get,则会将实例分配给sprocket,它将完全跳过实例化。请注意,您根本不使用key参数,因此不会影响任何内容。

使用Map确实是实现目标的一种方式:

public class SprocketCache {

    private SlowSprocketFactory sprocketFactory;
    private Map<Object, Sprocket> instances = new HashMap<Object, Sprocket>();

    public SprocketCache(SlowSprocketFactory sprocketFactory) {
        this.sprocketFactory = sprocketFactory;
    }

    public Sprocket get(Object key) {
        if (!instances.containsKey(key)) {
            instances.put(sprocket);
        }

        return instances.get(key);
    }
}

答案 1 :(得分:0)

好吧,你当前的Cache实现不依赖于key,所以难怪它总是返回相同的cached-once值。

如果你想为密钥存储不同的值,并假设你希望它是线程安全的,你可能最终会做这样的事情:

public class SprocketCache {

    private SlowSprocketFactory sprocketFactory;
    private ConcurrentHashMap<Object, Sprocket> cache = new ConcurrentHashMap<?>();

    public SprocketCache(SlowSprocketFactory sprocketFactory) {
        this.sprocketFactory = sprocketFactory;
    }

    public Sprocket get(Object key) {
        if (!cache.contains(key)) {
            // we only wan't acquire lock for cache seed operation rather than for every get
            synchronized (key){                     
                // kind of double check locking to make sure no other thread has populated cache while we were waiting for monitor to be released
                if (!cache.contains(key)){ 
                    cache.putIfAbsent(key, sprocketFactory.createSprocket());
                }
            } 
        }
        return cache.get(key);
    }
}

重要的附注:

  • 你需要CocncurrentHashMap来确保在范式之前发生,所以其他线程会立即看到缓存是否已被填充;
  • 必须同步新的缓存值创建,以便每个并发 线程不会生成它自己的值,在竞争条件期间覆盖以前的值;
  • 同步是非常昂贵的,因此我们只是在需要时才能使用它,并且由于相同的竞争条件,您可能会同时获得多个线程持有监视器。这就是为什么在同步块之后需要进行另一次检查以确保其他线程还没有填充该值。