From 77721457b86278f6851321c557030d76445f6ab2 Mon Sep 17 00:00:00 2001 From: Ben Manes Date: Sat, 18 Apr 2015 06:06:24 -0700 Subject: [PATCH] Rewrote the stack to support elimination & combining backoff strategies Eliminaton cancels out opposite operations (push and pop). Combining merges similar operations (pushes) into a batch operation. Both use an arena to transfer elements between threads as a backoff strategy when the stack's top reference is contended (CAS failed). This greatly improves scalability by reducing the number of threads contending on a shared reference. The approach taken is much simpler than the previous EliminationStack or the DECS paper's algorithm. Both of the alternatives rely on multiple arena states to model different scenarios. In this version, the arena slot is either empty or full. The consumer may receive multiple elements and it mimics a producer after taking one of the elements for itself. This greatly simplifies the DECS algorithm, resulting in higher performance despite a penalty incurred by the consumer when producing. --- ...va => ConcurrentLinkedStackBenchmark.java} | 22 +- .../github/benmanes/caffeine/QueueType.java | 3 +- .../SingleConsumerQueueBenchmark.java | 6 +- ...ava => ConcurrentLinkedStackProfiler.java} | 8 +- ...nStack.java => ConcurrentLinkedStack.java} | 580 ++++++++++-------- .../caffeine/SingleConsumerQueue.java | 30 +- .../caffeine/ConcurrentLinkedStackTest.java | 454 ++++++++++++++ .../caffeine/EliminationStackTest.java | 370 ----------- .../IsValidConcurrentLinkedStack.java | 88 +++ .../caffeine/SingleConsumerQueueTest.java | 6 +- .../caffeine/ConcurrentLinkedStackTests.java | 87 +++ .../caffeine/EliminationStackTests.java | 49 -- .../benmanes/caffeine/PackageSanityTests.java | 2 + .../caffeine/SingleConsumerQueueTests.java | 2 + .../caffeine/cache/MapTestFactory.java | 4 +- wiki/concurrent_linked_stack.png | Bin 0 -> 64680 bytes 16 files changed, 989 insertions(+), 722 deletions(-) rename caffeine/src/jmh/java/com/github/benmanes/caffeine/{EliminationStackBenchmark.java => ConcurrentLinkedStackBenchmark.java} (73%) rename caffeine/src/jmh/java/com/github/benmanes/caffeine/profiler/{EliminationStackProfiler.java => ConcurrentLinkedStackProfiler.java} (80%) rename caffeine/src/main/java/com/github/benmanes/caffeine/{EliminationStack.java => ConcurrentLinkedStack.java} (50%) create mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/ConcurrentLinkedStackTest.java delete mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/EliminationStackTest.java create mode 100644 caffeine/src/test/java/com/github/benmanes/caffeine/IsValidConcurrentLinkedStack.java create mode 100644 guava/src/test/java/com/github/benmanes/caffeine/ConcurrentLinkedStackTests.java delete mode 100644 guava/src/test/java/com/github/benmanes/caffeine/EliminationStackTests.java create mode 100644 wiki/concurrent_linked_stack.png diff --git a/caffeine/src/jmh/java/com/github/benmanes/caffeine/EliminationStackBenchmark.java b/caffeine/src/jmh/java/com/github/benmanes/caffeine/ConcurrentLinkedStackBenchmark.java similarity index 73% rename from caffeine/src/jmh/java/com/github/benmanes/caffeine/EliminationStackBenchmark.java rename to caffeine/src/jmh/java/com/github/benmanes/caffeine/ConcurrentLinkedStackBenchmark.java index 568e297431..789b6dedb5 100644 --- a/caffeine/src/jmh/java/com/github/benmanes/caffeine/EliminationStackBenchmark.java +++ b/caffeine/src/jmh/java/com/github/benmanes/caffeine/ConcurrentLinkedStackBenchmark.java @@ -31,10 +31,8 @@ * @author ben.manes@gmail.com (Ben Manes) */ @State(Scope.Group) -public class EliminationStackBenchmark { - @Param({"EliminationStack", "ConcurrentLinkedQueue", "ArrayBlockingQueue", - "LinkedBlockingQueueBenchmark", "LinkedTransferQueue", "SynchronousQueue", - "SynchronizedArrayDeque"}) +public class ConcurrentLinkedStackBenchmark { + @Param({"ConcurrentLinkedStack_linearizable", "ConcurrentLinkedQueue"}) QueueType queueType; Queue queue; @@ -44,23 +42,23 @@ public void setup() { queue = queueType.create(); } - @Benchmark @Group("no_contention") @GroupThreads(1) - public void no_contention_offer() { + @Benchmark @Group("low_contention") @GroupThreads(1) + public void low_contention_offer() { queue.offer(Boolean.TRUE); } - @Benchmark @Group("no_contention") @GroupThreads(1) - public void no_contention_poll() { + @Benchmark @Group("low_contention") @GroupThreads(1) + public void low_contention_poll() { queue.poll(); } - @Benchmark @Group("mild_contention") @GroupThreads(4) - public void mild_contention_offer() { + @Benchmark @Group("medium_contention") @GroupThreads(4) + public void medium_contention_offer() { queue.offer(Boolean.TRUE); } - @Benchmark @Group("mild_contention") @GroupThreads(4) - public void mild_contention_poll() { + @Benchmark @Group("medium_contention") @GroupThreads(4) + public void medium_contention_poll() { queue.poll(); } diff --git a/caffeine/src/jmh/java/com/github/benmanes/caffeine/QueueType.java b/caffeine/src/jmh/java/com/github/benmanes/caffeine/QueueType.java index 87867962b9..5926d60ecc 100644 --- a/caffeine/src/jmh/java/com/github/benmanes/caffeine/QueueType.java +++ b/caffeine/src/jmh/java/com/github/benmanes/caffeine/QueueType.java @@ -32,7 +32,8 @@ public enum QueueType { SingleConsumerQueue_optimistic(SingleConsumerQueue::optimistic), SingleConsumerQueue_linearizable(SingleConsumerQueue::linearizable), - EliminationStack(() -> new EliminationStack<>().asLifoQueue()), + ConcurrentLinkedStack_optimistic(() -> ConcurrentLinkedStack.optimistic().asLifoQueue()), + ConcurrentLinkedStack_linearizable(() -> ConcurrentLinkedStack.linearizable().asLifoQueue()), ConcurrentLinkedQueue(ConcurrentLinkedQueue::new), ArrayBlockingQueue(() -> new ArrayBlockingQueue<>(10000)), LinkedBlockingQueueBenchmark(LinkedBlockingQueue::new), diff --git a/caffeine/src/jmh/java/com/github/benmanes/caffeine/SingleConsumerQueueBenchmark.java b/caffeine/src/jmh/java/com/github/benmanes/caffeine/SingleConsumerQueueBenchmark.java index 08df1f5294..3dae524edb 100644 --- a/caffeine/src/jmh/java/com/github/benmanes/caffeine/SingleConsumerQueueBenchmark.java +++ b/caffeine/src/jmh/java/com/github/benmanes/caffeine/SingleConsumerQueueBenchmark.java @@ -33,9 +33,7 @@ */ @State(Scope.Group) public class SingleConsumerQueueBenchmark { - @Param({"SingleConsumerQueue_optimistic", - "SingleConsumerQueue_linearizable", - "ConcurrentLinkedQueue"}) + @Param({"SingleConsumerQueue_linearizable", "ConcurrentLinkedQueue"}) QueueType queueType; Queue queue; @@ -71,7 +69,7 @@ public void high_contention_offer() { } @Benchmark @Group("high_contention") @GroupThreads(1) - public void high_contention_poll() { + public void high_contention_clear() { queue.clear(); } } diff --git a/caffeine/src/jmh/java/com/github/benmanes/caffeine/profiler/EliminationStackProfiler.java b/caffeine/src/jmh/java/com/github/benmanes/caffeine/profiler/ConcurrentLinkedStackProfiler.java similarity index 80% rename from caffeine/src/jmh/java/com/github/benmanes/caffeine/profiler/EliminationStackProfiler.java rename to caffeine/src/jmh/java/com/github/benmanes/caffeine/profiler/ConcurrentLinkedStackProfiler.java index 1fe9cee391..eeddc83635 100644 --- a/caffeine/src/jmh/java/com/github/benmanes/caffeine/profiler/EliminationStackProfiler.java +++ b/caffeine/src/jmh/java/com/github/benmanes/caffeine/profiler/ConcurrentLinkedStackProfiler.java @@ -17,15 +17,15 @@ import java.util.concurrent.ThreadLocalRandom; -import com.github.benmanes.caffeine.EliminationStack; +import com.github.benmanes.caffeine.ConcurrentLinkedStack; /** * @author Ben Manes (ben.manes@gmail.com) */ -public final class EliminationStackProfiler extends ProfilerHook { +public final class ConcurrentLinkedStackProfiler extends ProfilerHook { static final Integer ELEMENT = 1; - final EliminationStack stack = new EliminationStack<>(); + final ConcurrentLinkedStack stack = ConcurrentLinkedStack.linearizable(); @Override protected void profile() { @@ -41,7 +41,7 @@ protected void profile() { } public static void main(String[] args) { - ProfilerHook profile = new EliminationStackProfiler(); + ProfilerHook profile = new ConcurrentLinkedStackProfiler(); profile.run(); } } diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/EliminationStack.java b/caffeine/src/main/java/com/github/benmanes/caffeine/ConcurrentLinkedStack.java similarity index 50% rename from caffeine/src/main/java/com/github/benmanes/caffeine/EliminationStack.java rename to caffeine/src/main/java/com/github/benmanes/caffeine/ConcurrentLinkedStack.java index 48f66709d7..d096140303 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/EliminationStack.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/ConcurrentLinkedStack.java @@ -29,58 +29,65 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.Queue; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; import javax.annotation.Nonnull; import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; +import com.github.benmanes.caffeine.ConcurrentLinkedStack.Node; +import com.github.benmanes.caffeine.base.UnsafeAccess; + /** * An unbounded thread-safe stack based on linked nodes. This stack orders elements LIFO - * (last-in-first-out). The top of the stack is that element that has been on the stack - * the shortest time. New elements are inserted at and retrieved from the top of the stack. A - * {@code EliminationStack} is an appropriate choice when many threads will exchange elements + * (last-in-first-out). The top of the stack is that element that has been on the stack the + * shortest time. New elements are inserted at and retrieved from the top of the stack. A + * {@code ConcurrentLinkedStack} is an appropriate choice when many threads will exchange elements * through shared access to a common collection. Like most other concurrent collection * implementations, this class does not permit the use of {@code null} elements. *

- * This implementation employs elimination to transfer elements between threads that are pushing - * and popping concurrently. This technique avoids contention on the stack by attempting to cancel - * operations if an immediate update to the stack is not successful. This approach is described in - * A Scalable Lock-free - * Stack Algorithm. + * This implementation employs elimination and combination to transfer elements between threads that + * are pushing and popping concurrently. This technique avoids contention on the stack by attempting + * to cancel opposing operations and merge additive operations if an immediate update to the stack + * is not successful. When a pair of operations collide, the task of performing the combined set of + * operations is delegated to one of the threads and the other thread optionally waits for its + * operation to be completed. This decision of whether to wait for completion is determined by + * constructing either a linearizable or optimistic stack. *

* Iterators are weakly consistent, returning elements reflecting the state of the stack at - * some point at or since the creation of the iterator. They do not throw {@link - * java.util.ConcurrentModificationException}, and may proceed concurrently with other operations. - * Elements contained in the stack since the creation of the iterator will be returned exactly once. + * some point at or since the creation of the iterator. They do not throw + * {@link java.util.ConcurrentModificationException}, and may proceed concurrently with other + * operations. Elements contained in the stack since the creation of the iterator will be returned + * exactly once. *

- * Beware that, unlike in most collections, the {@code size} method is NOT a - * constant-time operation. Because of the asynchronous nature of these stacks, determining the - * current number of elements requires a traversal of the elements, and so may report inaccurate - * results if this collection is modified during traversal. + * Beware that, unlike in most collections, the {@code size} method is NOT a constant-time + * operation. Because of the asynchronous nature of these stacks, determining the current number of + * elements requires a traversal of the elements, and so may report inaccurate results if this + * collection is modified during traversal. * * @author ben.manes@gmail.com (Ben Manes) * @param the type of elements held in this collection */ @Beta @ThreadSafe -public final class EliminationStack extends AbstractCollection implements Serializable { +public final class ConcurrentLinkedStack extends TopRef implements Serializable { /* * A Treiber's stack is represented as a singly-linked list with an atomic top reference and uses * compare-and-swap to modify the value atomically. * - * The stack is augmented with an elimination array to minimize the top reference becoming a - * sequential bottleneck. Elimination allows pairs of operations with reverse semantics, like - * pushes and pops on a stack, to complete without any central coordination, and therefore - * substantially aids scalability [1, 2, 3]. If a thread fails to update the stack's top reference - * then it backs off to a collision arena where a location is chosen at random and it attempts to - * coordinate with another operation that concurrently chose the same location. If a transfer is - * not successful then the thread repeats the process until the element is added to the stack or - * a cancellation occurs. + * The stack is augmented with an elimination-combining array to minimize the top reference + * becoming a sequential bottleneck. Elimination allows pairs of operations with reverse + * semantics, like pushes and pops on a stack, to complete without any central coordination, and + * therefore substantially aids scalability [1, 2, 3]. Combining allows pairs of operations with + * identical semantics, specifically pushes on a stack, to batch the work and therefore reduces + * the number of threads updating the top reference. The aproach to dynamically eliminate and + * combine operations is explored in [4]. * * This implementation borrows optimizations from {@link java.util.concurrent.Exchanger} for - * choosing an arena location and awaiting a match [4]. + * choosing an arena location and awaiting a match [5]. * * [1] A Scalable Lock-free Stack Algorithm * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.156.8728 @@ -88,7 +95,9 @@ public final class EliminationStack extends AbstractCollection implements * http://www.cs.tau.ac.il/~shanir/concurrent-data-structures.pdf * [3] Using elimination to implement scalable and lock-free fifo queues * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.108.6422 - * [4] A Scalable Elimination-based Exchange Channel + * [4] A Dynamic Elimination-Combining Stack Algorithm + * http://www.cs.bgu.ac.il/~hendlerd/papers/DECS.pdf + * [5] A Scalable Elimination-based Exchange Channel * http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.59.7396 */ @@ -101,9 +110,6 @@ public final class EliminationStack extends AbstractCollection implements /** The mask value for indexing into the arena. */ static int ARENA_MASK = ARENA_LENGTH - 1; - /** The number of times to step ahead, probe, and try to match. */ - static final int LOOKAHEAD = Math.min(4, NCPU); - /** * The number of times to spin (doing nothing except polling a memory location) before giving up * while waiting to eliminate an operation. Should be zero on uniprocessors. On multiprocessors, @@ -115,46 +121,52 @@ public final class EliminationStack extends AbstractCollection implements */ static final int SPINS = (NCPU == 1) ? 0 : 2000; - /** The number of times to spin per lookahead step */ - static final int SPINS_PER_STEP = (SPINS / LOOKAHEAD); - - /** A marker indicating that the arena slot is free. */ - static final Object FREE = null; - - /** A marker indicating that a thread is waiting in that slot to be transfered an element. */ - static final Object WAITER = new Object(); + /** The offset to the thread-specific probe field. */ + static final long PROBE = UnsafeAccess.objectFieldOffset(Thread.class, "threadLocalRandomProbe"); static int ceilingNextPowerOfTwo(int x) { // From Hacker's Delight, Chapter 3, Harry S. Warren Jr. return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(x - 1)); } - /** The top of the stack. */ - final AtomicReference> top; - - /** The arena where slots can be used to perform an exchange. */ - final AtomicReference[] arena; + final AtomicReference>[] arena; + final Function> factory; /** Creates a {@code EliminationStack} that is initially empty. */ @SuppressWarnings({"unchecked", "rawtypes"}) - public EliminationStack() { - top = new AtomicReference<>(); + private ConcurrentLinkedStack(Function> factory) { arena = new AtomicReference[ARENA_LENGTH]; for (int i = 0; i < ARENA_LENGTH; i++) { - arena[i] = new AtomicReference(); + arena[i] = new AtomicReference<>(); } + this.factory = factory; } /** - * Creates a {@code EliminationStack} initially containing the elements of the given collection, - * added in traversal order of the collection's iterator. + * Creates a stack with an optimistic backoff strategy. A thread completes its operation without + * waiting after it successfully hands off the additional element(s) to another producing thread + * for batch insertion. This optimistic behavior may result in additions not appearing in LIFO + * order due to the backoff strategy trying to compensate for stack contention. * - * @param c the collection of elements to initially contain - * @throws NullPointerException if the specified collection or any of its elements are null + * @param the type of elements held in this collection + * @return a new stack where producers complete their operation immediately if combined with + * another thread's */ - public EliminationStack(Collection c) { - this(); - addAll(c); + public static ConcurrentLinkedStack optimistic() { + return new ConcurrentLinkedStack<>(Node::new); + } + + /** + * Creates a stack with a linearizable backoff strategy. A thread waits for a completion signal if + * it successfully hands off the additional element(s) to another producing thread for batch + * insertion. + * + * @param the type of elements held in this collection + * @return a new stack where producers wait for a completion signal after combining its addition + * with another thread's + */ + public static ConcurrentLinkedStack linearizable() { + return new ConcurrentLinkedStack<>(LinearizableNode::new); } /** @@ -165,13 +177,13 @@ public EliminationStack(Collection c) { @Override public boolean isEmpty() { for (;;) { - Node node = top.get(); + Node node = top; if (node == null) { return true; } E e = node.get(); if (e == null) { - top.compareAndSet(node, node.next); + casTop(node, node.next); } else { return false; } @@ -192,7 +204,7 @@ public boolean isEmpty() { @Override public int size() { int size = 0; - for (Node node = top.get(); node != null; node = node.next) { + for (Node node = top; node != null; node = node.next) { if (node.get() != null) { size++; } @@ -203,7 +215,7 @@ public int size() { /** Removes all of the elements from this stack. */ @Override public void clear() { - top.set(null); + top = null; } /** @@ -216,9 +228,10 @@ public void clear() { */ @Override public boolean contains(@Nullable Object o) { - requireNonNull(o); - - for (Node node = top.get(); node != null; node = node.next) { + if (o == null) { + return false; + } + for (Node node = top; node != null; node = node.next) { E value = node.get(); if (o.equals(value)) { return true; @@ -236,13 +249,13 @@ public boolean contains(@Nullable Object o) { @Nullable public E peek() { for (;;) { - Node node = top.get(); + Node node = top; if (node == null) { return null; } E e = node.get(); if (e == null) { - top.compareAndSet(node, node.next); + casTop(node, node.next); } else { return e; } @@ -254,22 +267,40 @@ public E peek() { * * @return the top of this stack, or null if this stack is empty */ - public @Nullable E pop() { + @Nullable + public E pop() { for (;;) { - Node current = top.get(); + Node current = top; if (current == null) { return null; } - // Attempt to pop from the stack, backing off to the elimination array if contended - if ((top.get() == current) && top.compareAndSet(current, current.next)) { + if (casTop(current, current.next)) { return current.get(); } - E e = tryReceive(); - if (e != null) { - return e; + Node node = tryReceive(); + if (node != null) { + return node.get(); + } + } + } + + Node tryReceive() { + int index = index(); + AtomicReference> slot = arena[index]; + + for (int spin = 0; spin < SPINS; spin++) { + Node found = slot.get(); + if ((found != null) && slot.compareAndSet(found, null)) { + found.complete(); + Node next = found.next; + if (next != null) { + append(next, findLast(next)); + } + return found; } } + return null; } /** @@ -280,31 +311,130 @@ public E peek() { public void push(@Nonnull E e) { requireNonNull(e); - Node node = new Node(e); + Node node = factory.apply(e); + append(node, node); + } + + void append(Node first, Node last) { for (;;) { - node.next = top.get(); + last.next = top; - // Attempt to push to the stack, backing off to the elimination array if contended - if ((top.get() == node.next) && top.compareAndSet(node.next, node)) { - return; + if (casTop(last.next, first)) { + for (;;) { + first.complete(); + if (first == last) { + return; + } + first = first.next; + } } - if (tryTransfer(e)) { + last.next = null; + Node node = transferOrCombine(first, last); + if (node == null) { + last.await(); return; + } else if (node != first) { + last = node; } } } + /** + * Attempts to receive a linked list from a waiting producer or transfer the specified linked list + * to an arriving producer. + * + * @param first the first node in the linked list to try to transfer + * @param last the last node in the linked list to try to transfer + * @return either {@code null} if the element was transferred, the first node if neither a + * transfer nor receive were successful, or the received last element from a producer + */ + Node transferOrCombine(Node first, Node last) { + int index = index(); + AtomicReference> slot = arena[index]; + + for (;;) { + Node found = slot.get(); + if (found == null) { + if (slot.compareAndSet(null, first)) { + for (int spin = 0; spin < SPINS; spin++) { + if (slot.get() != first) { + return null; + } + } + return slot.compareAndSet(first, null) ? first : null; + } + } else if (slot.compareAndSet(found, null)) { + last.next = found; + last = findLast(found); + for (int i = 1; i < ARENA_LENGTH; i++) { + slot = arena[(i + index) & ARENA_MASK]; + found = slot.get(); + if ((found != null) && slot.compareAndSet(found, null)) { + last.next = found; + last = findLast(found); + } + } + return last; + } + } + } + + /** Returns the arena index for the current thread. */ + static final int index() { + int probe = UnsafeAccess.UNSAFE.getInt(Thread.currentThread(), PROBE); + if (probe == 0) { + ThreadLocalRandom.current(); // force initialization + probe = UnsafeAccess.UNSAFE.getInt(Thread.currentThread(), PROBE); + } + return (probe & ARENA_MASK); + } + + /** Returns the last node in the linked list. */ + @Nonnull static Node findLast(@Nonnull Node node) { + Node next; + while ((next = node.next) != null) { + node = next; + } + return node; + } + @Override public boolean add(E e) { push(e); return true; } + @Override + public boolean addAll(Collection c) { + requireNonNull(c); + + Node first = null; + Node last = null; + for (E e : c) { + requireNonNull(e); + if (last == null) { + last = factory.apply(e); + first = last; + } else { + Node newFirst = new Node<>(e); + newFirst.next = first; + first = newFirst; + } + } + if (first == null) { + return false; + } + append(first, last); + return true; + } + @Override public boolean remove(Object o) { - requireNonNull(o); + if (o == null) { + return false; + } - for (Node node = top.get(); node != null; node = node.next) { + for (Node node = top; node != null; node = node.next) { E value = node.get(); if (o.equals(value) && node.compareAndSet(value, null)) { return true; @@ -318,6 +448,17 @@ public Iterator iterator() { return new StackIterator(); } + /** + * Returns a view as a last-in-first-out (LIFO) {@link Queue}. Method add is mapped to + * push, remove is mapped to pop and so on. This view can be useful + * when you would like to use a method requiring a Queue but you need LIFO ordering. + * + * @return the queue + */ + public Queue asLifoQueue() { + return new AsLifoQueue(this); + } + /** An iterator that traverses the stack, skipping elements that have been removed. */ final class StackIterator implements Iterator { Node cursor; @@ -325,7 +466,7 @@ final class StackIterator implements Iterator { E nextValue; StackIterator() { - next = top.get(); + next = top; if (next != null) { nextValue = next.get(); } @@ -376,171 +517,64 @@ public void remove() { } } - /** - * Returns a view as a last-in-first-out (Lifo) {@link Queue}. Method add is mapped to - * push, remove is mapped to pop and so on. This view can be useful - * when you would like to use a method requiring a Queue but you need Lifo ordering. - * - * @return the queue - */ - public Queue asLifoQueue() { - return new AsLifoQueue<>(this); - } + /** A view as a last-in-first-out (LIFO) {@link Queue}. */ + static final class AsLifoQueue extends AbstractQueue implements Queue, Serializable { + private static final long serialVersionUID = 1L; + private final ConcurrentLinkedStack stack; - /** - * Attempts to transfer the element to a waiting consumer. - * - * @param e the element to try to exchange - * @return if the element was successfully transfered - */ - boolean tryTransfer(E e) { - int start = startIndex(); - return scanAndTransferToWaiter(e, start) || awaitExchange(e, start); - } + AsLifoQueue(ConcurrentLinkedStack stack) { + this.stack = stack; + } - /** - * Scans the arena searching for a waiting consumer to exchange with. - * - * @param e the element to try to exchange - * @return if the element was successfully transfered - */ - boolean scanAndTransferToWaiter(E e, int start) { - for (int i = 0; i < ARENA_LENGTH; i++) { - int index = (start + i) & ARENA_MASK; - AtomicReference slot = arena[index]; - // if some thread is waiting to receive an element then attempt to provide it - if ((slot.get() == WAITER) && slot.compareAndSet(WAITER, e)) { - return true; - } + @Override + public boolean isEmpty() { + return stack.isEmpty(); } - return false; - } - /** - * Waits for (by spinning) to have the element transfered to another thread. The element is - * filled into an empty slot in the arena and spun on until it is transfered or a per-slot spin - * limit is reached. This search and wait strategy is repeated by selecting another slot until a - * total spin limit is reached. - * - * @param e the element to transfer - * @param start the arena location to start at - * @return if an exchange was completed successfully - */ - boolean awaitExchange(E e, int start) { - for (int step = 0, totalSpins = 0; (step < ARENA_LENGTH) && (totalSpins < SPINS); step++) { - int index = (start + step) & ARENA_MASK; - AtomicReference slot = arena[index]; + @Override + public int size() { + return stack.size(); + } - Object found = slot.get(); - if ((found == WAITER) && slot.compareAndSet(WAITER, e)) { - return true; - } else if ((found == FREE) && slot.compareAndSet(FREE, e)) { - int slotSpins = 0; - for (;;) { - found = slot.get(); - if (found != e) { - return true; - } else if ((slotSpins >= SPINS_PER_STEP) && (slot.compareAndSet(e, FREE))) { - // failed to transfer the element; try a new slot - totalSpins += slotSpins; - break; - } - slotSpins++; - } - } + @Override + public void clear() { + stack.clear(); } - // failed to transfer the element; give up - return false; - } - /** - * Attempts to receive an element from a waiting provider. - * - * @return an element if successfully transfered or null if unsuccessful - */ - @Nullable E tryReceive() { - int start = startIndex(); - E e = scanAndMatch(start); - return (e == null) - ? awaitMatch(start) - : e; - } + @Override + public boolean contains(@Nullable Object o) { + return stack.contains(o); + } - /** - * Scans the arena searching for a waiting producer to transfer from. - * - * @param start the arena location to start at - * @return an element if successfully transfered or null if unsuccessful - */ - @Nullable E scanAndMatch(int start) { - for (int i = 0; i < ARENA_LENGTH; i++) { - int index = (start + i) & ARENA_MASK; - AtomicReference slot = arena[index]; - - // accept a transfer if an element is available - Object found = slot.get(); - if ((found != FREE) && (found != WAITER) && slot.compareAndSet(found, FREE)) { - @SuppressWarnings("unchecked") - E e = (E) found; - return e; - } + @Override + public E peek() { + return stack.peek(); } - return null; - } - /** - * Waits for (by spinning) to have an element transfered from another thread. A marker is filled - * into an empty slot in the arena and spun on until it is replaced with an element or a per-slot - * spin limit is reached. This search and wait strategy is repeated by selecting another slot - * until a total spin limit is reached. - * - * @param start the arena location to start at - * @return an element if successfully transfered or null if unsuccessful - */ - @Nullable E awaitMatch(int start) { - for (int step = 0, totalSpins = 0; (step < ARENA_LENGTH) && (totalSpins < SPINS); step++) { - int index = (start + step) & ARENA_MASK; - AtomicReference slot = arena[index]; - Object found = slot.get(); - - if (found == FREE) { - if (slot.compareAndSet(FREE, WAITER)) { - int slotSpins = 0; - for (;;) { - found = slot.get(); - if ((found != WAITER) && slot.compareAndSet(found, FREE)) { - @SuppressWarnings("unchecked") - E e = (E) found; - return e; - } else if ((slotSpins >= SPINS_PER_STEP) && (found == WAITER) - && (slot.compareAndSet(WAITER, FREE))) { - // failed to receive an element; try a new slot - totalSpins += slotSpins; - break; - } - slotSpins++; - } - } - } else if ((found != WAITER) && slot.compareAndSet(found, FREE)) { - @SuppressWarnings("unchecked") - E e = (E) found; - return e; - } + @Override + public boolean offer(E e) { + return stack.add(e); } - // failed to receive an element; give up - return null; - } + @Override + public E poll() { + return stack.pop(); + } - /** - * Returns the start index to begin searching the arena with. Uses a one-step FNV-1a hash code - * (http://www.isthe.com/chongo/tech/comp/fnv) based on the current thread's id. These hash codes - * have more uniform distribution properties with respect to small moduli (here 1-31) than do - * other simple hashing functions. - */ - static int startIndex() { - int id = (int) Thread.currentThread().getId(); - return (id ^ 0x811c9dc5) * 0x01000193; + @Override + public boolean addAll(Collection c) { + return stack.addAll(c); + } + + @Override + public boolean remove(Object o) { + return stack.remove(o); + } + + @Override + public Iterator iterator() { + return stack.iterator(); + } } /* ---------------- Serialization Support -------------- */ @@ -555,17 +589,21 @@ private void readObject(ObjectInputStream stream) throws InvalidObjectException throw new InvalidObjectException("Proxy required"); } - /** A proxy that is serialized instead of the stack, containing only the elements. */ + /** A proxy that is serialized instead of the stack. */ static final class SerializationProxy implements Serializable { + final boolean linearizable; final List elements; - SerializationProxy(EliminationStack stack) { - this.elements = new ArrayList<>(stack); + SerializationProxy(ConcurrentLinkedStack stack) { + linearizable = (stack.factory.apply(null) instanceof LinearizableNode); + elements = new ArrayList<>(stack); } Object readResolve() { Collections.reverse(elements); - return new EliminationStack<>(elements); + ConcurrentLinkedStack stack = linearizable ? linearizable() : optimistic(); + stack.addAll(elements); + return stack; } static final long serialVersionUID = 1; @@ -575,68 +613,74 @@ Object readResolve() { * An item on the stack. The node is mutable prior to being inserted to avoid object churn and * is immutable by the time it has been published to other threads. */ - static final class Node extends AtomicReference { + static class Node extends AtomicReference { private static final long serialVersionUID = 1L; Node next; + volatile boolean done; Node(E value) { super(value); } - } - /** A view as a last-in-first-out (Lifo) {@link Queue}. */ - static class AsLifoQueue extends AbstractQueue implements Queue, Serializable { - private static final long serialVersionUID = 1L; - private final EliminationStack stack; + /** A no-op notification that the element was added to the queue. */ + void complete() {} - AsLifoQueue(EliminationStack stack) { - this.stack = stack; - } + /** A no-op wait until the operation has completed. */ + void await() {} - @Override - public boolean offer(E e) { - return stack.add(e); + /** Always returns that the operation completed. */ + boolean isDone() { + return true; } @Override - public E poll() { - return stack.pop(); + public String toString() { + return getClass().getSimpleName() + "[" + get() + "]"; } + } - @Override - public E peek() { - return stack.peek(); - } + static final class LinearizableNode extends Node { + private static final long serialVersionUID = 1L; - @Override - public void clear() { - stack.clear(); - } + volatile boolean done; - @Override - public int size() { - return stack.size(); + LinearizableNode(E value) { + super(value); } + /** A notification that the element was added to the queue. */ @Override - public boolean isEmpty() { - return stack.isEmpty(); + void complete() { + done = true; } + /** A busy wait until the operation has completed. */ @Override - public boolean contains(@Nullable Object o) { - return stack.contains(o); + void await() { + while (!done) {}; } + /** Returns whether the operation completed. */ @Override - public boolean remove(Object o) { - return stack.remove(o); + boolean isDone() { + return done; } + } +} - @Override - public Iterator iterator() { - return stack.iterator(); - } +abstract class PadTop extends AbstractCollection { + long p00, p01, p02, p03, p04, p05, p06, p07; + long p30, p31, p32, p33, p34, p35, p36, p37; +} + +/** Enforces a memory layout to avoid false sharing by padding the top of the stack. */ +abstract class TopRef extends PadTop { + static final long TOP_OFFSET = UnsafeAccess.objectFieldOffset(TopRef.class, "top"); + + volatile Node top; + + boolean casTop(Node expect, Node update) { + return UnsafeAccess.UNSAFE.compareAndSwapObject(this, TOP_OFFSET, expect, update); } } diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/SingleConsumerQueue.java b/caffeine/src/main/java/com/github/benmanes/caffeine/SingleConsumerQueue.java index a4050eb803..4f95755a18 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/SingleConsumerQueue.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/SingleConsumerQueue.java @@ -148,10 +148,10 @@ private SingleConsumerQueue(Function> factory) { } /** - * Creates a queue that with an optimistic backoff strategy. A thread completes its operation + * Creates a queue with an optimistic backoff strategy. A thread completes its operation * without waiting after it successfully hands off the additional element(s) to another producing * thread for batch insertion. This optimistic behavior may result in additions not appearing in - * FIFO order due to the backoff strategy trying to compensate for stack contention. + * FIFO order due to the backoff strategy trying to compensate for queue contention. * * @param the type of elements held in this collection * @return a new queue where producers complete their operation immediately if combined with @@ -162,7 +162,7 @@ public static SingleConsumerQueue optimistic() { } /** - * Creates a queue that with a linearizable backoff strategy. A thread waits for a completion + * Creates a queue with a linearizable backoff strategy. A thread waits for a completion * signal if it successfully hands off the additional element(s) to another producing * thread for batch insertion. * @@ -189,8 +189,8 @@ public int size() { if (next == null) { while ((next = cursor.next) == null) {} } - size++; cursor = next; + size++; } return size; } @@ -200,6 +200,19 @@ public void clear() { lazySetHead(tail); } + @Override + public boolean contains(Object o) { + if (o == null) { + return false; + } + for (Iterator it = iterator(); it.hasNext();) { + if (o.equals(it.next())) { + return true; + } + } + return false; + } + @Override public E peek() { Node h = head; @@ -228,8 +241,7 @@ public E poll() { Node h = head; Node next = h.getNextRelaxed(); if (next == null) { - Node t = tail; - if (h == t) { + if (h == tail) { return null; } else { while ((next = h.next) == null) {} @@ -417,16 +429,16 @@ private void readObject(ObjectInputStream stream) throws InvalidObjectException /** A proxy that is serialized instead of the queue. */ static final class SerializationProxy implements Serializable { final boolean linearizable; - final List list; + final List elements; SerializationProxy(SingleConsumerQueue queue) { linearizable = (queue.factory.apply(null) instanceof LinearizableNode); - list = new ArrayList<>(queue); + elements = new ArrayList<>(queue); } Object readResolve() { SingleConsumerQueue queue = linearizable ? linearizable() : optimistic(); - queue.addAll(list); + queue.addAll(elements); return queue; } diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/ConcurrentLinkedStackTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/ConcurrentLinkedStackTest.java new file mode 100644 index 0000000000..75a0d161b9 --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/ConcurrentLinkedStackTest.java @@ -0,0 +1,454 @@ +/* + * Copyright 2013 Ben Manes. All Rights Reserved. + * + * 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.github.benmanes.caffeine; + +import static com.github.benmanes.caffeine.IsValidConcurrentLinkedStack.validate; +import static com.google.common.collect.Iterators.elementsEqual; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.LongAdder; +import java.util.stream.IntStream; + +import org.testng.IInvokedMethod; +import org.testng.IInvokedMethodListener; +import org.testng.ITestResult; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import com.github.benmanes.caffeine.ConcurrentLinkedStack.Node; +import com.github.benmanes.caffeine.ConcurrentLinkedStackTest.ValidatingStackListener; +import com.github.benmanes.caffeine.base.UnsafeAccess; +import com.google.common.collect.Iterables; +import com.google.common.testing.SerializableTester; + +/** + * @author ben.manes@gmail.com (Ben Manes) + */ +@Listeners(ValidatingStackListener.class) +public final class ConcurrentLinkedStackTest { + private static final int POPULATED_SIZE = 5; + private static final int NUM_THREADS = 10; + private static final int COUNT = 10_000; + + @Test(dataProvider = "empty") + public void clear_whenEmpty(ConcurrentLinkedStack stack) { + stack.clear(); + assertThat(stack.isEmpty(), is(true)); + } + + @Test(dataProvider = "empty") + public void clear_whenPopulated(ConcurrentLinkedStack stack) { + stack.clear(); + assertThat(stack.isEmpty(), is(true)); + } + + @Test(dataProvider = "empty") + public void isEmpty_whenEmpty(ConcurrentLinkedStack stack) { + assertThat(stack.isEmpty(), is(true)); + } + + @Test(dataProvider = "populated") + public void isEmpty_whenPopulated(ConcurrentLinkedStack stack) { + assertThat(stack.isEmpty(), is(false)); + } + + @Test(dataProvider = "empty") + public void size_whenEmpty(ConcurrentLinkedStack stack) { + assertThat(stack.size(), is(0)); + } + + @Test(dataProvider = "populated") + public void size_whenPopulated(ConcurrentLinkedStack stack) { + assertThat(stack.size(), is(POPULATED_SIZE)); + } + + @Test(dataProvider = "empty") + public void contains_withNull(ConcurrentLinkedStack stack) { + assertThat(stack.contains(null), is(false)); + } + + @Test(dataProvider = "populated") + public void contains_whenFound(ConcurrentLinkedStack stack) { + assertThat(stack.contains(1), is(true)); + } + + @Test(dataProvider = "populated") + public void contains_whenNotFound(ConcurrentLinkedStack stack) { + assertThat(stack.contains(-1), is(false)); + } + + @Test(dataProvider = "empty", expectedExceptions = NullPointerException.class) + public void push_withNull(ConcurrentLinkedStack stack) { + stack.push(null); + } + + @Test(dataProvider = "empty") + public void push_whenEmpty(ConcurrentLinkedStack stack) { + stack.push(1); + assertThat(stack.peek(), is(1)); + assertThat(stack.size(), is(1)); + } + + @Test(dataProvider = "populated") + public void push_whenPopulated(ConcurrentLinkedStack stack) { + stack.push(1); + assertThat(stack.peek(), is(1)); + assertThat(stack.size(), is(POPULATED_SIZE + 1)); + } + + @Test(dataProvider = "empty") + public void peek_whenEmpty(ConcurrentLinkedStack stack) { + assertThat(stack.peek(), is(nullValue())); + } + + @Test(dataProvider = "populated") + public void peek_whenPopulated(ConcurrentLinkedStack stack) { + assertThat(stack.peek(), is(POPULATED_SIZE - 1)); + } + + @Test(dataProvider = "populated") + public void peek_deadNode(ConcurrentLinkedStack stack) { + Iterator it = stack.iterator(); + it.next(); + it.remove(); + assertThat(stack.peek(), is(POPULATED_SIZE - 2)); + } + + @Test(dataProvider = "empty") + public void pop_whenEmpty(ConcurrentLinkedStack stack) { + assertThat(stack.pop(), is(nullValue())); + } + + @Test(dataProvider = "populated") + public void pop_whenPopulated(ConcurrentLinkedStack stack) { + Integer first = stack.peek(); + assertThat(stack.pop(), is(first)); + assertThat(stack, not(contains(first))); + assertThat(stack.size(), is(POPULATED_SIZE - 1)); + } + + @Test(dataProvider = "populated") + public void pop_toEmpty(ConcurrentLinkedStack stack) { + while (!stack.isEmpty()) { + Integer value = stack.pop(); + assertThat(stack.contains(value), is(false)); + } + assertThat(stack.isEmpty(), is(true)); + } + + @Test(dataProvider = "empty") + public void remove_whenEmpty(ConcurrentLinkedStack stack) { + assertThat(stack.remove(123), is(false)); + } + + @Test(dataProvider = "populated") + public void remove_whenPopulated(ConcurrentLinkedStack stack) { + assertThat(stack.remove(POPULATED_SIZE / 2), is(true)); + assertThat(stack, not(contains(POPULATED_SIZE / 2))); + assertThat(stack.size(), is(POPULATED_SIZE - 1)); + } + + @Test(dataProvider = "populated") + public void remove_toEmpty(ConcurrentLinkedStack stack) { + while (!stack.isEmpty()) { + Integer value = stack.peek(); + assertThat(stack.remove(value), is(true)); + assertThat(stack.contains(value), is(false)); + } + assertThat(stack.isEmpty(), is(true)); + } + + @Test(dataProvider = "empty") + public void asLifoQueue(ConcurrentLinkedStack stack) { + Queue queue = stack.asLifoQueue(); + assertThat(queue.offer(1), is(true)); + assertThat(queue.offer(2), is(true)); + assertThat(queue.offer(3), is(true)); + assertThat(queue.isEmpty(), is(false)); + assertThat(queue.contains(1), is(true)); + assertThat(queue.size(), is(3)); + assertThat(queue.peek(), is(3)); + assertThat(queue.poll(), is(3)); + assertThat(queue.remove(2), is(true)); + assertThat(queue.iterator().hasNext(), is(true)); + queue.clear(); + assertThat(queue.isEmpty(), is(true)); + } + + @Test(dataProvider = "empty") + public void transfer_pushToPop(ConcurrentLinkedStack stack) { + ConcurrentTestHarness.execute(() -> { + setThreadIndex(); + Node node = new Node<>(1); + while (stack.transferOrCombine(node, node) != null) {} + }); + Awaits.await().until(() -> { + setThreadIndex(); + return stack.tryReceive(); + }, is(not(nullValue()))); + } + + @Test(dataProvider = "empty") + public void transfer_pushToPop_batch(ConcurrentLinkedStack stack) { + ConcurrentTestHarness.execute(() -> { + setThreadIndex(); + Node first = new Node<>(1); + Node last = first; + for (int i = 0; i < POPULATED_SIZE; i++) { + last.next = new Node<>(i); + last = last.next; + } + while (stack.transferOrCombine(first, last) != null) {} + }); + Awaits.await().until(() -> { + setThreadIndex(); + return stack.tryReceive(); + }, is(not(nullValue()))); + assertThat(stack, hasSize(POPULATED_SIZE)); + } + + @Test(dataProvider = "empty") + public void transfer_pushToPush(ConcurrentLinkedStack stack) { + AtomicBoolean transferred = new AtomicBoolean(); + AtomicBoolean received = new AtomicBoolean(); + + ConcurrentTestHarness.execute(() -> { + setThreadIndex(); + Node node = new Node<>(1); + for (;;) { + Node result = stack.transferOrCombine(node, node); + if (result == null) { + transferred.set(true); + break; + } else if (result != node) { + received.set(true); + break; + } + } + }); + Node node = new Node<>(2); + Awaits.await().until(() -> { + setThreadIndex(); + Node result = stack.transferOrCombine(node, node); + if (result == null) { + Awaits.await().untilTrue(received); + return true; + } else if (result != node) { + Awaits.await().untilTrue(transferred); + return true; + } + return false; + }); + } + + private static void setThreadIndex() { + UnsafeAccess.UNSAFE.putInt(Thread.currentThread(), ConcurrentLinkedStack.PROBE, 1); + } + + @Test(dataProvider = "empty") + public void concurrent_push(ConcurrentLinkedStack stack) { + ConcurrentTestHarness.timeTasks(NUM_THREADS, () -> { + for (int i = 0; i < COUNT; i++) { + stack.push(i); + } + }); + assertThat(stack, hasSize(NUM_THREADS * COUNT)); + assertThat(stack.size(), is(equalTo(Iterables.size(stack)))); + } + + @Test(dataProvider = "empty") + public void concurrent_pop(ConcurrentLinkedStack stack) { + IntStream.range(0, NUM_THREADS * COUNT).forEach(stack::push); + + ConcurrentTestHarness.timeTasks(NUM_THREADS, () -> { + int count = 0; + do { + if (stack.pop() != null) { + count++; + } + } while (count != COUNT); + }); + assertThat(stack, is(empty())); + } + + @Test(dataProvider = "empty") + public void concurrent_pushAndPop(ConcurrentLinkedStack stack) { + LongAdder pushed = new LongAdder(); + LongAdder popped = new LongAdder(); + + ConcurrentTestHarness.timeTasks(10, () -> { + for (int i = 0; i < 100; i++) { + stack.push(ThreadLocalRandom.current().nextInt()); + pushed.increment(); + + Thread.yield(); + if (stack.pop() != null) { + popped.increment(); + } + } + }); + + for (AtomicReference slot : stack.arena) { + assertThat(slot.get(), is(nullValue())); + } + assertThat(pushed.intValue(), is(equalTo(stack.size() + popped.intValue()))); + } + + @Test(dataProvider = "empty", expectedExceptions = NoSuchElementException.class) + public void iterator_noMoreElements(ConcurrentLinkedStack stack) { + stack.iterator().next(); + } + + @Test(dataProvider = "populated", expectedExceptions = IllegalStateException.class) + public void iterator_removal_unread(ConcurrentLinkedStack stack) { + stack.iterator().remove(); + } + + @Test(dataProvider = "populated", expectedExceptions = IllegalStateException.class) + public void iterator_removal_duplicate(ConcurrentLinkedStack stack) { + Iterator it = stack.iterator(); + it.next(); + it.remove(); + it.remove(); + } + + @Test(dataProvider = "empty") + public void iterator_whenEmpty(ConcurrentLinkedStack stack) { + assertThat(stack.iterator().hasNext(), is(false)); + } + + @Test(dataProvider = "populated") + public void iterator_whenPopulated(ConcurrentLinkedStack stack) { + List list = new ArrayList<>(); + populate(list, POPULATED_SIZE); + Collections.reverse(list); + assertThat(String.format("\nExpected: %s%n but: %s", stack, list), + elementsEqual(stack.iterator(), list.iterator())); + } + + @Test(dataProvider = "empty", expectedExceptions = IllegalStateException.class) + public void iterator_removalWhenEmpty(ConcurrentLinkedStack stack) { + stack.iterator().remove(); + } + + @Test(dataProvider = "populated") + public void iterator_removalWhenPopulated(ConcurrentLinkedStack stack) { + Iterator it = stack.iterator(); + Integer first = stack.peek(); + it.next(); + it.remove(); + assertThat(stack, not(contains(first))); + assertThat(stack.size(), is(POPULATED_SIZE - 1)); + } + + @Test(dataProvider = "empty") + public void serialize_whenEmpty(ConcurrentLinkedStack stack) { + List expected = new ArrayList<>(stack); + List actual = new ArrayList<>(SerializableTester.reserialize(stack)); + assertThat(expected, is(equalTo(actual))); + } + + @Test(dataProvider = "populated") + public void serialize_whenPopulated(ConcurrentLinkedStack stack) { + List expected = new ArrayList<>(stack); + List actual = new ArrayList<>(SerializableTester.reserialize(stack)); + assertThat(expected, is(equalTo(actual))); + } + + /* ---------------- Stack providers -------------- */ + + @DataProvider(name = "empty") + public Object[][] emptyStack() { + return new Object[][] { + { ConcurrentLinkedStack.optimistic() }, + { ConcurrentLinkedStack.linearizable() }}; + } + + @DataProvider(name = "populated") + public Object[][] populatedStack() { + return new Object[][] {{ newPopulatedStack(true) }, { newPopulatedStack(false) }}; + } + + ConcurrentLinkedStack newPopulatedStack(boolean optimistic) { + ConcurrentLinkedStack stack = optimistic + ? ConcurrentLinkedStack.optimistic() + : ConcurrentLinkedStack.linearizable(); + populate(stack, POPULATED_SIZE); + return stack; + } + + static void populate(Collection collection, int start) { + for (int i = 0; i < start; i++) { + collection.add(i); + } + } + + /** A listener that validates the internal structure after a successful test execution. */ + public static final class ValidatingStackListener implements IInvokedMethodListener { + @Override + public void beforeInvocation(IInvokedMethod method, ITestResult testResult) {} + + @Override + public void afterInvocation(IInvokedMethod method, ITestResult testResult) { + try { + if (testResult.isSuccess()) { + for (Object param : testResult.getParameters()) { + if (param instanceof ConcurrentLinkedStack) { + assertThat((ConcurrentLinkedStack) param, is(validate())); + } + } + } + } catch (AssertionError caught) { + testResult.setStatus(ITestResult.FAILURE); + testResult.setThrowable(caught); + } finally { + cleanUp(testResult); + } + } + } + + /** Free memory by clearing unused resources after test execution. */ + static void cleanUp(ITestResult testResult) { + Object[] params = testResult.getParameters(); + for (int i = 0; i < params.length; i++) { + Object param = params[i]; + if ((param instanceof ConcurrentLinkedStack)) { + params[i] = param.getClass().getSimpleName(); + } else { + params[i] = Objects.toString(param); + } + } + } +} diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/EliminationStackTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/EliminationStackTest.java deleted file mode 100644 index 3abdea9e8f..0000000000 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/EliminationStackTest.java +++ /dev/null @@ -1,370 +0,0 @@ -/* - * Copyright 2013 Ben Manes. All Rights Reserved. - * - * 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.github.benmanes.caffeine; - -import static com.google.common.collect.Iterators.elementsEqual; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; -import static org.hamcrest.Matchers.nullValue; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Queue; -import java.util.concurrent.ThreadLocalRandom; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicReference; -import java.util.concurrent.atomic.LongAdder; - -import org.testng.annotations.DataProvider; -import org.testng.annotations.Test; - -import com.google.common.testing.SerializableTester; - -/** - * @author ben.manes@gmail.com (Ben Manes) - */ -public final class EliminationStackTest { - static final int POPULATED_SIZE = 5; - - @Test(dataProvider = "empty") - public void clear_whenEmpty(EliminationStack stack) { - stack.clear(); - assertThat(stack.isEmpty(), is(true)); - } - - @Test(dataProvider = "empty") - public void clear_whenPopulated(EliminationStack stack) { - stack.clear(); - assertThat(stack.isEmpty(), is(true)); - } - - @Test(dataProvider = "empty") - public void isEmpty_whenEmpty(EliminationStack stack) { - assertThat(stack.isEmpty(), is(true)); - } - - @Test(dataProvider = "populated") - public void isEmpty_whenPopulated(EliminationStack stack) { - assertThat(stack.isEmpty(), is(false)); - } - - @Test(dataProvider = "empty") - public void size_whenEmpty(EliminationStack stack) { - assertThat(stack.size(), is(0)); - } - - @Test(dataProvider = "populated") - public void size_whenPopulated(EliminationStack stack) { - assertThat(stack.size(), is(POPULATED_SIZE)); - } - - @Test(dataProvider = "empty", expectedExceptions = NullPointerException.class) - public void contains_withNull(EliminationStack stack) { - stack.contains(null); - } - - @Test(dataProvider = "populated") - public void contains_whenFound(EliminationStack stack) { - assertThat(stack.contains(1), is(true)); - } - - @Test(dataProvider = "populated") - public void contains_whenNotFound(EliminationStack stack) { - assertThat(stack.contains(-1), is(false)); - } - - @Test(dataProvider = "empty", expectedExceptions = NullPointerException.class) - public void push_withNull(EliminationStack stack) { - stack.push(null); - } - - @Test(dataProvider = "empty") - public void push_whenEmpty(EliminationStack stack) { - stack.push(1); - assertThat(stack.peek(), is(1)); - assertThat(stack.size(), is(1)); - } - - @Test(dataProvider = "populated") - public void push_whenPopulated(EliminationStack stack) { - stack.push(1); - assertThat(stack.peek(), is(1)); - assertThat(stack.size(), is(POPULATED_SIZE + 1)); - } - - @Test(dataProvider = "empty") - public void peek_whenEmpty(EliminationStack stack) { - assertThat(stack.peek(), is(nullValue())); - } - - @Test(dataProvider = "populated") - public void peek_whenPopulated(EliminationStack stack) { - assertThat(stack.peek(), is(POPULATED_SIZE - 1)); - } - - @Test(dataProvider = "populated") - public void peek_deadNode(EliminationStack stack) { - Iterator it = stack.iterator(); - it.next(); - it.remove(); - assertThat(stack.peek(), is(POPULATED_SIZE - 2)); - } - - @Test(dataProvider = "empty") - public void pop_whenEmpty(EliminationStack stack) { - assertThat(stack.pop(), is(nullValue())); - } - - @Test(dataProvider = "populated") - public void pop_whenPopulated(EliminationStack stack) { - Integer first = stack.peek(); - assertThat(stack.pop(), is(first)); - assertThat(stack, not(contains(first))); - assertThat(stack.size(), is(POPULATED_SIZE - 1)); - } - - @Test(dataProvider = "populated") - public void pop_toEmpty(EliminationStack stack) { - while (!stack.isEmpty()) { - Integer value = stack.pop(); - assertThat(stack.contains(value), is(false)); - } - assertThat(stack.isEmpty(), is(true)); - } - - @Test(dataProvider = "empty") - public void remove_whenEmpty(EliminationStack stack) { - assertThat(stack.remove(123), is(false)); - } - - @Test(dataProvider = "populated") - public void remove_whenPopulated(EliminationStack stack) { - assertThat(stack.remove(POPULATED_SIZE / 2), is(true)); - assertThat(stack, not(contains(POPULATED_SIZE / 2))); - assertThat(stack.size(), is(POPULATED_SIZE - 1)); - } - - @Test(dataProvider = "populated") - public void remove_toEmpty(EliminationStack stack) { - while (!stack.isEmpty()) { - Integer value = stack.peek(); - assertThat(stack.remove(value), is(true)); - assertThat(stack.contains(value), is(false)); - } - assertThat(stack.isEmpty(), is(true)); - } - - @Test(dataProvider = "empty") - public void asLifoQueue(EliminationStack stack) { - Queue queue = stack.asLifoQueue(); - assertThat(queue.offer(1), is(true)); - assertThat(queue.offer(2), is(true)); - assertThat(queue.offer(3), is(true)); - assertThat(queue.isEmpty(), is(false)); - assertThat(queue.contains(1), is(true)); - assertThat(queue.size(), is(3)); - assertThat(queue.peek(), is(3)); - assertThat(queue.poll(), is(3)); - assertThat(queue.remove(2), is(true)); - assertThat(queue.iterator().hasNext(), is(true)); - queue.clear(); - assertThat(queue.isEmpty(), is(true)); - } - - @Test(dataProvider = "empty") - public void concurrent(final EliminationStack stack) throws Exception { - final LongAdder pushed = new LongAdder(); - final LongAdder popped = new LongAdder(); - - ConcurrentTestHarness.timeTasks(10, () -> { - for (int i = 0; i < 100; i++) { - stack.push(ThreadLocalRandom.current().nextInt()); - pushed.increment(); - - Thread.yield(); - if (stack.pop() != null) { - popped.increment(); - } - } - }); - - for (AtomicReference slot : stack.arena) { - assertThat(slot.get(), is(nullValue())); - } - assertThat(pushed.intValue(), is(equalTo(stack.size() + popped.intValue()))); - } - - @Test(dataProvider = "empty") - public void scanAndTransfer(final EliminationStack stack) { - final AtomicBoolean started = new AtomicBoolean(); - final AtomicBoolean done = new AtomicBoolean(); - final String value = "test"; - final int startIndex = 1; - ConcurrentTestHarness.execute(() -> { - started.set(true); - while (!done.get()) { - if (stack.scanAndTransferToWaiter(value, startIndex)) { - done.set(true); - } - } - }); - Awaits.await().untilTrue(started); - - try { - Awaits.await().until(() -> stack.awaitMatch(startIndex), is(value)); - } finally { - done.set(true); - } - } - - @Test(dataProvider = "empty") - public void awaitExchange(final EliminationStack stack) { - final AtomicBoolean started = new AtomicBoolean(); - final AtomicBoolean done = new AtomicBoolean(); - final String value = "test"; - final int startIndex = 1; - ConcurrentTestHarness.execute(() -> { - started.set(true); - while (!done.get()) { - if (stack.awaitExchange(value, startIndex)) { - done.set(true); - } - } - }); - Awaits.await().untilTrue(started); - - try { - Awaits.await().until(() -> stack.awaitMatch(startIndex), is(value)); - } finally { - done.set(true); - } - } - - @Test(dataProvider = "empty") - public void scanAndMatch(final EliminationStack stack) { - final AtomicBoolean started = new AtomicBoolean(); - final AtomicBoolean done = new AtomicBoolean(); - final String value = "test"; - final int startIndex = 1; - ConcurrentTestHarness.execute(() -> { - started.set(true); - while (!done.get()) { - if (stack.awaitExchange(value, startIndex)) { - done.set(true); - } - } - }); - Awaits.await().untilTrue(started); - - try { - Awaits.await().until(() -> stack.scanAndMatch(startIndex), is(value)); - } finally { - done.set(true); - } - } - - @Test(dataProvider = "empty", expectedExceptions = NoSuchElementException.class) - public void iterator_noMoreElements(EliminationStack stack) { - stack.iterator().next(); - } - - @Test(dataProvider = "populated", expectedExceptions = IllegalStateException.class) - public void iterator_removal_unread(EliminationStack stack) { - stack.iterator().remove(); - } - - @Test(dataProvider = "populated", expectedExceptions = IllegalStateException.class) - public void iterator_removal_duplicate(EliminationStack stack) { - Iterator it = stack.iterator(); - it.next(); - it.remove(); - it.remove(); - } - - @Test(dataProvider = "empty") - public void iterator_whenEmpty(EliminationStack stack) { - assertThat(stack.iterator().hasNext(), is(false)); - } - - @Test(dataProvider = "populated") - public void iterator_whenPopulated(EliminationStack stack) { - List list = new ArrayList<>(); - populate(list, POPULATED_SIZE); - Collections.reverse(list); - assertThat(String.format("\nExpected: %s%n but: %s", stack, list), - elementsEqual(stack.iterator(), list.iterator())); - } - - @Test(dataProvider = "empty", expectedExceptions = IllegalStateException.class) - public void iterator_removalWhenEmpty(EliminationStack stack) { - stack.iterator().remove(); - } - - @Test(dataProvider = "populated") - public void iterator_removalWhenPopulated(EliminationStack stack) { - Iterator it = stack.iterator(); - Integer first = stack.peek(); - it.next(); - it.remove(); - assertThat(stack, not(contains(first))); - assertThat(stack.size(), is(POPULATED_SIZE - 1)); - } - - @Test(dataProvider = "empty") - public void serialize_whenEmpty(EliminationStack stack) { - List expected = new ArrayList<>(stack); - List actual = new ArrayList<>(SerializableTester.reserialize(stack)); - assertThat(expected, is(equalTo(actual))); - } - - @Test(dataProvider = "populated") - public void serialize_whenPopulated(EliminationStack stack) { - List expected = new ArrayList<>(stack); - List actual = new ArrayList<>(SerializableTester.reserialize(stack)); - assertThat(expected, is(equalTo(actual))); - } - - /* ---------------- Stack providers -------------- */ - - @DataProvider(name = "empty") - public Object[][] emptyStack() { - return new Object[][] {{ new EliminationStack() }}; - } - - @DataProvider(name = "populated") - public Object[][] populatedStack() { - return new Object[][] {{ newPopulatedStack() }}; - } - - EliminationStack newPopulatedStack() { - EliminationStack stack = new EliminationStack(); - populate(stack, POPULATED_SIZE); - return stack; - } - - static void populate(Collection collection, int start) { - for (int i = 0; i < start; i++) { - collection.add(i); - } - } -} diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/IsValidConcurrentLinkedStack.java b/caffeine/src/test/java/com/github/benmanes/caffeine/IsValidConcurrentLinkedStack.java new file mode 100644 index 0000000000..5525903f26 --- /dev/null +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/IsValidConcurrentLinkedStack.java @@ -0,0 +1,88 @@ +/* + * Copyright 2014 Ben Manes. All Rights Reserved. + * + * 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.github.benmanes.caffeine; + +import static com.github.benmanes.caffeine.matchers.IsEmptyIterable.deeplyEmpty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.hamcrest.Description; +import org.hamcrest.Factory; +import org.hamcrest.TypeSafeDiagnosingMatcher; + +import com.github.benmanes.caffeine.ConcurrentLinkedStack.Node; +import com.github.benmanes.caffeine.matchers.DescriptionBuilder; +import com.google.common.collect.Sets; + +/** + * A matcher that evaluates a {@link ConcurrentLinkedStack} to determine if it is in a valid state. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +public final class IsValidConcurrentLinkedStack + extends TypeSafeDiagnosingMatcher> { + + @Override + public void describeTo(Description description) { + description.appendText("singleConsumerQueue"); + } + + @Override + protected boolean matchesSafely(ConcurrentLinkedStack stack, Description description) { + DescriptionBuilder builder = new DescriptionBuilder(description); + + if (stack.isEmpty()) { + builder.expectThat("empty stack", stack, is(deeplyEmpty())); + builder.expectThat("empty stack", stack.top, is(nullValue())); + builder.expectThat("empty stack", stack.asLifoQueue(), is(deeplyEmpty())); + } + checkForLoop(stack, builder); + checkArena(stack, builder); + + return builder.matches(); + } + + void checkForLoop(ConcurrentLinkedStack stack, DescriptionBuilder builder) { + Set> seen = Sets.newIdentityHashSet(); + Node node = stack.top; + while (node != null) { + if (node.get() != null) { + Node current = node; + Supplier errorMsg = () -> String.format("Loop detected: %s in %s", current, seen); + builder.expectThat(errorMsg, seen.add(node), is(true)); + builder.expectThat("not completed", node.isDone(), is(true)); + } + node = node.next; + } + builder.expectThat("stack size", stack, hasSize(seen.size())); + } + + void checkArena(ConcurrentLinkedStack stack, DescriptionBuilder builder) { + for (AtomicReference slot : stack.arena) { + builder.expectThat("not null arena slot", slot.get(), is(nullValue())); + } + } + + @Factory + public static IsValidConcurrentLinkedStack validate() { + return new IsValidConcurrentLinkedStack(); + } +} diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/SingleConsumerQueueTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/SingleConsumerQueueTest.java index a1fa44e6ee..b23d470204 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/SingleConsumerQueueTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/SingleConsumerQueueTest.java @@ -45,7 +45,7 @@ import org.testng.annotations.Test; import com.github.benmanes.caffeine.SingleConsumerQueue.LinearizableNode; -import com.github.benmanes.caffeine.SingleConsumerQueueTest.ValidatingListener; +import com.github.benmanes.caffeine.SingleConsumerQueueTest.ValidatingQueueListener; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.testing.SerializableTester; @@ -53,7 +53,7 @@ /** * @author ben.manes@gmail.com (Ben Manes) */ -@Listeners(ValidatingListener.class) +@Listeners(ValidatingQueueListener.class) public class SingleConsumerQueueTest { private static final int PRODUCE = 10_000; private static final int NUM_PRODUCERS = 10; @@ -528,7 +528,7 @@ static void populate(Collection collection, int start) { } /** A listener that validates the internal structure after a successful test execution. */ - public static final class ValidatingListener implements IInvokedMethodListener { + public static final class ValidatingQueueListener implements IInvokedMethodListener { @Override public void beforeInvocation(IInvokedMethod method, ITestResult testResult) {} diff --git a/guava/src/test/java/com/github/benmanes/caffeine/ConcurrentLinkedStackTests.java b/guava/src/test/java/com/github/benmanes/caffeine/ConcurrentLinkedStackTests.java new file mode 100644 index 0000000000..081d9d3bc3 --- /dev/null +++ b/guava/src/test/java/com/github/benmanes/caffeine/ConcurrentLinkedStackTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015 Ben Manes. All Rights Reserved. + * + * 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.github.benmanes.caffeine; + +import java.util.Collection; +import java.util.Queue; + +import com.google.common.collect.testing.CollectionTestSuiteBuilder; +import com.google.common.collect.testing.MinimalCollection; +import com.google.common.collect.testing.QueueTestSuiteBuilder; +import com.google.common.collect.testing.TestStringCollectionGenerator; +import com.google.common.collect.testing.TestStringQueueGenerator; +import com.google.common.collect.testing.features.CollectionFeature; +import com.google.common.collect.testing.features.CollectionSize; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Guava testlib map tests for {@link EliminationStack}. + * + * @author ben.manes@gmail.com (Ben Manes) + */ +public final class ConcurrentLinkedStackTests extends TestCase { + + public static Test suite() { + TestSuite suite = stackTest(true); + suite.addTest(stackTest(false)); + suite.addTest(queueTest(true)); + suite.addTest(queueTest(false)); + return suite; + } + + public static TestSuite stackTest(boolean optimistic) { + return CollectionTestSuiteBuilder + .using(new TestStringCollectionGenerator() { + @Override public Collection create(String[] elements) { + ConcurrentLinkedStack stack = optimistic + ? ConcurrentLinkedStack.optimistic() + : ConcurrentLinkedStack.linearizable(); + stack.addAll(MinimalCollection.of(elements)); + return stack; + } + }) + .named(ConcurrentLinkedStack.class.getSimpleName()) + .withFeatures( + CollectionFeature.ALLOWS_NULL_QUERIES, + CollectionFeature.GENERAL_PURPOSE, + CollectionFeature.SERIALIZABLE, + CollectionSize.ANY) + .createTestSuite(); + } + + public static TestSuite queueTest(boolean optimistic) { + return QueueTestSuiteBuilder + .using(new TestStringQueueGenerator() { + @Override public Queue create(String[] elements) { + ConcurrentLinkedStack stack = optimistic + ? ConcurrentLinkedStack.optimistic() + : ConcurrentLinkedStack.linearizable(); + stack.addAll(MinimalCollection.of(elements)); + return stack.asLifoQueue(); + } + }) + .named(ConcurrentLinkedStack.class.getSimpleName()) + .withFeatures( + CollectionFeature.ALLOWS_NULL_QUERIES, + CollectionFeature.GENERAL_PURPOSE, + CollectionFeature.SERIALIZABLE, + CollectionSize.ANY) + .createTestSuite(); + } +} diff --git a/guava/src/test/java/com/github/benmanes/caffeine/EliminationStackTests.java b/guava/src/test/java/com/github/benmanes/caffeine/EliminationStackTests.java deleted file mode 100644 index 62fa82250d..0000000000 --- a/guava/src/test/java/com/github/benmanes/caffeine/EliminationStackTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2015 Ben Manes. All Rights Reserved. - * - * 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.github.benmanes.caffeine; - -import java.util.Queue; - -import junit.framework.Test; -import junit.framework.TestCase; - -import com.google.common.collect.testing.MinimalCollection; -import com.google.common.collect.testing.QueueTestSuiteBuilder; -import com.google.common.collect.testing.TestStringQueueGenerator; -import com.google.common.collect.testing.features.CollectionFeature; -import com.google.common.collect.testing.features.CollectionSize; - -/** - * Guava testlib map tests for {@link EliminationStack}. - * - * @author ben.manes@gmail.com (Ben Manes) - */ -public final class EliminationStackTests extends TestCase { - - public static Test suite() throws NoSuchMethodException, SecurityException { - return QueueTestSuiteBuilder - .using(new TestStringQueueGenerator() { - @Override public Queue create(String[] elements) { - return new EliminationStack<>(MinimalCollection.of(elements)).asLifoQueue(); - } - }) - .named(EliminationStack.class.getSimpleName()) - .withFeatures( - CollectionFeature.GENERAL_PURPOSE, - CollectionSize.ANY) - .createTestSuite(); - } -} diff --git a/guava/src/test/java/com/github/benmanes/caffeine/PackageSanityTests.java b/guava/src/test/java/com/github/benmanes/caffeine/PackageSanityTests.java index ce6024b21d..4c6c0c0ce9 100644 --- a/guava/src/test/java/com/github/benmanes/caffeine/PackageSanityTests.java +++ b/guava/src/test/java/com/github/benmanes/caffeine/PackageSanityTests.java @@ -28,6 +28,8 @@ public PackageSanityTests() { publicApiOnly(); ignoreClasses(clazz -> clazz == Awaits.class || + clazz == SingleConsumerQueue.class || + clazz == ConcurrentLinkedStack.class || clazz.getSimpleName().startsWith("Is") || clazz.getSimpleName().contains("Test") || clazz.getSimpleName().contains("Stresser") || diff --git a/guava/src/test/java/com/github/benmanes/caffeine/SingleConsumerQueueTests.java b/guava/src/test/java/com/github/benmanes/caffeine/SingleConsumerQueueTests.java index c956ab9f1d..229cf8f4b9 100644 --- a/guava/src/test/java/com/github/benmanes/caffeine/SingleConsumerQueueTests.java +++ b/guava/src/test/java/com/github/benmanes/caffeine/SingleConsumerQueueTests.java @@ -53,7 +53,9 @@ private static TestSuite queueTest(boolean optimistic) { }) .named(SingleConsumerQueue.class.getSimpleName()) .withFeatures( + CollectionFeature.ALLOWS_NULL_QUERIES, CollectionFeature.GENERAL_PURPOSE, + CollectionFeature.SERIALIZABLE, CollectionFeature.KNOWN_ORDER, CollectionSize.ANY) .createTestSuite(); diff --git a/guava/src/test/java/com/github/benmanes/caffeine/cache/MapTestFactory.java b/guava/src/test/java/com/github/benmanes/caffeine/cache/MapTestFactory.java index 7a23dd0da1..5055d5f468 100644 --- a/guava/src/test/java/com/github/benmanes/caffeine/cache/MapTestFactory.java +++ b/guava/src/test/java/com/github/benmanes/caffeine/cache/MapTestFactory.java @@ -18,14 +18,14 @@ import java.util.Map; import java.util.Map.Entry; -import junit.framework.Test; - import com.google.common.collect.testing.MapTestSuiteBuilder; import com.google.common.collect.testing.TestStringMapGenerator; import com.google.common.collect.testing.features.CollectionFeature; import com.google.common.collect.testing.features.CollectionSize; import com.google.common.collect.testing.features.MapFeature; +import junit.framework.Test; + /** * A JUnit test suite factory for the map tests from Guava's testlib. * diff --git a/wiki/concurrent_linked_stack.png b/wiki/concurrent_linked_stack.png new file mode 100644 index 0000000000000000000000000000000000000000..c7ccfd0bee6d56a51e24026a9081497acce04ccb GIT binary patch literal 64680 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

*G7G@&cIWhx*ecQC!l9T2xM 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 literal 0 HcmV?d00001