All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ocadotechnology.indexedcache.IndexedImmutableObjectCache Maven / Gradle / Ivy

There is a newer version: 16.6.21
Show newest version
/*
 * Copyright © 2017-2023 Ocado (Ocava)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.ocadotechnology.indexedcache;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collector;
import java.util.stream.Stream;

import javax.annotation.CheckForNull;
import javax.annotation.ParametersAreNonnullByDefault;

import com.google.common.base.MoreObjects;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.UnmodifiableIterator;
import com.ocadotechnology.id.Identified;
import com.ocadotechnology.id.Identity;

/**
 * This implementation is not thread-safe.
 *
 * Calling any method which modifies the cache while another invocation is modifying the cache will cause a
 * ConcurrentModificationException.
 *
 * Calling any method which queries the cache while another invocation is modifying the cache will cause a
 * ConcurrentModificationException
 *
 * Calling a method which updates the cache while another invocation is querying the cache will not be detected.  This
 * is a limitation of the implementation, designed to be as performant as possible.  It is expected that thorough test
 * coverage of the calling code should detect this case as it seems unlikely that a multi-threaded approach could ensure
 * that the overlap only ever occurred in one direction
 *
 * Calling a method which queries the cache while another invocation is querying the cache is deliberately permitted.
 */
@ParametersAreNonnullByDefault
public class IndexedImmutableObjectCache, I> implements StateChangeListenable {
    /** There are several implementations of PredicateIndex and OptionalOneToOneIndex.
* There are optional methods to allow callers to hint about the expected workload * and for the cache to (possibly) take that into account. */ public enum Hints { optimiseForUpdate, optimiseForQuery, optimiseForInfrequentChanges } private final ObjectStore objectStore; private final List> indexes = new ArrayList<>(); private final List> stateChangeListeners = new ArrayList<>(); private final List> atomicStateChangeListeners = new ArrayList<>(); private final AtomicReference updatingThread = new AtomicReference<>(null); // The thread name of the thread currently updating the cache, null if no update is ongoing public static , I> IndexedImmutableObjectCache createHashMapBackedCache() { return new IndexedImmutableObjectCache<>(new HashMapObjectStore<>(128, 0.99f)); } public static , I> IndexedImmutableObjectCache createHashMapBackedCache(int initialSize, float fillFactor) { return new IndexedImmutableObjectCache<>(new HashMapObjectStore<>(initialSize, fillFactor)); } public IndexedImmutableObjectCache(ObjectStore objectStore) { this.objectStore = objectStore; } /** * Updates all of the provided values in the cache. Indexes will be updated and any appropriate cache update listeners will be run. * Accepts addint, updating or deleting objects in the cache. * * @param updates An collection of {@link Change} objects containing the value expected to be present in the cache and the value to be added * @throws CacheUpdateException if any expected values do not match the objects present in the cache */ public void updateAll(ImmutableCollection> updates) throws CacheUpdateException { try { updateStarting(); objectStore.updateAll(updates); updateIndexes(updates); } finally { updateComplete(); } } /** * Adds all of the provided values to the cache. Indexes will be updated and any appropriate cache update listeners will be run. * * @param newObjects the collection of values to be added to the cache * @throws CacheUpdateException if any objects are already present in the cache with matching ids */ public void addAll(ImmutableCollection newObjects) throws CacheUpdateException { try { updateStarting(); objectStore.addAll(newObjects); updateIndexes(newObjects.stream().map(Change::add).collect(ImmutableList.toImmutableList())); } finally { updateComplete(); } } /** * Adds the provided value to the cache. Indexes will be updated and any appropriate cache update listeners will be run. * * @param newObject the value to be added to the cache * @throws CacheUpdateException if there is an object stored in the cache with the provided id */ public void add(C newObject) throws CacheUpdateException { try { updateStarting(); objectStore.add(newObject); updateIndexes(newObject, null); } finally { updateComplete(); } } /** * Updates the provided value in the cache. Indexes will be updated and any appropriate cache update listeners will be run. * Accepts adding, updating or deleting an object in the cache. * * @param original the value expected to be in the cache, or null if the object is new * @param newObject the value to be added to the cache, or null if the object is to be deleted * @throws CacheUpdateException if the object stored in the cache with the new id does not match the provided original * @throws IllegalArgumentException if both original and newObject are null */ public void update(@CheckForNull C original, @CheckForNull C newObject) throws CacheUpdateException { try { updateStarting(); if (original == newObject) { return; } objectStore.update(original, newObject); updateIndexes(newObject, original); } finally { updateComplete(); } } /** * Removes all of the objects with the provided IDs from the cache. * * Removes from the cache all objects matching the identities in {@code ids}. * Indexes will be updated and any appropriate cache update listeners will be run. * * @param ids the collection of ids for values to be removed from the cache * @return a collection of the old values where any updates occurred, or else an empty collection if no updates were performed * @throws CacheUpdateException if any of the provided ids do not map to objects in the cache */ public ImmutableCollection deleteAll(ImmutableCollection> ids) throws CacheUpdateException { try { updateStarting(); ImmutableCollection oldObjects = objectStore.deleteAll(ids); updateIndexes(oldObjects.stream().map(Change::delete).collect(ImmutableList.toImmutableList())); return oldObjects; } finally { updateComplete(); } } /** * Removes the object with the provided ID from the cache. * * Indexes will be updated and any appropriate cache update listeners will be run. * * @param id the id for value to be removed from the cache * @return the old value corresponding to the provided id * @throws CacheUpdateException if the provided id does not map to one in the cache */ public C delete(Identity id) throws CacheUpdateException { try { updateStarting(); C old = objectStore.delete(id); updateIndexes(null, old); return old; } finally { updateComplete(); } } @Override public > T registerCustomIndex(T index) { return addIndex(index); } @Override @Deprecated public void registerStateAddedOrRemovedListener(Consumer consumer) { registerStateAddedListener(consumer::accept); registerStateRemovedListener(consumer::accept); } @Override public void registerStateAddedListener(CacheStateAddedListener listener) { this.stateChangeListeners.add((oldState, updatedState) -> { if (oldState == null) { listener.stateAdded(updatedState); } }); } @Override @SuppressWarnings("unchecked") public void registerStateChangeListener(CacheStateChangeListener listener) { // The cast is safe as the objects in the cache should be immutable this.stateChangeListeners.add((CacheStateChangeListener)listener); } /** Applies mapAndFilter to all updates, then calls listener (if either is non-null).
* Allows listeners to be written that are only notified when an object's type or property changes. */ public > void registerStateChangeListener(Function mapAndFilter, CacheStateChangeListener listener) { // The cast is safe as the objects in the cache should be immutable asFilteringListenable(mapAndFilter).registerStateChangeListener(listener); } @Override @SuppressWarnings("unchecked") public void registerAtomicStateChangeListener(AtomicStateChangeListener listener) { // The cast is safe as the objects in the cache should be immutable and the interface uses only immutable collections this.atomicStateChangeListeners.add((AtomicStateChangeListener)listener); } @Override public void registerStateRemovedListener(CacheStateRemovedListener listener) { this.stateChangeListeners.add(((oldState, updatedState) -> { if (updatedState == null) { listener.stateRemoved(oldState); } })); } @Override public void removeStateChangeListener(CacheStateChangeListener listener) { this.stateChangeListeners.remove(listener); } /** * @param mapAndFilter all arguments will be non-null and present in the cache. * The return from the function can be null (the null will either be passed to any listeners, or * the whole update will be ignored (eg, an addition, removal or null-to-null state change). * @return a listenable that will notify any registered listeners for a subset of the cache's contents (based on mapAndFilter) */ public > StateChangeListenable asFilteringListenable(Function mapAndFilter) { return new FilteringStateChangeListenable<>(this, mapAndFilter); } public PredicateIndex addPredicateIndex(Predicate predicate) { return addPredicateIndex(null, predicate); } public PredicateIndex addPredicateIndex(@CheckForNull String name, Predicate predicate) { return addPredicateIndex(name, predicate, Hints.optimiseForQuery); } /** * An optimiseForUpdate index has zero update overhead and streaming queries are performed by applying the predicate * function to every object in the parent cache.
* * An optimiseForQuery index caches a mapping from predicate result to object during update, so that streaming * queries can directly stream the pre-cached result.
* * An optimiseForInfrequentChanges index caches a mapping from predicate result to object id during update, so that * streaming queries can stream the pre-cached result and lookup the actual object in the parent cache. This makes * querying faster than an optimiseForUpdate index (especially when there are few matches), and updates faster * than an optimiseForQuery index (when the outcome of the predicate function does not change, the index cache * requires no changes).
*/ public PredicateIndex addPredicateIndex(Predicate predicate, Hints hint) { return addPredicateIndex(null, predicate, hint); } /** * An optimiseForUpdate index has zero update overhead and streaming queries are performed by applying the predicate * function to every object in the parent cache.
* * An optimiseForQuery index caches a mapping from predicate result to object during update, so that streaming * queries can directly stream the pre-cached result.
* * An optimiseForInfrequentChanges index caches a mapping from predicate result to object id during update, so that * streaming queries can stream the pre-cached result and lookup the actual object in the parent cache. This makes * querying faster than an optimiseForUpdate index (especially when there are few matches), and updates faster * than an optimiseForQuery index (when the outcome of the predicate function does not change, the index cache * requires no changes).
*/ public PredicateIndex addPredicateIndex(@CheckForNull String name, Predicate predicate, Hints hint) { switch (hint) { case optimiseForUpdate: // We could consider a lazy index (build on first query), but our currently use case doesn't justify that return addIndex(new UncachedPredicateIndex<>(name, this, predicate)); case optimiseForQuery: return addIndex(new DefaultPredicateIndex<>(name, predicate)); case optimiseForInfrequentChanges: return addIndex(new IdCachedPredicateIndex<>(name, this, predicate)); default: throw new UnsupportedOperationException("Missing case:" + hint); } } public PredicateValue addPredicateValue( Predicate predicate, Function extract, BiFunction leftAccumulate, BiFunction rightDecumulate, T initial) { return addPredicateValue(null, predicate, extract, leftAccumulate, rightDecumulate, initial); } public PredicateValue addPredicateValue( @CheckForNull String name, Predicate predicate, Function extract, BiFunction leftAccumulate, BiFunction rightDecumulate, T initial) { PredicateValue value = new PredicateValue<>(name, predicate, extract, leftAccumulate, rightDecumulate, initial); return addIndex(value); } public PredicateCountValue addPredicateCount(Predicate predicate) { return addPredicateCount(null, predicate); } public PredicateCountValue addPredicateCount(@CheckForNull String name, Predicate predicate) { PredicateCountValue counter = new PredicateCountValue<>(name, predicate); return addIndex(counter); } public ManyToManyIndex addManyToManyIndex(Function> function) { return addManyToManyIndex(null, function); } public ManyToManyIndex addManyToManyIndex(@CheckForNull String name, Function> function) { ManyToManyIndex index = new ManyToManyIndex<>(name, function); return addIndex(index); } /** * @param function key extraction function * @param comparator A comparator on a set of elements C which is consistent with equals(). * More formally, a total-order comparator on a set of elements C where * compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true. * This requirement is strictly enforced. Violating it will produce an IllegalStateException * and leave the cache in an inconsistent state. */ public OptionalSortedManyToManyIndex addOptionalSortedManyToManyIndex( Function>> function, Comparator comparator) { return addOptionalSortedManyToManyIndex(null, function, comparator); } /** * @param name optional String parameter - the name of the index. * @param function key extraction function * @param comparator A comparator on a set of elements C which is consistent with equals(). * More formally, a total-order comparator on a set of elements C where * compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true. * This requirement is strictly enforced. Violating it will produce an IllegalStateException * and leave the cache in an inconsistent state. */ public OptionalSortedManyToManyIndex addOptionalSortedManyToManyIndex( @CheckForNull String name, Function>> function, Comparator comparator) { OptionalSortedManyToManyIndex index = new OptionalSortedManyToManyIndex<>(name, function, comparator); return addIndex(index); } public OneToOneIndex addOneToOneIndex(Function function) { return addOneToOneIndex(null, function); } public OneToOneIndex addOneToOneIndex(@CheckForNull String name, Function function) { return addOneToOneIndex(name, function, Hints.optimiseForQuery); } public OneToOneIndex addOneToOneIndex(Function function, Hints hint) { return addOneToOneIndex(null, function, hint); } public OneToOneIndex addOneToOneIndex(@CheckForNull String name, Function function, Hints hint) { return addIndex(new OneToOneIndex<>(name, function, hint)); } public OneToManyIndex addOneToManyIndex(Function function) { return addOneToManyIndex(null, function); } public OneToManyIndex addOneToManyIndex(@CheckForNull String name, Function function) { return addIndex(OneToManyIndex.create(name, function)); } public ManyToOneIndex addManyToOneIndex(Function> function) { return addManyToOneIndex(null, function); } public ManyToOneIndex addManyToOneIndex(@CheckForNull String name, Function> function) { ManyToOneIndex index = new ManyToOneIndex<>(name, function); return addIndex(index); } public OptionalOneToManyIndex addOptionalOneToManyIndex(Function> function) { return addOptionalOneToManyIndex(null, function); } public OptionalOneToManyIndex addOptionalOneToManyIndex(@CheckForNull String name, Function> function) { OptionalOneToManyIndex index = new OptionalOneToManyIndex<>(name, function); return addIndex(index); } public OptionalOneToOneIndex addOptionalOneToOneIndex(Function> function) { return addOptionalOneToOneIndex(null, function); } public OptionalOneToOneIndex addOptionalOneToOneIndex(@CheckForNull String name, Function> function) { return addOptionalOneToOneIndex(name, function, Hints.optimiseForQuery); } /** An Index optimised for update is a little faster. Query times are equal.
* The update-optimised implementation does not separately validate uniqueness of value * (which is what makes a faster update possible). */ public OptionalOneToOneIndex addOptionalOneToOneIndex(Function> function, Hints hint) { return addOptionalOneToOneIndex(null, function, hint); } /** An Index optimised for update is a little faster. Query times are equal.
* The update-optimised implementation does not separately validate uniqueness of value * (which is what makes a faster update possible). */ public OptionalOneToOneIndex addOptionalOneToOneIndex(@CheckForNull String name, Function> function, Hints hint) { return addIndex(OptionalOneToOneIndexFactory.newOptionalOneToOneIndex(name, function, hint)); } /** * @param function key extraction function. * @param comparator A comparator on a set of elements C which is consistent with equals(). * More formally, a total-order comparator on a set of elements C where * compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true. * This requirement is strictly enforced. Violating it will produce an IllegalStateException * and leave the cache in an inconsistent state. */ public SortedOneToManyIndex addSortedOneToManyIndex( Function function, Comparator comparator) { return addSortedOneToManyIndex(null, function, comparator); } /** * @param name optional String parameter - the name of the index. * @param function key extraction function. * @param comparator A comparator on a set of elements C which is consistent with equals(). * More formally, a total-order comparator on a set of elements C where * compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true. * This requirement is strictly enforced. Violating it will produce an IllegalStateException * and leave the cache in an inconsistent state. */ public SortedOneToManyIndex addSortedOneToManyIndex( @CheckForNull String name, Function function, Comparator comparator) { SortedOneToManyIndex index = new SortedOneToManyIndex<>(name, function, comparator); return addIndex(index); } /** * @param function key extraction function. * @param comparatorGenerator generates a comparator for a given key * A comparator on a set of elements C which is consistent with equals(). * More formally, a total-order comparator on a set of elements C where * compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true. * This requirement is strictly enforced. Violating it will produce an IllegalStateException * and leave the cache in an inconsistent state. */ public SeparatelySortedOneToManyIndex addSeparatelySortedOneToManyIndex( Function function, Function> comparatorGenerator) { return addSeparatelySortedOneToManyIndex(null, function, comparatorGenerator); } /** * @param name optional String parameter - the name of the index. * @param function key extraction function. * @param comparatorGenerator generates a comparator for a given key * A comparator on a set of elements C which is consistent with equals(). * More formally, a total-order comparator on a set of elements C where * compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true. * This requirement is strictly enforced. Violating it will produce an IllegalStateException * and leave the cache in an inconsistent state. */ public SeparatelySortedOneToManyIndex addSeparatelySortedOneToManyIndex( @CheckForNull String name, Function function, Function> comparatorGenerator) { SeparatelySortedOneToManyIndex index = new SeparatelySortedOneToManyIndex<>(name, function, comparatorGenerator); return addIndex(index); } /** * @param function key extraction function * @param comparator A comparator on a set of elements C which is consistent with equals(). * More formally, a total-order comparator on a set of elements C where * compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true. * This requirement is strictly enforced. Violating it will produce an IllegalStateException * and leave the cache in an inconsistent state. */ public OptionalSortedOneToManyIndex addOptionalSortedOneToManyIndex( Function> function, Comparator comparator) { return addOptionalSortedOneToManyIndex(null, function, comparator); } /** * @param name optional String parameter - the name of the index. * @param function key extraction function * @param comparator A comparator on a set of elements C which is consistent with equals(). * More formally, a total-order comparator on a set of elements C where * compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true. * This requirement is strictly enforced. Violating it will produce an IllegalStateException * and leave the cache in an inconsistent state. */ public OptionalSortedOneToManyIndex addOptionalSortedOneToManyIndex( @CheckForNull String name, Function> function, Comparator comparator) { OptionalSortedOneToManyIndex index = new OptionalSortedOneToManyIndex<>(name, function, comparator); return addIndex(index); } public CachedGroupBy cacheGroupBy(Function groupByExtractor, Collector collector) { return cacheGroupBy(null, groupByExtractor, collector); } public CachedGroupBy cacheGroupBy(@CheckForNull String name, Function groupByExtractor, Collector collector) { CachedGroupBy cachedGroupByAggregation = new CachedGroupBy<>(name, groupByExtractor, collector); return addIndex(cachedGroupByAggregation); } public MappedPredicateIndex addMappedPredicateIndex(Predicate predicate, Function mappingFunction) { return addMappedPredicateIndex(null, predicate, mappingFunction); } public MappedPredicateIndex addMappedPredicateIndex(@CheckForNull String name, Predicate predicate, Function mappingFunction) { MappedPredicateIndex index = new MappedPredicateIndex<>(name, predicate, mappingFunction); return addIndex(index); } /** * @param comparator - A comparator on a set of elements C which is consistent with equals(). * More formally, a total-order comparator on a set of elements C where * compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true. * This requirement is strictly enforced. Violating it will produce an IllegalStateException * and leave the cache in an inconsistent state. */ public CachedSort addCacheSort(Comparator comparator) { return addCacheSort(null, comparator); } /** * @param name optional String parameter - the name of the index. * @param comparator - A comparator on a set of elements C which is consistent with equals(). * More formally, a total-order comparator on a set of elements C where * compare(c1, c2) == 0 implies that Objects.equals(c1, c2) == true. * This requirement is strictly enforced. Violating it will produce an IllegalStateException * and leave the cache in an inconsistent state. */ public CachedSort addCacheSort(@CheckForNull String name, Comparator comparator) { CachedSort cacheSort = new CachedSort<>(name, comparator); return addIndex(cacheSort); } public C get(@CheckForNull Identity id) { return objectStore.get(id); } public boolean containsId(@CheckForNull Identity id) { return objectStore.containsId(id); } public int size() { return objectStore.size(); } public boolean isEmpty() { return objectStore.size() == 0; } @Override public Stream stream() { return objectStore.stream(); } @Override public UnmodifiableIterator iterator() { return objectStore.iterator(); } @Override public void forEach(Consumer action) { objectStore.forEach(action); } @SuppressWarnings("unchecked") private > T addIndex(T index) { try { updateStarting(); // The cast is safe as the objects in the cache should be immutable and the interface uses only immutable collections Index castedIndex = (Index) index; indexes.add(castedIndex); //Do not collect to an ImmutableSet - Guava's default collector does not infer the stream size //which results in a lot of collisions and array extensions. ImmutableList> allStates = objectStore.stream().map(Change::add).collect(ImmutableList.toImmutableList()); try { castedIndex.updateAll(allStates); } catch (IndexUpdateException e) { throw new IllegalStateException("Failed to add new index", e); } return index; } finally { updateComplete(); } } private void updateIndexes(@CheckForNull C newValue, @CheckForNull C oldValue) { for (int i = 0; i < indexes.size(); ++i) { try { indexes.get(i).update(newValue, oldValue); } catch (IndexUpdateException e) { rollbackSingleUpdate(newValue, oldValue, i, e); throw new CacheUpdateException("Failed to update indices", e); } } updateStateChangeListeners(oldValue, newValue); updateAtomicStateChangeListeners(oldValue, newValue); } private void updateAtomicStateChangeListeners(@CheckForNull C oldValue, @CheckForNull C newValue) { if (atomicStateChangeListeners.isEmpty()) { return; } ImmutableList> changes = ImmutableList.of(Change.change(oldValue, newValue)); atomicStateChangeListeners.forEach(l -> l.stateChanged(changes)); } private void updateIndexes(ImmutableCollection> changes) { for (int i = 0; i < indexes.size(); ++i) { try { indexes.get(i).updateAll(changes); } catch (IndexUpdateException e) { rollbackBatchUpdate(changes, i, e); throw new CacheUpdateException("Failed to update indices", e); } } changes.forEach(update -> updateStateChangeListeners(update.originalObject, update.newObject)); if (!atomicStateChangeListeners.isEmpty()) { atomicStateChangeListeners.forEach(l -> l.stateChanged(changes)); } } private void updateStateChangeListeners(@CheckForNull C oldState, @CheckForNull C newState) { stateChangeListeners.forEach(s -> s.stateChanged(oldState, newState)); } private void rollbackSingleUpdate(@CheckForNull C newValue, @CheckForNull C oldValue, int failedIndexNumber, IndexUpdateException cause) { try { for (int i = 0; i < failedIndexNumber; ++i) { indexes.get(i).update(oldValue, newValue); } objectStore.update(newValue, oldValue); } catch (IndexUpdateException | CacheUpdateException e) { throw new IllegalStateException("Failed to rollback changes after index failure: " + cause.getMessage(), e); } } private void rollbackBatchUpdate(ImmutableCollection> changes, int failedIndexNumber, IndexUpdateException cause) { ImmutableList> reverseChanges = changes.stream() .map(Change::inverse) .collect(ImmutableList.toImmutableList()); try { for (int i = 0; i < failedIndexNumber; ++i) { indexes.get(i).updateAll(reverseChanges); } objectStore.updateAll(reverseChanges); } catch (IndexUpdateException | CacheUpdateException e) { throw new IllegalStateException("Failed to rollback changes after index failure: " + cause.getMessage(), e); } } public void clear() { try { updateStarting(); ImmutableCollection> clearedObjects = objectStore.stream().map(Change::delete).collect(ImmutableList.toImmutableList()); objectStore.clear(); updateIndexes(clearedObjects); } finally { updateComplete(); } } @Override public String toString() { return MoreObjects.toStringHelper(this) .add("objects", objectStore.size()) .toString(); } public ImmutableMap, C> snapshotObjects() { return objectStore.snapshot(); } private void updateStarting() { if (!updatingThread.compareAndSet(null, Thread.currentThread().getName())) { failUpdate(); } } /** * Method separated out to make it easier for the JVM to inline the updateStarting method for performance */ private void failUpdate() { throw new ConcurrentModificationException( String.format("Attempting to update cache while another update is ongoing. currentThread=[%s] otherThread=[%s]", Thread.currentThread().getName(), updatingThread)); } private void updateComplete() { updatingThread.compareAndSet(Thread.currentThread().getName(), null); } }