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

org.xipki.pkcs11.wrapper.concurrent.ConcurrentBag Maven / Gradle / Ivy

There is a newer version: 1.0.9
Show newest version
/*
 * 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 org.xipki.pkcs11.wrapper.concurrent;

import org.xipki.pkcs11.wrapper.StaticLogger;

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 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 final CopyOnWriteArrayList sharedList;
  private final boolean weakThreadLocals;

  private final ThreadLocal> threadList;
  private final AtomicInteger waiters;
  private volatile boolean closed;

  private final SynchronousQueue handoffQueue;

  int STATE_NOT_IN_USE = 0;
  int STATE_IN_USE = 1;
  int STATE_REMOVED = -1;
  int STATE_RESERVED = -2;

  /**
   * Construct a ConcurrentBag with the specified listener.
   */
  public ConcurrentBag() {
    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<>(ConcurrentBagEntry.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 List list = threadList.get();
    for (int i = list.size() - 1; i >= 0; i--) {
      final Object 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;
      }
    }

    try {
      for (T bagEntry : sharedList) {
        if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
          return bagEntry;
        }
      }

      timeout = timeUnit.toNanos(timeout);
      do {
        final long start = ClockSource.currentTime();
        final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS);
        if (bagEntry == null || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) {
          return bagEntry;
        }

        timeout -= ClockSource.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 (int 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 List 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) {
      StaticLogger.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) {
      StaticLogger.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) {
      StaticLogger.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 ConcurrentBagEntry} states
   * @return a possibly empty list of objects having the state specified
   */
  public List values(final int state) {
    final List 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 {
      StaticLogger.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) {
    int count = 0;
    for (T e : sharedList) {
      if (e.getState() == state) {
        count++;
      }
    }
    return count;
  }

  public int[] getStateCounts() {
    final int[] states = new int[6];
    for (T 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 -> StaticLogger.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("org.xipki.useWeakReferences") != null) {
        // undocumented manual override of WeakReference behavior
        return Boolean.getBoolean("org.xipki.useWeakReferences");
      }

      return getClass().getClassLoader() != ClassLoader.getSystemClassLoader();
    } catch (SecurityException se) {
      return true;
    }
  }
}