class ImmutableItemCache {

    public ITEM find(ID id, ImmutableItemManager<ID, ITEM> itemManager) throws InterruptedException, IOException
    {
        ITEM item;
        boolean cleanupPlaceholder = false;
        CacheEntry<ID, ITEM> entry = cache.get(id);
        if (entry != null && !entry.placeholder()) {
            entry.recentAccess(true);
            item = entry.item();
        } else {
            try {
                synchronized (this) {
                    while ((entry = cache.get(id)) != null && entry.placeholder()) {
                        wait();
                    }
                    if (entry == null) {
                        assert cacheSize <= cacheCapacity;
                        if (cacheSize == cacheCapacity) {
                            // Cache is full. Evict something.
                            ITEM victim = clock.takeItemToEvict();
                            assert victim != null;
                            CacheEntry<ID, ITEM> removed = cache.remove(victim.id());
                            assert removed == victim : String.format("replaced: %s, entry: %s", removed, entry);
                            itemManager.cleanupItemEvictedFromCache(victim);
                            cacheSize--;
                        }
                        assert cacheSize < cacheCapacity;
                        entry = ItemPlaceholder.<ID, ITEM>forCurrentThread();
                        CacheEntry<ID, ITEM> replaced = cache.put(id, entry);
                        assert replaced == null;
                        cacheSize++;
                    } else {
                        assert !entry.placeholder();
                        assert entry.id().equals(id);
                        entry.recentAccess(true);
                    }
                }
                if (entry.placeholder()) {
                    // This thread wrote a placeholder. Read the cache item outside the lock, since this
                    // is probably slow.
                    assert entry.owner() == Thread.currentThread();
                    item = itemManager.getItemForCache(id);
                    // Inside the lock, replace the placeholder with the item, and notify waiters.
                    synchronized (this) {
                        CacheEntry<ID, ITEM> replaced = cache.put(id, item);
                        assert replaced == entry : String.format("replaced: %s, entry: %s", replaced, entry);
                        clock.addItem(item);
                        item.recentAccess(true);
                        notifyAll();
                    }
                } else {
                    entry.recentAccess(true);
                    item = entry.item();
                }
            } catch (RuntimeException | Error e) {
                cleanupPlaceholder = true;
                throw e;
            } finally {
                if (cleanupPlaceholder) {
                    synchronized (this) {
                        entry = cache.get(id);
                        if (entry != null && entry.placeholder()) {
                            cache.remove(id);
                        }
                    }
                }
            }
        }
        assert item.id().equals(id);
        return item;
    }

    public  ImmutableItemCache(int cacheCapacity);
    public  ImmutableItemCache(int cacheCapacity, CacheEntryList.Observer<ID, ITEM> observer);

}