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

org.exist.util.WeakLazyStripes Maven / Gradle / Ivy

There is a newer version: 6.3.0
Show newest version
/*
 * eXist Open Source Native XML Database
 * Copyright (C) 2001-2017 The eXist Project
 * http://exist-db.org
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

package org.exist.util;

import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.jcip.annotations.GuardedBy;
import net.jcip.annotations.ThreadSafe;

import javax.annotation.Nullable;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.StampedLock;
import java.util.function.Function;

/**
 * Inspired by Guava's com.google.common.util.concurrent.Striped#lazyWeakReadWriteLock(int)
 * implementation.
 * See https://google.github.io/guava/releases/21.0/api/docs/com/google/common/util/concurrent/Striped.html#lazyWeakReadWriteLock-int-.
 *
 * However this is much simpler, and there is no hashing; we
 * will always return the same object (stripe) for the same key.
 *
 * This class basically couples Weak References with a
 * thread safe HashMap and manages draining expired Weak
 * References from the HashMap.
 *
 * Weak References will be cleaned up from the internal map
 * after they have been cleared by the GC. Two cleanup policies
 * are provided: "Batch" and "Amortize". The policy is chosen
 * by the constructor parameter {@code amortizeCleanup}.
 *
 * Batch Cleanup
 *     With Batch Cleanup, expired Weak References will
 *     be collected up to the {@link #MAX_EXPIRED_REFERENCE_READ_COUNT}
 *     limit, at which point the calling thread which causes
 *     that ceiling to be detected will cleanup all expired references.
 *
 * Amortize Cleanup
 *     With Amortize Cleanup, each calling thread will attempt
 *     to cleanup up to {@link #DRAIN_MAX} expired weak
 *     references on each write operation, or after
 *     {@link #READ_DRAIN_THRESHOLD} since the last cleanup.
 *
 * With either cleanup policy, only a single calling thread
 * performs the cleanup at any time.
 *
 * @param  The type of the key for the stripe.
 * @param  The type of the stripe.
 *
 * @author Adam Retter
 */
@ThreadSafe
public class WeakLazyStripes {
    private static final int INITIAL_CAPACITY = 1000;
    private static final float LOAD_FACTOR = 0.75f;

    /**
     * When {@link #amortizeCleanup} is false, this is the
     * number of reads allowed which return expired references
     * before calling {@link #drainClearedReferences()}.
     */
    private static final int MAX_EXPIRED_REFERENCE_READ_COUNT = 1000;

    /**
     * When {@link #amortizeCleanup} is true, this is the
     * number of reads which are performed between calls
     * to {@link #drainClearedReferences()}.
     */
    private static final int READ_DRAIN_THRESHOLD = 64;

    /**
     * When {@link #amortizeCleanup} is true, this is the
     * maximum number of entries to be drained
     * by {@link #drainClearedReferences()}.
     */
    private static final int DRAIN_MAX = 16;

    private final ReferenceQueue referenceQueue;

    private final StampedLock stripesLock = new StampedLock();
    @GuardedBy("stripesLock") private final Object2ObjectOpenHashMap> stripes;

    /**
     * The number of reads on {@link #stripes} which have returned
     * expired weak references.
     */
    private final AtomicInteger expiredReferenceReadCount = new AtomicInteger();

    /**
     * The number of reads on {@link #stripes} since
     * {@link #drainClearedReferences()} was last
     * completed.
     */
    private final AtomicInteger readCount = new AtomicInteger();

    private final Function creator;
    private final boolean amortizeCleanup;

    /**
     * Guard so that only a single thread drains
     * references at once.
     */
    private final AtomicBoolean draining = new AtomicBoolean();

    /**
     * Constructs a WeakLazyStripes where the concurrencyLevel
     * is the lower of either ConcurrentHashMap#DEFAULT_CONCURRENCY_LEVEL
     * or {@code Runtime.getRuntime().availableProcessors() * 2}.
     *
     * @param creator A factory for creating new Stripes when needed
     */
    public WeakLazyStripes(final Function creator) {
       this(Math.min(16, Runtime.getRuntime().availableProcessors() * 2), creator);  // 16 == ConcurrentHashMap#DEFAULT_CONCURRENCY_LEVEL
    }

    /**
     * Constructs a WeakLazyStripes.
     *
     * @param concurrencyLevel The concurrency level for the underlying stripes map
     * @param creator A factory for creating new Stripes when needed
     */
    public WeakLazyStripes(final int concurrencyLevel, final Function creator) {
        this(concurrencyLevel, creator, true);
    }

    /**
     * Constructs a WeakLazyStripes.
     *
     * @param concurrencyLevel The concurrency level for the underlying stripes map
     * @param creator A factory for creating new Stripes when needed
     * @param amortizeCleanup true if the cleanup of weak references should be
     *     amortized across many calls (default), false if the cleanup should be batched up
     *     and apportioned to a particular caller at a threshold
     */
    public WeakLazyStripes(final int concurrencyLevel, final Function creator, final boolean amortizeCleanup) {
        this.stripes = new Object2ObjectOpenHashMap<>(INITIAL_CAPACITY, LOAD_FACTOR);
        this.referenceQueue = new ReferenceQueue<>();
        this.creator = creator;
        this.amortizeCleanup = amortizeCleanup;
    }

    /**
     * Get the stripe for the given key
     *
     * If the stripe does not exist, it will be created by
     * calling {@link Function#apply(Object)} on {@link #creator}
     *
     * @param key the key for the stripe
     * @return the stripe
     */
    public S get(final K key) {

        while (true) {

            final Holder written = new Holder<>(false);

            // 1) attempt lookup via optimistic read and immediate conversion to write lock
            WeakValueReference stripeRef = getOptimistic(key, written);
            if (stripeRef == null) {

                // 2) attempt lookup via pessimistic read and immediate conversion to write lock
                stripeRef = getPessimistic(key, written);
                if (stripeRef == null) {

                    // 3) attempt lookup via exclusive write lock
                    stripeRef = getExclusive(key, written);
                }
            }

            if (amortizeCleanup) {
                if (written.value) {
                    // TODO (AR) if we find that we are too frequently draining and it is expensive
                    // then we could make the read and write drain paths both use the DRAIN_THRESHOLD
                    drainClearedReferences();
                } else if (readCount.get() >= READ_DRAIN_THRESHOLD) {
                    drainClearedReferences();
                }
            } else {
                // have we reached the threshold where we should clear
                // out any cleared WeakReferences from the stripes map
                final int count = expiredReferenceReadCount.get();
                if (count > MAX_EXPIRED_REFERENCE_READ_COUNT
                        && expiredReferenceReadCount.compareAndSet(count, 0)) {
                    drainClearedReferences();
                }
            }

            // check the weak reference before returning!
            final S stripe = stripeRef.get();
            if (stripe != null) {
                return stripe;
            }

            // weak reference has expired in the mean time, so loop...
        }
    }

    /**
     * Get the stripe via immediate conversion of an optimistic read lock to a write lock.
     *
     * @param key the stripe key
     * @param written (OUT) will be set to true if {@link #stripes} was updated
     *
     * @return null if we could not perform an optimistic read, or a new object needed to be
     *     created and we could not take the {@link #stripesLock} write lock immediately,
     *     otherwise the stripe.
     */
    private @Nullable WeakValueReference getOptimistic(final K key, final Holder written) {
        // optimistic read
        final long stamp = stripesLock.tryOptimisticRead();
        WeakValueReference stripeRef = stripes.get(key);
        if (stripeRef == null || stripeRef.get() == null) {
            final long writeStamp = stripesLock.tryConvertToWriteLock(stamp);
            if (writeStamp != 0l) {
                final boolean wasGCd = stripeRef != null && stripeRef.get() == null;
                try {
                    stripeRef = new WeakValueReference<>(key, creator.apply(key), referenceQueue);
                    stripes.put(key, stripeRef);
                } finally {
                    stripesLock.unlockWrite(writeStamp);
                }

                written.value = true;

                if (wasGCd && !amortizeCleanup) {
                    expiredReferenceReadCount.incrementAndGet();
                }
            } else {
                // invalid conversion to write lock... small optimisation for the fall-through to #getPessimistic(K, Holder) in #get(K)
                stripeRef = null;
            }
        } else {
            if (stripesLock.validate(stamp)) {
                if (amortizeCleanup) {
                    readCount.incrementAndGet();
                }
            } else {
                // invalid optimistic read
                stripeRef = null;
            }
        }

        return stripeRef;
    }

    /**
     * Get the stripe via immediate conversion of a read lock to a write lock.
     *
     * @param key the stripe key
     * @param written (OUT) will be set to true if {@link #stripes} was updated
     *
     * @return null if a new object needed to be created and we could not take the {@link #stripesLock}
     *     write lock immediately, otherwise the stripe.
     */
    private @Nullable WeakValueReference getPessimistic(final K key, final Holder written) {
        WeakValueReference stripeRef;
        long stamp = stripesLock.readLock();
        try {
            stripeRef = stripes.get(key);
            if (stripeRef == null || stripeRef.get() == null) {
                final long writeStamp = stripesLock.tryConvertToWriteLock(stamp);
                if (writeStamp != 0l) {
                    final boolean wasGCd = stripeRef != null && stripeRef.get() == null;

                    stamp = writeStamp;  // NOTE: this causes the write lock to be released in the finally further down
                    stripeRef = new WeakValueReference<>(key, creator.apply(key), referenceQueue);
                    stripes.put(key, stripeRef);

                    written.value = true;

                    if (wasGCd && !amortizeCleanup) {
                        expiredReferenceReadCount.incrementAndGet();
                    }
                } else {
                    // invalid conversion to write lock... small optimisation for the fall-through to #getExclusive(K, Holder) in #get(K)
                    stripeRef = null;
                }

                return stripeRef;
            }
        } finally {
            stripesLock.unlock(stamp);
        }

        // else (we don't need the lock on this path)
        if (amortizeCleanup) {
            readCount.incrementAndGet();
        }

        return stripeRef;
    }

    /**
     * Get the stripe whilst holding the write lock.
     *
     * @param key the stripe key
     * @param written (OUT) will be set to true if {@link #stripes} was updated
     *
     * @return the stripe
     */
    private WeakValueReference getExclusive(final K key, final Holder written) {
        WeakValueReference stripeRef;
        final long writeStamp = stripesLock.writeLock();
        try {
            stripeRef = stripes.get(key);
            if (stripeRef == null || stripeRef.get() == null) {
                final boolean wasGCd = stripeRef != null && stripeRef.get() == null;

                stripeRef = new WeakValueReference<>(key, creator.apply(key), referenceQueue);
                stripes.put(key, stripeRef);

                written.value = true;

                if (wasGCd && !amortizeCleanup) {
                    expiredReferenceReadCount.incrementAndGet();
                }

                return stripeRef;
            }
        } finally {
            stripesLock.unlockWrite(writeStamp);
        }

        // else (we don't need the write lock on this path)
        if (amortizeCleanup) {
            readCount.incrementAndGet();
        }
        return stripeRef;
    }

    /**
     * Removes cleared WeakReferences
     * from the stripes map.
     *
     * If {@link #amortizeCleanup} is false, then
     * all cleared WeakReferences will be removed,
     * otherwise up to {@link #DRAIN_MAX} are removed.
     */
    private void drainClearedReferences() {
        if (draining.compareAndSet(false, true)) {  // critical section
            Reference ref;
            int i = 0;
            while ((ref = referenceQueue.poll()) != null) {
                @SuppressWarnings("unchecked") final WeakValueReference stripeRef = (WeakValueReference) ref;

                final long writeStamp = stripesLock.writeLock();
                try {

                    // TODO(AR) it may be more performant to call #drainClearedReferences() at the beginning of #get(K) as oposed to the end, then we could avoid the extra check here which calls stripes#get(K)

                    /*
                        NOTE: we have to check that we have not added a new reference to replace an
                        expired reference in #get(K) before calling #drainClearedReferences()
                     */
                    final WeakValueReference check = stripes.get(stripeRef.key);
                    if (check != null && check.get() == null) {

                        stripes.remove(stripeRef.key);

                    }
                } finally {
                    stripesLock.unlockWrite(writeStamp);
                }

                if (amortizeCleanup && ++i == DRAIN_MAX) {
                    break;
                }
            }
            if (amortizeCleanup) {
                readCount.set(0);
            }
            draining.set(false);
        }
    }

    /**
     * Extends a WeakReference with a strong reference to a key.
     *
     * Used for cleaning up the {@link #stripes} from the {@link #referenceQueue}.
     */
    private static class WeakValueReference extends WeakReference {
        final K key;
        public WeakValueReference(final K key, final V referent, final ReferenceQueue q) {
            super(referent, q);
            this.key = key;
        }
    }
}