// need 'synchronized' to ensure atomic initialization of merged data
  // because several threads that acquired read lock may simultaneously execute the method
  private ValueContainerImpl<Value> getMergedData() {
    ValueContainerImpl<Value> merged = myMerged;
    if (merged != null) {
      return merged;
    }
    synchronized (myInitializer.getLock()) {
      merged = myMerged;
      if (merged != null) {
        return merged;
      }

      final ValueContainer<Value> fromDisk = myInitializer.compute();
      final ValueContainerImpl<Value> newMerged;

      if (fromDisk instanceof ValueContainerImpl) {
        newMerged = ((ValueContainerImpl<Value>) fromDisk).copy();
      } else {
        newMerged = ((ChangeTrackingValueContainer<Value>) fromDisk).getMergedData().copy();
      }

      TIntHashSet invalidated = myInvalidated;
      if (invalidated != null) {
        invalidated.forEach(
            new TIntProcedure() {
              @Override
              public boolean execute(int inputId) {
                newMerged.removeAssociatedValue(inputId);
                return true;
              }
            });
      }

      ValueContainerImpl<Value> added = myAdded;
      if (added != null) {
        added.forEach(
            new ContainerAction<Value>() {
              @Override
              public boolean perform(final int id, final Value value) {
                newMerged.removeAssociatedValue(
                    id); // enforcing "one-value-per-file for particular key" invariant
                newMerged.addValue(id, value);
                return true;
              }
            });
      }
      setNeedsCompacting(fromDisk.needsCompacting());

      myMerged = newMerged;
      return newMerged;
    }
  }