com.zaxxer.hikari.util.ConcurrentBag Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of HikariCP Show documentation
Show all versions of HikariCP Show documentation
Ultimate JDBC Connection Pool
/*
* Copyright (C) 2013, 2014 Brett Wooldridge
*
* 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.zaxxer.hikari.util;
import com.zaxxer.hikari.util.ConcurrentBag.IConcurrentBagEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static com.zaxxer.hikari.util.ClockSource.currentTime;
import static com.zaxxer.hikari.util.ClockSource.elapsedNanos;
import static com.zaxxer.hikari.util.ConcurrentBag.IConcurrentBagEntry.*;
import static java.util.concurrent.TimeUnit.MICROSECONDS;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.locks.LockSupport.parkNanos;
/**
* This is a specialized concurrent bag that achieves superior performance
* to LinkedBlockingQueue and LinkedTransferQueue for the purposes of a
* connection pool. It uses ThreadLocal storage when possible to avoid
* locks, but resorts to scanning a common collection if there are no
* available items in the ThreadLocal list. Not-in-use items in the
* ThreadLocal lists can be "stolen" when the borrowing thread has none
* of its own. It is a "lock-less" implementation using a specialized
* AbstractQueuedLongSynchronizer to manage cross-thread signaling.
*
* Note that items that are "borrowed" from the bag are not actually
* removed from any collection, so garbage collection will not occur
* even if the reference is abandoned. Thus care must be taken to
* "requite" borrowed objects otherwise a memory leak will result. Only
* the "remove" method can completely remove an object from the bag.
*
* @author Brett Wooldridge
*
* @param the templated type to store in the bag
*/
public class ConcurrentBag implements AutoCloseable
{
private static final Logger LOGGER = LoggerFactory.getLogger(ConcurrentBag.class);
private final CopyOnWriteArrayList sharedList;
private final boolean weakThreadLocals;
private final ThreadLocal> threadList;
private final IBagStateListener listener;
private final AtomicInteger waiters;
private volatile boolean closed;
private final SynchronousQueue handoffQueue;
public interface IConcurrentBagEntry
{
int STATE_NOT_IN_USE = 0;
int STATE_IN_USE = 1;
int STATE_REMOVED = -1;
int STATE_RESERVED = -2;
boolean compareAndSet(int expectState, int newState);
void setState(int newState);
int getState();
}
public interface IBagStateListener
{
void addBagItem(int waiting);
}
/**
* Construct a ConcurrentBag with the specified listener.
*
* @param listener the IBagStateListener to attach to this bag
*/
public ConcurrentBag(final IBagStateListener listener)
{
this.listener = listener;
this.weakThreadLocals = useWeakThreadLocals();
this.handoffQueue = new SynchronousQueue<>(true);
this.waiters = new AtomicInteger();
this.sharedList = new CopyOnWriteArrayList<>();
if (weakThreadLocals) {
this.threadList = ThreadLocal.withInitial(() -> new ArrayList<>(16));
}
else {
this.threadList = ThreadLocal.withInitial(() -> new FastList<>(IConcurrentBagEntry.class, 16));
}
}
/**
* The method will borrow a BagEntry from the bag, blocking for the
* specified timeout if none are available.
*
* @param timeout how long to wait before giving up, in units of unit
* @param timeUnit a TimeUnit
determining how to interpret the timeout parameter
* @return a borrowed instance from the bag or null if a timeout occurs
* @throws InterruptedException if interrupted while waiting
*/
public T borrow(long timeout, final TimeUnit timeUnit) throws InterruptedException
{
// Try the thread-local list first
final var list = threadList.get();
for (int i = list.size() - 1; i >= 0; i--) {
final var entry = list.remove(i);
@SuppressWarnings("unchecked")
final T bagEntry = weakThreadLocals ? ((WeakReference) entry).get() : (T) entry;
if (bagEntry != null && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
}
}
// Otherwise, scan the shared list ... then poll the handoff queue
final int waiting = waiters.incrementAndGet();
try {
for (T bagEntry : sharedList) {
if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
// If we may have stolen another waiter's connection, request another bag add.
if (waiting > 1) {
listener.addBagItem(waiting - 1);
}
return bagEntry;
}
}
listener.addBagItem(waiting);
timeout = timeUnit.toNanos(timeout);
do {
final var start = currentTime();
final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
return bagEntry;
}
timeout -= elapsedNanos(start);
} while (timeout > 10_000);
return null;
}
finally {
waiters.decrementAndGet();
}
}
/**
* This method will return a borrowed object to the bag. Objects
* that are borrowed from the bag but never "requited" will result
* in a memory leak.
*
* @param bagEntry the value to return to the bag
* @throws NullPointerException if value is null
* @throws IllegalStateException if the bagEntry was not borrowed from the bag
*/
public void requite(final T bagEntry)
{
bagEntry.setState(STATE_NOT_IN_USE);
for (var i = 0; waiters.get() > 0; i++) {
if (bagEntry.getState() != STATE_NOT_IN_USE || handoffQueue.offer(bagEntry)) {
return;
}
else if ((i & 0xff) == 0xff) {
parkNanos(MICROSECONDS.toNanos(10));
}
else {
Thread.yield();
}
}
final var threadLocalList = threadList.get();
if (threadLocalList.size() < 50) {
threadLocalList.add(weakThreadLocals ? new WeakReference<>(bagEntry) : bagEntry);
}
}
/**
* Add a new object to the bag for others to borrow.
*
* @param bagEntry an object to add to the bag
*/
public void add(final T bagEntry)
{
if (closed) {
LOGGER.info("ConcurrentBag has been closed, ignoring add()");
throw new IllegalStateException("ConcurrentBag has been closed, ignoring add()");
}
sharedList.add(bagEntry);
// spin until a thread takes it or none are waiting
while (waiters.get() > 0 && bagEntry.getState() == STATE_NOT_IN_USE && !handoffQueue.offer(bagEntry)) {
Thread.yield();
}
}
/**
* Remove a value from the bag. This method should only be called
* with objects obtained by borrow(long, TimeUnit)
or reserve(T)
*
* @param bagEntry the value to remove
* @return true if the entry was removed, false otherwise
* @throws IllegalStateException if an attempt is made to remove an object
* from the bag that was not borrowed or reserved first
*/
public boolean remove(final T bagEntry)
{
if (!bagEntry.compareAndSet(STATE_IN_USE, STATE_REMOVED) && !bagEntry.compareAndSet(STATE_RESERVED, STATE_REMOVED) && !closed) {
LOGGER.warn("Attempt to remove an object from the bag that was not borrowed or reserved: {}", bagEntry);
return false;
}
final boolean removed = sharedList.remove(bagEntry);
if (!removed && !closed) {
LOGGER.warn("Attempt to remove an object from the bag that does not exist: {}", bagEntry);
}
threadList.get().remove(bagEntry);
return removed;
}
/**
* Close the bag to further adds.
*/
@Override
public void close()
{
closed = true;
}
/**
* This method provides a "snapshot" in time of the BagEntry
* items in the bag in the specified state. It does not "lock"
* or reserve items in any way. Call reserve(T)
* on items in list before performing any action on them.
*
* @param state one of the {@link IConcurrentBagEntry} states
* @return a possibly empty list of objects having the state specified
*/
public List values(final int state)
{
final var list = sharedList.stream().filter(e -> e.getState() == state).collect(Collectors.toList());
Collections.reverse(list);
return list;
}
/**
* This method provides a "snapshot" in time of the bag items. It
* does not "lock" or reserve items in any way. Call reserve(T)
* on items in the list, or understand the concurrency implications of
* modifying items, before performing any action on them.
*
* @return a possibly empty list of (all) bag items
*/
@SuppressWarnings("unchecked")
public List values()
{
return (List) sharedList.clone();
}
/**
* The method is used to make an item in the bag "unavailable" for
* borrowing. It is primarily used when wanting to operate on items
* returned by the values(int)
method. Items that are
* reserved can be removed from the bag via remove(T)
* without the need to unreserve them. Items that are not removed
* from the bag can be make available for borrowing again by calling
* the unreserve(T)
method.
*
* @param bagEntry the item to reserve
* @return true if the item was able to be reserved, false otherwise
*/
public boolean reserve(final T bagEntry)
{
return bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_RESERVED);
}
/**
* This method is used to make an item reserved via reserve(T)
* available again for borrowing.
*
* @param bagEntry the item to unreserve
*/
@SuppressWarnings("SpellCheckingInspection")
public void unreserve(final T bagEntry)
{
if (bagEntry.compareAndSet(STATE_RESERVED, STATE_NOT_IN_USE)) {
// spin until a thread takes it or none are waiting
while (waiters.get() > 0 && !handoffQueue.offer(bagEntry)) {
Thread.yield();
}
}
else {
LOGGER.warn("Attempt to relinquish an object to the bag that was not reserved: {}", bagEntry);
}
}
/**
* Get the number of threads pending (waiting) for an item from the
* bag to become available.
*
* @return the number of threads waiting for items from the bag
*/
public int getWaitingThreadCount()
{
return waiters.get();
}
/**
* Get a count of the number of items in the specified state at the time of this call.
*
* @param state the state of the items to count
* @return a count of how many items in the bag are in the specified state
*/
public int getCount(final int state)
{
var count = 0;
for (var e : sharedList) {
if (e.getState() == state) {
count++;
}
}
return count;
}
public int[] getStateCounts()
{
final var states = new int[6];
for (var e : sharedList) {
++states[e.getState()];
}
states[4] = sharedList.size();
states[5] = waiters.get();
return states;
}
/**
* Get the total number of items in the bag.
*
* @return the number of items in the bag
*/
public int size()
{
return sharedList.size();
}
public void dumpState()
{
sharedList.forEach(entry -> LOGGER.info(entry.toString()));
}
/**
* Determine whether to use WeakReferences based on whether there is a
* custom ClassLoader implementation sitting between this class and the
* System ClassLoader.
*
* @return true if we should use WeakReferences in our ThreadLocals, false otherwise
*/
private boolean useWeakThreadLocals()
{
try {
if (System.getProperty("com.zaxxer.hikari.useWeakReferences") != null) { // undocumented manual override of WeakReference behavior
return Boolean.getBoolean("com.zaxxer.hikari.useWeakReferences");
}
return getClass().getClassLoader() != ClassLoader.getSystemClassLoader();
}
catch (SecurityException se) {
return true;
}
}
}