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

org.eclipse.jetty.util.Pool Maven / Gradle / Ivy

There is a newer version: 12.0.13
Show newest version
//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.util;

import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;

import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.component.DumpableCollection;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.Locker;

/**
 * A fast pool of objects, with optional support for
 * multiplexing, max usage count and several optimized strategies plus
 * an optional {@link ThreadLocal} cache of the last release entry.
 * 

* When the method {@link #close()} is called, all {@link Closeable}s in the pool * are also closed. *

* @param */ public class Pool implements AutoCloseable, Dumpable { private static final Logger LOGGER = Log.getLogger(Pool.class); private final List entries = new CopyOnWriteArrayList<>(); private final int maxEntries; private final AtomicInteger pending = new AtomicInteger(); private final StrategyType strategyType; /* * The cache is used to avoid hammering on the first index of the entry list. * Caches can become poisoned (i.e.: containing entries that are in use) when * the release isn't done by the acquiring thread or when the entry pool is * undersized compared to the load applied on it. * When an entry can't be found in the cache, the global list is iterated * with the configured strategy so the cache has no visible effect besides performance. */ private final Locker locker = new Locker(); private final ThreadLocal cache; private final AtomicInteger nextIndex; private volatile boolean closed; private volatile int maxMultiplex = 1; private volatile int maxUsageCount = -1; /** * The type of the strategy to use for the pool. * The strategy primarily determines where iteration over the pool entries begins. */ public enum StrategyType { /** * A strategy that looks for an entry always starting from the first entry. * It will favour the early entries in the pool, but may contend on them more. */ FIRST, /** * A strategy that looks for an entry by iterating from a random starting * index. No entries are favoured and contention is reduced. */ RANDOM, /** * A strategy that uses the {@link Thread#getId()} of the current thread * to select a starting point for an entry search. Whilst not as performant as * using the {@link ThreadLocal} cache, it may be suitable when the pool is substantially smaller * than the number of available threads. * No entries are favoured and contention is reduced. */ THREAD_ID, /** * A strategy that looks for an entry by iterating from a starting point * that is incremented on every search. This gives similar results to the * random strategy but with more predictable behaviour. * No entries are favoured and contention is reduced. */ ROUND_ROBIN, } /** * Construct a Pool with a specified lookup strategy and no * {@link ThreadLocal} cache. * * @param strategyType The strategy to used for looking up entries. * @param maxEntries the maximum amount of entries that the pool will accept. */ public Pool(StrategyType strategyType, int maxEntries) { this(strategyType, maxEntries, false); } /** * Construct a Pool with the specified thread-local cache size and * an optional {@link ThreadLocal} cache. * @param strategyType The strategy to used for looking up entries. * @param maxEntries the maximum amount of entries that the pool will accept. * @param cache True if a {@link ThreadLocal} cache should be used to try the most recently released entry. */ public Pool(StrategyType strategyType, int maxEntries, boolean cache) { this.maxEntries = maxEntries; this.strategyType = strategyType; this.cache = cache ? new ThreadLocal<>() : null; nextIndex = strategyType == StrategyType.ROUND_ROBIN ? new AtomicInteger() : null; } public int getReservedCount() { return pending.get(); } public int getIdleCount() { return (int)entries.stream().filter(Entry::isIdle).count(); } public int getInUseCount() { return (int)entries.stream().filter(Entry::isInUse).count(); } public int getMaxEntries() { return maxEntries; } public int getMaxMultiplex() { return maxMultiplex; } public final void setMaxMultiplex(int maxMultiplex) { if (maxMultiplex < 1) throw new IllegalArgumentException("Max multiplex must be >= 1"); this.maxMultiplex = maxMultiplex; } public int getMaxUsageCount() { return maxUsageCount; } public final void setMaxUsageCount(int maxUsageCount) { if (maxUsageCount == 0) throw new IllegalArgumentException("Max usage count must be != 0"); this.maxUsageCount = maxUsageCount; } /** * Create a new disabled slot into the pool. * The returned entry must ultimately have the {@link Entry#enable(Object, boolean)} * method called or be removed via {@link Pool.Entry#remove()} or * {@link Pool#remove(Pool.Entry)}. * * @param allotment the desired allotment, where each entry handles an allotment of maxMultiplex, * or a negative number to always trigger the reservation of a new entry. * @return a disabled entry that is contained in the pool, * or null if the pool is closed or if the pool already contains * {@link #getMaxEntries()} entries, or the allotment has already been reserved */ public Entry reserve(int allotment) { try (Locker.Lock l = locker.lock()) { if (closed) return null; int space = maxEntries - entries.size(); if (space <= 0) return null; // The pending count is an AtomicInteger that is only ever incremented here with // the lock held. Thus the pending count can be reduced immediately after the // test below, but never incremented. Thus the allotment limit can be enforced. if (allotment >= 0 && (pending.get() * getMaxMultiplex()) >= allotment) return null; pending.incrementAndGet(); Entry entry = new Entry(); entries.add(entry); return entry; } } /** * Acquire the entry from the pool at the specified index. This method bypasses the thread-local mechanism. * @deprecated No longer supported. Instead use a {@link StrategyType} to configure the pool. * @param idx the index of the entry to acquire. * @return the specified entry or null if there is none at the specified index or if it is not available. */ @Deprecated public Entry acquireAt(int idx) { if (closed) return null; try { Entry entry = entries.get(idx); if (entry.tryAcquire()) return entry; } catch (IndexOutOfBoundsException e) { // no entry at that index } return null; } /** * Acquire an entry from the pool. * Only enabled entries will be returned from this method and their enable method must not be called. * @return an entry from the pool or null if none is available. */ public Entry acquire() { if (closed) return null; int size = entries.size(); if (size == 0) return null; if (cache != null) { Pool.Entry entry = cache.get(); if (entry != null && entry.tryAcquire()) return entry; } int index = startIndex(size); for (int tries = size; tries-- > 0;) { try { Pool.Entry entry = entries.get(index); if (entry != null && entry.tryAcquire()) return entry; } catch (IndexOutOfBoundsException e) { LOGGER.ignore(e); size = entries.size(); } index = (index + 1) % size; } return null; } private int startIndex(int size) { switch (strategyType) { case FIRST: return 0; case RANDOM: return ThreadLocalRandom.current().nextInt(size); case ROUND_ROBIN: return nextIndex.getAndUpdate(c -> Math.max(0, c + 1)) % size; case THREAD_ID: return (int)(Thread.currentThread().getId() % size); default: throw new IllegalArgumentException("Unknown strategy type: " + strategyType); } } /** * Utility method to acquire an entry from the pool, * reserving and creating a new entry if necessary. * * @param creator a function to create the pooled value for a reserved entry. * @return an entry from the pool or null if none is available. */ public Entry acquire(Function.Entry, T> creator) { Entry entry = acquire(); if (entry != null) return entry; entry = reserve(-1); if (entry == null) return null; T value; try { value = creator.apply(entry); } catch (Throwable th) { remove(entry); throw th; } if (value == null) { remove(entry); return null; } return entry.enable(value, true) ? entry : null; } /** * This method will return an acquired object to the pool. Objects * that are acquired from the pool but never released will result * in a memory leak. * * @param entry the value to return to the pool * @return true if the entry was released and could be acquired again, * false if the entry should be removed by calling {@link #remove(Pool.Entry)} * and the object contained by the entry should be disposed. * @throws NullPointerException if value is null */ public boolean release(Entry entry) { if (closed) return false; boolean released = entry.tryRelease(); if (released && cache != null) cache.set(entry); return released; } /** * Remove a value from the pool. * * @param entry the value to remove * @return true if the entry was removed, false otherwise */ public boolean remove(Entry entry) { if (closed) return false; if (!entry.tryRemove()) { if (LOGGER.isDebugEnabled()) LOGGER.debug("Attempt to remove an object from the pool that is still in use: {}", entry); return false; } boolean removed = entries.remove(entry); if (!removed && LOGGER.isDebugEnabled()) LOGGER.debug("Attempt to remove an object from the pool that does not exist: {}", entry); return removed; } public boolean isClosed() { return closed; } @Override public void close() { List copy; try (Locker.Lock l = locker.lock()) { closed = true; copy = new ArrayList<>(entries); entries.clear(); } // iterate the copy and close its entries for (Entry entry : copy) { if (entry.tryRemove() && entry.pooled instanceof Closeable) IO.close((Closeable)entry.pooled); } } public int size() { return entries.size(); } public Collection values() { return Collections.unmodifiableCollection(entries); } @Override public void dump(Appendable out, String indent) throws IOException { Dumpable.dumpObjects(out, indent, this, new DumpableCollection("entries", entries)); } @Override public String toString() { return String.format("%s@%x[size=%d closed=%s pending=%d]", getClass().getSimpleName(), hashCode(), entries.size(), closed, pending.get()); } public class Entry { // hi: positive=open/maxUsage counter; negative=closed; MIN_VALUE pending // lo: multiplexing counter private final AtomicBiInteger state; // The pooled item. This is not volatile as it is set once and then never changed. // Other threads accessing must check the state field above first, so a good before/after // relationship exists to make a memory barrier. private T pooled; Entry() { this.state = new AtomicBiInteger(Integer.MIN_VALUE, 0); } /** Enable a reserved entry {@link Entry}. * An entry returned from the {@link #reserve(int)} method must be enabled with this method, * once and only once, before it is usable by the pool. * The entry may be enabled and not acquired, in which case it is immediately available to be * acquired, potentially by another thread; or it can be enabled and acquired atomically so that * no other thread can acquire it, although the acquire may still fail if the pool has been closed. * @param pooled The pooled item for the entry * @param acquire If true the entry is atomically enabled and acquired. * @return true If the entry was enabled. * @throws IllegalStateException if the entry was already enabled */ public boolean enable(T pooled, boolean acquire) { Objects.requireNonNull(pooled); if (state.getHi() != Integer.MIN_VALUE) { if (state.getHi() == -1) return false; // Pool has been closed throw new IllegalStateException("Entry already enabled: " + this); } this.pooled = pooled; int usage = acquire ? 1 : 0; if (!state.compareAndSet(Integer.MIN_VALUE, usage, 0, usage)) { this.pooled = null; if (state.getHi() == -1) return false; // Pool has been closed throw new IllegalStateException("Entry already enabled: " + this); } pending.decrementAndGet(); return true; } public T getPooled() { return pooled; } /** * Release the entry. * This is equivalent to calling {@link Pool#release(Pool.Entry)} passing this entry. * @return true if released. */ public boolean release() { return Pool.this.release(this); } /** * Remove the entry. * This is equivalent to calling {@link Pool#remove(Pool.Entry)} passing this entry. * @return true if remove. */ public boolean remove() { return Pool.this.remove(this); } /** * Try to acquire the entry if possible by incrementing both the usage * count and the multiplex count. * @return true if the usage count is <= maxUsageCount and * the multiplex count is maxMultiplex and the entry is not closed, * false otherwise. */ boolean tryAcquire() { while (true) { long encoded = state.get(); int usageCount = AtomicBiInteger.getHi(encoded); boolean closed = usageCount < 0; int multiplexingCount = AtomicBiInteger.getLo(encoded); int currentMaxUsageCount = maxUsageCount; if (closed || multiplexingCount >= maxMultiplex || (currentMaxUsageCount > 0 && usageCount >= currentMaxUsageCount)) return false; if (state.compareAndSet(encoded, usageCount + 1, multiplexingCount + 1)) return true; } } /** * Try to release the entry if possible by decrementing the multiplexing * count unless the entity is closed. * @return true if the entry was released, * false if {@link #tryRemove()} should be called. */ boolean tryRelease() { int newMultiplexingCount; int usageCount; while (true) { long encoded = state.get(); usageCount = AtomicBiInteger.getHi(encoded); boolean closed = usageCount < 0; if (closed) return false; newMultiplexingCount = AtomicBiInteger.getLo(encoded) - 1; if (newMultiplexingCount < 0) throw new IllegalStateException("Cannot release an already released entry"); if (state.compareAndSet(encoded, usageCount, newMultiplexingCount)) break; } int currentMaxUsageCount = maxUsageCount; boolean overUsed = currentMaxUsageCount > 0 && usageCount >= currentMaxUsageCount; return !(overUsed && newMultiplexingCount == 0); } public boolean isOverUsed() { int currentMaxUsageCount = maxUsageCount; int usageCount = state.getHi(); return currentMaxUsageCount > 0 && usageCount >= currentMaxUsageCount; } /** * Try to mark the entry as removed. * @return true if the entry has to be removed from the containing pool, false otherwise. */ boolean tryRemove() { while (true) { long encoded = state.get(); int usageCount = AtomicBiInteger.getHi(encoded); int multiplexCount = AtomicBiInteger.getLo(encoded); int newMultiplexCount = Math.max(multiplexCount - 1, 0); boolean removed = state.compareAndSet(usageCount, -1, multiplexCount, newMultiplexCount); if (removed) { if (usageCount == Integer.MIN_VALUE) pending.decrementAndGet(); return newMultiplexCount == 0; } } } public boolean isClosed() { return state.getHi() < 0; } public boolean isIdle() { long encoded = state.get(); return AtomicBiInteger.getHi(encoded) >= 0 && AtomicBiInteger.getLo(encoded) == 0; } public boolean isInUse() { long encoded = state.get(); return AtomicBiInteger.getHi(encoded) >= 0 && AtomicBiInteger.getLo(encoded) > 0; } public int getUsageCount() { return Math.max(state.getHi(), 0); } @Override public String toString() { long encoded = state.get(); int usageCount = AtomicBiInteger.getHi(encoded); int multiplexCount = AtomicBiInteger.getLo(encoded); String state = usageCount < 0 ? "CLOSED" : multiplexCount == 0 ? "IDLE" : "INUSE"; return String.format("%s@%x{%s, usage=%d, multiplex=%d/%d, pooled=%s}", getClass().getSimpleName(), hashCode(), state, Math.max(usageCount, 0), Math.max(multiplexCount, 0), getMaxMultiplex(), pooled); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy