org.apache.activemq.artemis.utils.collections.LinkedListImpl Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.apache.activemq.artemis.utils.collections;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Array;
import java.util.Comparator;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.function.Consumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A linked list implementation which allows multiple iterators to exist at the same time on the queue, and which see any
* elements added or removed from the queue either directly or via iterators.
*
* This class is not thread safe.
*/
public class LinkedListImpl implements LinkedList {
private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final int INITIAL_ITERATOR_ARRAY_SIZE = 10;
private final Node head = new NodeHolder<>(null);
private final Comparator comparator;
private Node tail = null;
private int size;
// We store in an array rather than a Map for the best performance
private volatile Iterator[] iters;
private int numIters;
private int nextIndex;
private NodeStore nodeStore;
private volatile Node lastAdd;
public LinkedListImpl() {
this(null, null);
}
public LinkedListImpl(Comparator comparator) {
this(comparator, null);
}
public LinkedListImpl(Comparator comparator, NodeStore supplier) {
iters = createIteratorArray(INITIAL_ITERATOR_ARRAY_SIZE);
this.comparator = comparator;
this.nodeStore = supplier;
}
@Override
public void clearID() {
if (nodeStore != null) {
nodeStore.clear();
}
nodeStore = null;
}
@Override
public void setNodeStore(NodeStore supplier) {
this.nodeStore = supplier;
try (Iterator iterator = (Iterator) iterator()) {
while (iterator.hasNext()) {
E value = iterator.next();
Node position = iterator.last;
putID(value, position);
}
}
}
private void putID(E element, Node node) {
nodeStore.storeNode(element, node);
}
@Override
public void addHead(E e) {
Node node = Node.with(e);
node.next = head.next;
node.prev = head;
head.next = node;
if (size == 0) {
tail = node;
} else {
// Need to set the previous element on the former head
node.next.prev = node;
}
itemAdded(node, e);
size++;
}
@Override
public E peek() {
Node current = head.next;
if (current == null) {
return null;
} else {
return current.val();
}
}
@Override
public E get(int position) {
Node current = head.next;
for (int i = 0; i < position && current != null; i++) {
current = current.next;
}
if (current == null) {
throw new IndexOutOfBoundsException(position + " > " + size());
}
return current.val();
}
@Override
public synchronized E removeWithID(String listID, long id) {
assert nodeStore != null; // it is assumed the code will call setNodeStore before callin removeWithID
Node node = nodeStore.getNode(listID, id);
if (node == null) {
return null;
}
// the node will always have a prev element
removeAfter(node.prev);
return node.val();
}
@Override
public void forEach(Consumer consumer) {
try (LinkedListIterator iter = iterator()) {
while (iter.hasNext()) {
E nextValue = iter.next();
consumer.accept(nextValue);
}
}
}
private void itemAdded(Node node, E item) {
assert node.val() == item;
lastAdd = node;
if (logger.isTraceEnabled()) {
logger.trace("Setting lastAdd as {}, e={}", lastAdd, lastAdd.val());
}
if (nodeStore != null) {
putID(item, node);
}
}
private void itemRemoved(Node node) {
lastAdd = null;
if (nodeStore != null) {
nodeStore.removeNode(node.val(), node);
}
}
@Override
public void addTail(E e) {
if (size == 0) {
addHead(e);
} else {
Node node = Node.with(e);
node.prev = tail;
tail.next = node;
tail = node;
itemAdded(node, e);
size++;
}
}
public void addSorted(E e) {
final Node localLastAdd = lastAdd;
logger.trace("**** addSorted element {}", e);
if (comparator == null) {
throw new NullPointerException("comparator=null");
}
if (size == 0) {
logger.trace("adding head as there are no elements {}", e);
addHead(e);
} else {
if (comparator.compare(head.next.val(), e) < 0) {
if (logger.isTraceEnabled()) {
logger.trace("addHead as e={} and head={}", e, head.next.val());
}
addHead(e);
return;
}
// in our usage, most of the times we will just add to the end
// as the QueueImpl cancellations in AMQP will return the buffer back to the queue, in the order they were consumed.
// There is an exception to that case, when there are more messages on the queue.
// This would be an optimization for our usage.
// avoiding scanning the entire List just to add at the end, so we compare the end first.
if (comparator.compare(tail.val(), e) >= 0) {
logger.trace("addTail as e={} and tail={}", e, tail.val());
addTail(e);
return;
}
if (localLastAdd != null) { // as an optimization we check against the last add rather than always scan.
if (logger.isDebugEnabled()) {
logger.debug("localLastAdd Value = {}, we are adding {}", localLastAdd.val(), e);
}
int compareLastAdd = comparator.compare(localLastAdd.val(), e);
if (compareLastAdd > 0) {
if (scanRight(localLastAdd, e)) {
return;
}
}
if (compareLastAdd < 0) {
if (scanLeft(localLastAdd, e)) {
return;
}
}
}
if (addSortedScan(e)) {
return;
}
// this shouldn't happen as the tail was compared before iterating
// the only possibilities for this to happen are:
// - there is a bug on the comparator
// - This method is buggy
// - The list wasn't properly synchronized as this list does't support concurrent access
//
// Also I'm not bothering about creating a Logger ID for this, because the only reason for this code to exist
// is because my OCD level is not letting this out.
throw new IllegalStateException("Cannot find a suitable place for your element, There's a mismatch in the comparator or there was concurrent adccess on the queue");
}
}
protected boolean scanRight(Node position, E e) {
Node fetching = position.next;
while (fetching != null) {
if (comparator.compare(fetching.val(), e) < 0) {
addAfter(position, e);
return true;
}
position = fetching;
fetching = fetching.next;
}
return false; // unlikely to happen, using this just to be safe
}
protected boolean scanLeft(Node position, E e) {
Node fetching = position.prev;
while (fetching != null) {
if (comparator.compare(fetching.val(), e) > 0) {
addAfter(fetching, e);
return true;
}
fetching = fetching.prev;
}
return false; // unlikely to happen, using this just to be safe
}
protected boolean addSortedScan(E e) {
logger.trace("addSortedScan {}...", e);
Node fetching = head.next;
while (fetching.next != null) {
int compareNext = comparator.compare(fetching.next.val(), e);
if (compareNext <= 0) {
addAfter(fetching, e);
logger.trace("... addSortedScan done, returning true");
return true;
}
fetching = fetching.next;
}
logger.trace("... addSortedScan done, could not find a spot, returning false");
return false;
}
private void addAfter(Node node, E e) {
Node newNode = Node.with(e);
Node nextNode = node.next;
node.next = newNode;
newNode.prev = node;
newNode.next = nextNode;
nextNode.prev = newNode;
itemAdded(newNode, e);
size++;
}
@Override
public E poll() {
Node ret = head.next;
if (ret != null) {
removeAfter(head);
return ret.val();
} else {
return null;
}
}
@Override
public void clear() {
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
while (poll() != null) {
}
}
@Override
public int size() {
return size;
}
@Override
public LinkedListIterator iterator() {
return new Iterator();
}
@Override
public String toString() {
StringBuilder str = new StringBuilder("LinkedListImpl [ ");
Node node = head;
while (node != null) {
str.append(node.toString());
if (node.next != null) {
str.append(", ");
}
node = node.next;
}
return str.toString();
}
public int numIters() {
return numIters;
}
private Iterator[] createIteratorArray(int size) {
return (Iterator[]) Array.newInstance(Iterator.class, size);
}
private void removeAfter(Node node) {
Node toRemove = node.next;
node.next = toRemove.next;
if (toRemove.next != null) {
toRemove.next.prev = node;
}
itemRemoved(toRemove);
if (toRemove == tail) {
tail = node;
}
size--;
if (toRemove.iterCount != 0) {
LinkedListImpl.this.nudgeIterators(toRemove);
}
//Help GC - otherwise GC potentially has to traverse a very long list to see if elements are reachable, this can result in OOM
//https://jira.jboss.org/browse/HORNETQ-469
toRemove.next = toRemove.prev = null;
}
private synchronized void nudgeIterators(Node node) {
for (int i = 0; i < numIters; i++) {
Iterator iter = iters[i];
if (iter != null) {
iter.nudged(node);
}
}
}
private synchronized void addIter(Iterator iter) {
if (numIters == iters.length) {
resize(2 * numIters);
}
iters[nextIndex++] = iter;
numIters++;
}
private synchronized void resize(int newSize) {
Iterator[] newIters = createIteratorArray(newSize);
System.arraycopy(iters, 0, newIters, 0, numIters);
iters = newIters;
}
private synchronized void removeIter(Iterator iter) {
for (int i = 0; i < numIters; i++) {
if (iter == iters[i]) {
iters[i] = null;
if (i != numIters - 1) {
// Fill in the hole
System.arraycopy(iters, i + 1, iters, i, numIters - i - 1);
}
numIters--;
if (numIters >= INITIAL_ITERATOR_ARRAY_SIZE && numIters == iters.length / 2) {
resize(numIters);
}
nextIndex--;
return;
}
}
throw new IllegalStateException("Cannot find iter to remove");
}
private static final class NodeHolder extends Node {
private final E val;
//only the head is allowed to hold a null
private NodeHolder(E e) {
val = e;
}
@Override
protected E val() {
return val;
}
}
public static class Node {
private Node next;
private Node prev;
private int iterCount;
private static Node with(final E o) {
Objects.requireNonNull(o, "Only HEAD nodes are allowed to hold null values");
if (o instanceof Node) {
final Node node = (Node) o;
//only a node that not belong already to a list is allowed to be reused
if (node.prev == null && node.next == null) {
//reset the iterCount
node.iterCount = 0;
return node;
}
}
return new NodeHolder<>(o);
}
@SuppressWarnings("unchecked")
protected E val() {
return (E) this;
}
protected final LinkedListImpl.Node next() {
return next;
}
protected final LinkedListImpl.Node prev() {
return prev;
}
@Override
public String toString() {
return val() == this ? "Intrusive Node" : "Node, value = " + val();
}
}
private class Iterator implements LinkedListIterator {
Node last;
Node current = head.next;
boolean repeat;
Iterator() {
if (current != null) {
current.iterCount++;
}
addIter(this);
}
@Override
public void repeat() {
repeat = true;
}
@Override
public boolean hasNext() {
synchronized (LinkedListImpl.this) {
Node e = getNode();
if (e != null && (e != last || repeat)) {
return true;
}
return canAdvance();
}
}
@Override
public E next() {
synchronized (LinkedListImpl.this) {
Node e = getNode();
if (repeat) {
repeat = false;
if (e != null) {
return e.val();
} else {
if (canAdvance()) {
advance();
e = getNode();
return e.val();
} else {
throw new NoSuchElementException();
}
}
}
if (e == null || e == last) {
if (canAdvance()) {
advance();
e = getNode();
} else {
throw new NoSuchElementException();
}
}
last = e;
repeat = false;
return e.val();
}
}
@Override
public void remove() {
synchronized (LinkedListImpl.this) {
if (last == null) {
throw new NoSuchElementException();
}
if (current == null) {
return;
}
Node prev = current.prev;
if (prev != null) {
LinkedListImpl.this.removeAfter(prev);
last = null;
}
}
}
@Override
public void close() {
removeIter(this);
}
public void nudged(Node node) {
if (current == node) {
if (canAdvance()) {
advance();
} else {
if (current.prev != head) {
current.iterCount--;
current = current.prev;
current.iterCount++;
if (last == node) {
last = current;
}
} else {
current = null;
}
}
}
}
private Node getNode() {
if (current == null) {
current = head.next;
if (current != null) {
current.iterCount++;
}
}
if (current != null) {
return current;
} else {
return null;
}
}
private boolean canAdvance() {
if (current == null) {
current = head.next;
if (current != null) {
current.iterCount++;
}
}
return current != null && current.next != null;
}
private void advance() {
if (current == null || current.next == null) {
throw new NoSuchElementException();
}
current.iterCount--;
current = current.next;
current.iterCount++;
}
}
}