/* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ package com.facebook.common.references; import javax.annotation.concurrent.GuardedBy; import java.util.IdentityHashMap; import java.util.Map; import com.facebook.common.internal.VisibleForTesting; import com.facebook.common.internal.Preconditions; import com.facebook.common.logging.FLog; /** * A shared-reference class somewhat similar to c++ shared_ptr. The underlying value is reference * counted, and when the count drops to zero, the underlying value is "disposed" * <p> * Unlike the c++ implementation, which provides for a bunch of syntactic sugar with copy * constructors and destructors, Java does not provide the equivalents. So we instead have the * explicit addReference() and deleteReference() calls, and we need to be extremely careful * about using these in the presence of exceptions, or even otherwise. * <p> * Despite the extra (and clunky) method calls, this is still worthwhile in many cases to avoid * the overhead of garbage collection. * <p> * The somewhat clunky rules are * 1. If a function returns a SharedReference, it must guarantee that the reference count * is at least 1. In the case where a SharedReference is being constructed and returned, * the SharedReference constructor will already set the ref count to 1. * 2. If a function calls another function with a shared-reference parameter, * 2.1 The caller must ensure that the reference is valid for the duration of the * invocation. * 2.2 The callee *is not* responsible for the cleanup of the reference. * 2.3 If the callee wants to keep the reference around even after the call returns (for * example, stashing it away in a map), then it should "clone" the reference by invoking * {@link #addReference()} * <p> * Example #1 (function with a shared reference parameter): * void foo(SharedReference r, ...) { * // first assert that the reference is valid * Preconditions.checkArgument(SharedReference.isValid(r)); * ... * // do something with the contents of r * ... * // do not increment/decrement the ref count * } * <p> * Example #2 (function with a shared reference parameter that keeps around the shared ref) * void foo(SharedReference r, ...) { * // first assert that the reference is valid * Preconditions.checkArgument(SharedReference.isValid(r)); * ... * // increment ref count * r.addReference(); * // stash away the reference * ... * return; * } * <p> * Example #3 (function with a shared reference parameter that passes along the reference to * another function) * void foo(SharedReference r, ...) { * // first assert that the reference is valid * Preconditions.checkArgument(SharedReference.isValid(r)); * ... * bar(r, ...); // call to other function * ... * } * <p> * Example #4 (function that returns a shared reference) * SharedReference foo(...) { * // do something * ... * // create a new shared reference (refcount automatically at 1) * SharedReference r = new SharedReference(x); * // return this shared reference * return r; * } * <p> * Example #5 (function with a shared reference parameter that returns the shared reference) * void foo(SharedReference r, ...) { * // first assert that the reference is valid * Preconditions.checkArgument(SharedReference.isValid(r)); * ... * // increment ref count before returning * r.addReference(); * return r; * } */ @VisibleForTesting public class SharedReference<T> { // Keeps references to all live objects so finalization of those Objects always happens after // SharedReference first disposes of it. Note, this does not prevent CloseableReference's from // being finalized when the reference is no longer reachable. @GuardedBy("itself") private static final Map<Object, Integer> sLiveObjects = new IdentityHashMap<>(); @GuardedBy("this") private T mValue; @GuardedBy("this") private int mRefCount; private final ResourceReleaser<T> mResourceReleaser; /** * Construct a new shared-reference that will 'own' the supplied {@code value}. * The reference count will be set to 1. When the reference count decreases to zero * {@code resourceReleaser} will be used to release the {@code value} * @param value non-null value to manage * @param resourceReleaser non-null ResourceReleaser for the value */ public SharedReference(T value, ResourceReleaser<T> resourceReleaser) { mValue = Preconditions.checkNotNull(value); mResourceReleaser = Preconditions.checkNotNull(resourceReleaser); mRefCount = 1; addLiveReference(value); } /** * Increases the reference count of a live object in the static map. Adds it if it's not * being held. * * @param value the value to add. */ private static void addLiveReference(Object value) { synchronized (sLiveObjects) { Integer count = sLiveObjects.get(value); if (count == null) { sLiveObjects.put(value, 1); } else { sLiveObjects.put(value, count + 1); } } } /** * Decreases the reference count of live object from the static map. Removes it if it's reference * count has become 0. * * @param value the value to remove. */ private static void removeLiveReference(Object value) { synchronized (sLiveObjects) { Integer count = sLiveObjects.get(value); if (count == null) { // Uh oh. FLog.wtf( "SharedReference", "No entry in sLiveObjects for value of type %s", value.getClass()); } else if (count == 1) { sLiveObjects.remove(value); } else { sLiveObjects.put(value, count - 1); } } } /** * Get the current referenced value. Null if there's no value. * @return the referenced value */ public synchronized T get() { return mValue; } /** * Checks if this shared-reference is valid i.e. its reference count is greater than zero. * @return true if shared reference is valid */ public synchronized boolean isValid() { return mRefCount > 0; } /** * Checks if the shared-reference is valid i.e. its reference count is greater than zero * @return true if the shared reference is valid */ public static boolean isValid(SharedReference<?> ref) { return ref != null && ref.isValid(); } /** * Bump up the reference count for the shared reference * Note: The reference must be valid (aka not null) at this point */ public synchronized void addReference() { ensureValid(); mRefCount++; } /** * Decrement the reference count for the shared reference. If the reference count drops to zero, * then dispose of the referenced value */ public void deleteReference() { if (decreaseRefCount() == 0) { T deleted; synchronized (this) { deleted = mValue; mValue = null; } mResourceReleaser.release(deleted); removeLiveReference(deleted); } } /** * Decrements reference count for the shared reference. Returns value of mRefCount after * decrementing */ private synchronized int decreaseRefCount() { ensureValid(); Preconditions.checkArgument(mRefCount > 0); mRefCount--; return mRefCount; } /** * Assert that there is a valid referenced value. Throw a NullReferenceException otherwise * @throws NullReferenceException, if the reference is invalid (i.e.) the underlying value is null */ private void ensureValid() { if (!isValid(this)) { throw new NullReferenceException(); } } /** * A test-only method to get the ref count * DO NOT USE in regular code */ public synchronized int getRefCountTestOnly() { return mRefCount; } /** * The moral equivalent of NullPointerException for SharedReference. Indicates that the * referenced object is null */ public static class NullReferenceException extends RuntimeException { public NullReferenceException() { super("Null shared reference"); } } }