public void putFileData(final FileData data, Priority priority,
      LowLevelCacheCounters qfCounters, CacheTag tag) {
    // TODO: buffers are accounted for at allocation time, but ideally we should report the memory
    //       overhead from the java objects to memory manager and remove it when discarding file.
    if (data.stripes == null || data.stripes.isEmpty()) {
      LlapIoImpl.LOG.warn("Trying to cache FileData with no data for " + data.fileKey);
      return;
    }
    FileCache<FileData> subCache = null;
    FileData cached = null;
    data.rwLock.writeLock().lock();
    try {
      subCache = FileCache.getOrAddFileSubCache(
          cache, data.fileKey, new Function<Void, FileData>() {
        @Override
        public FileData apply(Void input) {
          return data; // If we don't have a file cache, we will add this one as is.
        }
      }, tag);
      cached = subCache.getCache();
    } finally {
      if (data != cached) {
        data.rwLock.writeLock().unlock();
      }
    }
    try {
      if (data != cached) {
        cached.rwLock.writeLock().lock();
      }
      try {
        for (StripeData si : data.stripes) {
          lockAllBuffersForPut(si, priority, subCache);
        }
        if (data == cached) {
          if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) {
            LlapIoImpl.CACHE_LOGGER.trace("Cached new data " + data);
          }
          return;
        }
        if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) {
          LlapIoImpl.CACHE_LOGGER.trace("Merging old " + cached + " and new " + data);
        }
        ArrayList<StripeData> combined = new ArrayList<>(
            cached.stripes.size() + data.stripes.size());
        combined.addAll(cached.stripes);
        combined.addAll(data.stripes);
        Collections.sort(combined, new StripeInfoComparator());
        int lastIx = combined.size() - 1;
        for (int ix = 0; ix < lastIx; ++ix) {
          StripeData cur = combined.get(ix), next = combined.get(ix + 1);
          if (cur.lastEnd <= next.firstStart) continue; // All good.
          if (cur.firstStart == next.firstStart && cur.lastEnd == next.lastEnd) {
            mergeStripeInfos(cur, next);
            combined.remove(ix + 1);
            --lastIx;
            // Don't recheck with next, only 2 lists each w/o collisions.
            continue;
          }
          // The original lists do not contain collisions, so only one is 'old'.
          boolean isCurOriginal = cached.stripes.contains(cur);
          handleRemovedStripeInfo(combined.remove(isCurOriginal ? ix : ix + 1));
          --ix;
          --lastIx;
        }
        cached.stripes = combined;
        if (LlapIoImpl.CACHE_LOGGER.isTraceEnabled()) {
          LlapIoImpl.CACHE_LOGGER.trace("New cache data is " + combined);
        }
      } finally {
        cached.rwLock.writeLock().unlock();
      }
    } finally {
      subCache.decRef();
    }
  }
RW lock ensures we have a consistent view of the file data, which is important given that
we generate "stripe" boundaries arbitrarily. Reading buffer data itself doesn't require
that this lock is held; however, everything else in stripes list does.
TODO: make more granular? We only care that each one reader sees consistent boundaries.
      So, we could shallow-copy the stripes list, then have individual locks inside each.
llap-server/src/java/org/apache/hadoop/hive/llap/cache/SerDeLowLevelCacheImpl.java