├── .gitignore ├── renovate.json ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ └── java │ │ └── lbmq │ │ ├── package-info.java │ │ ├── DefaultSubQueueSelection.java │ │ ├── AbstractPollable.java │ │ ├── AbstractOfferable.java │ │ ├── Offerable.java │ │ ├── Pollable.java │ │ └── LinkedBlockingMultiQueue.java └── test │ └── java │ └── lbmq │ ├── VersionLoggerTest.java │ ├── Benchmark.java │ ├── BenchmarkLoopLbmq.java │ ├── BenchmarkLoopAlt.java │ ├── TestCase.java │ └── LinkedBlockingMultiQueueTest.java ├── gradle.properties ├── .github └── workflows │ └── main.yml ├── LICENSE.txt ├── gradlew.bat ├── README.md └── gradlew /.gitignore: -------------------------------------------------------------------------------- 1 | /.gradle/ 2 | /build/ 3 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marianobarrios/linked-blocking-multi-queue/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/lbmq/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * "lbmq" is short for "Linked Blocking Multi Queue", which is a concurrent queue that complements 3 | * the ones offered in the standard package {@link java.util.concurrent} 4 | */ 5 | package lbmq; 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/test/java/lbmq/VersionLoggerTest.java: -------------------------------------------------------------------------------- 1 | package lbmq; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | /** 6 | * Not really a test, but a way of confirming that tests are running with the intended JVM version. 7 | */ 8 | public class VersionLoggerTest { 9 | 10 | @Test 11 | public void versionLogger() { 12 | System.out.printf("Running in JVM version %s\n", System.getProperty("java.version")); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Work around Google java format problem: https://github.com/diffplug/spotless/issues/834 2 | org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ 3 | --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ 4 | --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ 5 | --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ 6 | --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | java-version: [ 8, 11, 17, 21, 25 ] 9 | steps: 10 | - uses: actions/checkout@v6 11 | 12 | - uses: actions/setup-java@v5 13 | with: 14 | distribution: temurin 15 | # Being in the last position, the latest JDK is used by default to build, the version from the matrix is made 16 | # available to run the tests, to check compatibility. 17 | java-version: | 18 | ${{ matrix.java-version }} 19 | 25 20 | 21 | - run: ./gradlew assemble check --info 22 | env: 23 | TEST_JAVA_TOOLCHAIN: ${{ matrix.java-version }} 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2019 by the authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided 4 | that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and 7 | the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and 10 | the following disclaimer in the documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED 13 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 14 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 15 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 16 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 17 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 18 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 19 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | 21 | -------------------------------------------------------------------------------- /src/main/java/lbmq/DefaultSubQueueSelection.java: -------------------------------------------------------------------------------- 1 | package lbmq; 2 | 3 | import java.util.ArrayList; 4 | 5 | /** 6 | * Chooses the next queue to be used from the highest priority group. If no queue is found it searches the lower 7 | * priority groups and so on until it finds a queue. 8 | */ 9 | public class DefaultSubQueueSelection implements LinkedBlockingMultiQueue.SubQueueSelection { 10 | 11 | private ArrayList.PriorityGroup> priorityGroups; 12 | 13 | @Override 14 | public LinkedBlockingMultiQueue.SubQueue getNext() { 15 | for (LinkedBlockingMultiQueue.PriorityGroup priorityGroup : priorityGroups) { 16 | LinkedBlockingMultiQueue.SubQueue subQueue = priorityGroup.getNextSubQueue(); 17 | if (subQueue != null) { 18 | return subQueue; 19 | } 20 | } 21 | return null; 22 | } 23 | 24 | @Override 25 | public E peek() { 26 | // assert takeLock.isHeldByCurrentThread(); 27 | for (LinkedBlockingMultiQueue.PriorityGroup priorityGroup : priorityGroups) { 28 | E dequed = priorityGroup.peek(); 29 | if (dequed != null) { 30 | return dequed; 31 | } 32 | } 33 | return null; 34 | } 35 | 36 | @Override 37 | public void setPriorityGroups(ArrayList.PriorityGroup> priorityGroups) { 38 | this.priorityGroups = priorityGroups; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/lbmq/AbstractPollable.java: -------------------------------------------------------------------------------- 1 | package lbmq; 2 | 3 | import java.util.NoSuchElementException; 4 | import java.util.Queue; 5 | 6 | /** 7 | * This class provides skeletal implementations of some {@link Pollable} operations. The 8 | * implementations in this class are appropriate when the base implementation does not 9 | * allow null elements. Methods {@link #remove remove}, and {@link #element element} 10 | * are based on {@link #poll poll}, and {@link #peek peek}, respectively, but throw exceptions 11 | * instead of indicating failure via false or null returns. 12 | * 13 | *

A Pollable implementation that extends this class must minimally define methods 14 | * {@link Queue#peek} and {@link Queue#poll}. Typically, additional methods will be overridden as 15 | * well. 16 | * 17 | * @param the type of elements held in this collection 18 | */ 19 | public abstract class AbstractPollable implements Pollable { 20 | 21 | /** 22 | * Retrieves and removes the head of this queue. This method differs from {@link #poll poll} only 23 | * in that it throws an exception if this queue is empty. 24 | * 25 | *

This implementation returns the result of poll unless the queue is empty. 26 | * 27 | * @return the head of this queue 28 | * @throws NoSuchElementException if this queue is empty 29 | */ 30 | public E remove() { 31 | E x = poll(); 32 | if (x == null) { 33 | throw new NoSuchElementException(); 34 | } 35 | return x; 36 | } 37 | 38 | /** 39 | * Retrieves, but does not remove, the head of this queue. This method differs from {@link #peek 40 | * peek} only in that it throws an exception if this queue is empty. 41 | * 42 | *

This implementation returns the result of peek unless the queue is empty. 43 | * 44 | * @return the head of this queue 45 | * @throws NoSuchElementException if this queue is empty 46 | */ 47 | public E element() { 48 | E x = peek(); 49 | if (x == null) { 50 | throw new NoSuchElementException(); 51 | } 52 | return x; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/lbmq/Benchmark.java: -------------------------------------------------------------------------------- 1 | package lbmq; 2 | 3 | import java.time.Duration; 4 | 5 | public class Benchmark { 6 | 7 | static boolean warmup = true; 8 | 9 | static boolean testAlt = true; 10 | static boolean testLbmq = true; 11 | 12 | static int totalQueueCapacity = 1_000_000; 13 | 14 | static int warmupSize = 50_000_000; 15 | static int warmupQueues = 1; 16 | static int warmupWritersPerQueue = 1; 17 | static int warmupReaders = 1; 18 | 19 | static int benchmarkSize = 20_000_000; 20 | static int queues = 5; 21 | static int writersPerQueue = 50; 22 | static int readers = 1; 23 | 24 | public static void main(String[] args) { 25 | if (warmup) { 26 | System.out.println("Warm up"); 27 | if (testAlt) { 28 | BenchmarkLoopAlt warmupAlt = new BenchmarkLoopAlt( 29 | warmupSize, totalQueueCapacity, warmupQueues, warmupWritersPerQueue, warmupReaders); 30 | warmupAlt.start(); 31 | } 32 | if (testLbmq) { 33 | BenchmarkLoopLbmq warmupLbmq = new BenchmarkLoopLbmq( 34 | warmupSize, totalQueueCapacity, warmupQueues, warmupWritersPerQueue, warmupReaders); 35 | warmupLbmq.start(); 36 | } 37 | } 38 | // test 39 | System.out.println("Benchmark"); 40 | Duration timeAlt = null; 41 | Duration timeLbmq = null; 42 | if (testAlt) { 43 | BenchmarkLoopAlt testAlt = 44 | new BenchmarkLoopAlt(benchmarkSize, totalQueueCapacity, queues, writersPerQueue, readers); 45 | timeAlt = testAlt.start(); 46 | } 47 | if (testLbmq) { 48 | BenchmarkLoopLbmq testLbmq = 49 | new BenchmarkLoopLbmq(benchmarkSize, totalQueueCapacity, queues, writersPerQueue, readers); 50 | timeLbmq = testLbmq.start(); 51 | } 52 | if (testAlt) { 53 | System.out.printf("Time Alternative: %s\n", timeAlt); 54 | } 55 | if (testLbmq) { 56 | System.out.printf("Time LBMQ: %s\n", timeLbmq); 57 | } 58 | } 59 | 60 | static void join(Thread thread) { 61 | try { 62 | thread.join(); 63 | } catch (InterruptedException e) { 64 | throw new RuntimeException(e); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /src/main/java/lbmq/AbstractOfferable.java: -------------------------------------------------------------------------------- 1 | package lbmq; 2 | 3 | import java.util.AbstractCollection; 4 | import java.util.Collection; 5 | import java.util.Queue; 6 | 7 | /** 8 | * This class provides skeletal implementations of some {@link Offerable} operations. The 9 | * implementations in this class are appropriate when the base implementation does not 10 | * allow null elements. Method {@link #add add} is based on {@link #offer offer}, but 11 | * throws exceptions instead of indicating failure via false or null 12 | * returns. 13 | * 14 | *

An Offerable implementation that extends this class must minimally define a 15 | * method {@link Queue#offer} which does not permit insertion of null elements, along 16 | * with methods {@link Collection#size}, and {@link Collection#iterator}. Typically, additional 17 | * methods will be overridden as well. If these requirements cannot be met, consider instead 18 | * subclassing {@link AbstractCollection}. 19 | * 20 | * @param the type of elements held in this collection 21 | */ 22 | public abstract class AbstractOfferable extends AbstractCollection implements Offerable { 23 | 24 | public boolean add(E e) { 25 | if (!offer(e)) { 26 | throw new IllegalStateException("Queue full"); 27 | } 28 | return true; 29 | } 30 | 31 | /** 32 | * Adds all the elements in the specified collection to this queue. Attempts to addAll of a 33 | * queue to itself result in IllegalArgumentException. Further, the behavior of this 34 | * operation is undefined if the specified collection is modified while the operation is in 35 | * progress. 36 | * 37 | *

This implementation iterates over the specified collection, and adds each element returned 38 | * by the iterator to this queue, in turn. A runtime exception encountered while trying to add an 39 | * element (including, in particular, a null element) may result in only some of the 40 | * elements having been successfully added when the associated exception is thrown. 41 | * 42 | * @param c collection containing elements to be added to this queue 43 | * @return true if this queue changed as a result of the call 44 | * @throws ClassCastException if the class of an element of the specified collection prevents it 45 | * from being added to this queue 46 | * @throws NullPointerException if the specified collection contains a null element and this queue 47 | * does not permit null elements, or if the specified collection is null 48 | * @throws IllegalArgumentException if some property of an element of the specified collection 49 | * prevents it from being added to this queue, or if the specified collection is this queue 50 | * @throws IllegalStateException if not all the elements can be added at this time due to 51 | * insertion restrictions 52 | * @see #add(Object) 53 | */ 54 | public boolean addAll(Collection c) { 55 | if (c == null) { 56 | throw new NullPointerException(); 57 | } 58 | if (c == this) { 59 | throw new IllegalArgumentException(); 60 | } 61 | boolean modified = false; 62 | for (E e : c) { 63 | if (add(e)) { 64 | modified = true; 65 | } 66 | } 67 | return modified; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/lbmq/BenchmarkLoopLbmq.java: -------------------------------------------------------------------------------- 1 | package lbmq; 2 | 3 | import java.time.Duration; 4 | import java.util.List; 5 | import java.util.concurrent.atomic.LongAdder; 6 | import java.util.stream.Collectors; 7 | import java.util.stream.IntStream; 8 | import java.util.stream.Stream; 9 | 10 | class BenchmarkLoopLbmq { 11 | 12 | private final long benchmarkSize; 13 | private final int writerThreads; 14 | private final int readerThreads; 15 | private final int queues; 16 | 17 | private final Object element = new Object(); 18 | private final int individualQueueCapacity; 19 | 20 | private final LongAdder readElements = new LongAdder(); 21 | 22 | private List readers = null; 23 | private List writers = null; 24 | 25 | public BenchmarkLoopLbmq( 26 | int benchmarkSize, int totalQueueCapacity, int queues, int writerThreads, int readerThreads) { 27 | this.benchmarkSize = benchmarkSize; 28 | this.writerThreads = writerThreads; 29 | this.readerThreads = readerThreads; 30 | this.queues = queues; 31 | this.individualQueueCapacity = (totalQueueCapacity + queues - 1) / queues; 32 | System.out.println("Size per queue: " + individualQueueCapacity); 33 | } 34 | 35 | Duration start() { 36 | LinkedBlockingMultiQueue queue = new LinkedBlockingMultiQueue<>(); 37 | Stream.SubQueue> subQueues = IntStream.range(0, queues) 38 | .mapToObj(i -> { 39 | queue.addSubQueue(i, 1, individualQueueCapacity); 40 | return queue.getSubQueue(i); 41 | }); 42 | writers = subQueues 43 | .flatMap(subQueue -> IntStream.range(0, writerThreads) 44 | .mapToObj(i -> new Thread(() -> writer(subQueue), String.format("writer-%d", i)))) 45 | .collect(Collectors.toList()); 46 | readers = IntStream.range(0, readerThreads) 47 | .mapToObj(i -> new Thread(() -> reader(queue), String.format("reader-%d", i))) 48 | .collect(Collectors.toList()); 49 | long start = System.nanoTime(); 50 | writers.forEach(w -> w.start()); 51 | readers.forEach(w -> w.start()); 52 | writers.forEach(w -> Benchmark.join(w)); 53 | readers.forEach(w -> Benchmark.join(w)); 54 | return Duration.ofNanos(System.nanoTime() - start); 55 | } 56 | 57 | private void writer(LinkedBlockingMultiQueue.SubQueue offerable) { 58 | try { 59 | for (int i = 0; ; i++) { 60 | offerable.put(element); 61 | if (i % 5_000_000 == 0) { 62 | System.out.printf( 63 | "[%s] Wrote %d elements. Queue size: %d\n", 64 | Thread.currentThread().getName(), i, offerable.size()); 65 | } 66 | } 67 | } catch (InterruptedException e) { 68 | // ok 69 | } 70 | } 71 | 72 | private void reader(Pollable pollable) { 73 | try { 74 | for (int i = 0; ; i++) { 75 | Object element = pollable.take(); 76 | if (i % 5_000_000 == 0) { 77 | System.out.printf( 78 | "[%s] Read %d elements.\n", Thread.currentThread().getName(), i); 79 | } 80 | readElements.increment(); 81 | if (readElements.longValue() >= benchmarkSize) { 82 | interruptOthers(); 83 | } 84 | } 85 | } catch (InterruptedException e) { 86 | // ok 87 | } 88 | } 89 | 90 | private void interruptOthers() { 91 | readers.forEach(t -> t.interrupt()); 92 | writers.forEach(t -> t.interrupt()); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/lbmq/BenchmarkLoopAlt.java: -------------------------------------------------------------------------------- 1 | package lbmq; 2 | 3 | import java.time.Duration; 4 | import java.util.List; 5 | import java.util.concurrent.BlockingQueue; 6 | import java.util.concurrent.LinkedBlockingQueue; 7 | import java.util.concurrent.atomic.LongAdder; 8 | import java.util.stream.Collectors; 9 | import java.util.stream.IntStream; 10 | import java.util.stream.Stream; 11 | 12 | class BenchmarkLoopAlt { 13 | 14 | private final long benchmarkSize; 15 | private final int writerThreadsPerQueue; 16 | private final int readerThreads; 17 | private final int queues; 18 | 19 | private final Object element = new Object(); 20 | private final int individualQueueCapacity; 21 | 22 | private final LongAdder readElements = new LongAdder(); 23 | 24 | private List readers = null; 25 | private List writers = null; 26 | 27 | BenchmarkLoopAlt( 28 | int benchmarkSize, int totalQueueCapacity, int queues, int writerThreadsPerQueue, int readerThreads) { 29 | this.queues = queues; 30 | this.benchmarkSize = benchmarkSize; 31 | this.writerThreadsPerQueue = writerThreadsPerQueue; 32 | this.readerThreads = readerThreads; 33 | int divisor = queues + 1; 34 | this.individualQueueCapacity = (totalQueueCapacity + divisor - 1) / divisor; 35 | System.out.println("Size per queue: " + individualQueueCapacity); 36 | } 37 | 38 | Duration start() { 39 | LinkedBlockingQueue> notifier = new LinkedBlockingQueue<>(individualQueueCapacity); 40 | Stream> subQueues = 41 | IntStream.range(0, queues).mapToObj(i -> new LinkedBlockingQueue<>(individualQueueCapacity)); 42 | writers = subQueues 43 | .flatMap(subQueue -> IntStream.range(0, writerThreadsPerQueue) 44 | .mapToObj(i -> new Thread(() -> writer(subQueue, notifier), String.format("writer-%d", i)))) 45 | .collect(Collectors.toList()); 46 | readers = IntStream.range(0, readerThreads) 47 | .mapToObj(i -> new Thread(() -> reader(notifier), String.format("reader-%d", i))) 48 | .collect(Collectors.toList()); 49 | long start = System.nanoTime(); 50 | writers.forEach(w -> w.start()); 51 | readers.forEach(w -> w.start()); 52 | writers.forEach(w -> Benchmark.join(w)); 53 | readers.forEach(w -> Benchmark.join(w)); 54 | return Duration.ofNanos(System.nanoTime() - start); 55 | } 56 | 57 | private void writer(BlockingQueue offerable, LinkedBlockingQueue> notifier) { 58 | try { 59 | for (int i = 0; ; i++) { 60 | offerable.put(element); 61 | notifier.put(offerable); 62 | if (i % 5_000_000 == 0) { 63 | System.out.printf( 64 | "[%s] Wrote %d elements. Queue size: %d\n", 65 | Thread.currentThread().getName(), i, offerable.size()); 66 | } 67 | } 68 | } catch (InterruptedException e) { 69 | // ok 70 | } 71 | } 72 | 73 | private void reader(BlockingQueue> notifier) { 74 | int queueSize; 75 | try { 76 | for (int i = 0; ; i++) { 77 | BlockingQueue queue = notifier.take(); 78 | queue.take(); 79 | queueSize = queue.size(); 80 | if (i % 5_000_000 == 0) { 81 | System.out.printf( 82 | "[%s] Read %d elements. Queue size: %d\n", 83 | Thread.currentThread().getName(), i, queueSize); 84 | } 85 | readElements.increment(); 86 | if (readElements.longValue() >= benchmarkSize) { 87 | interruptOthers(); 88 | } 89 | } 90 | } catch (InterruptedException e) { 91 | // ok 92 | } 93 | } 94 | 95 | private void interruptOthers() { 96 | readers.forEach(t -> t.interrupt()); 97 | writers.forEach(t -> t.interrupt()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/lbmq/Offerable.java: -------------------------------------------------------------------------------- 1 | package lbmq; 2 | 3 | import java.util.Collection; 4 | import java.util.concurrent.BlockingQueue; 5 | import java.util.concurrent.TimeUnit; 6 | 7 | /** This trait captures the "tail side" of the {@link BlockingQueue} interface. */ 8 | public interface Offerable { 9 | 10 | /** 11 | * Inserts the specified element into this queue if it is possible to do so immediately without 12 | * violating capacity restrictions, returning {@code true} upon success and throwing an {@link 13 | * IllegalStateException} if no space is currently available. 14 | * 15 | * @param e the element to add 16 | * @return {@code true} (as specified by {@link Collection#add}) 17 | * @throws IllegalStateException if the element cannot be added at this time due to capacity 18 | * restrictions 19 | * @throws ClassCastException if the class of the specified element prevents it from being added 20 | * to this queue 21 | * @throws NullPointerException if the specified element is null and this queue does not permit 22 | * null elements 23 | * @throws IllegalArgumentException if some property of this element prevents it from being added 24 | * to this queue 25 | */ 26 | boolean add(E e); 27 | 28 | /** 29 | * Inserts the specified element into this queue if it is possible to do so immediately without 30 | * violating capacity restrictions. When using a capacity-restricted queue, this method is 31 | * generally preferable to {@link #add}, which can fail to insert an element only by throwing an 32 | * exception. 33 | * 34 | * @param e the element to add 35 | * @return {@code true} if the element was added to this queue, else {@code false} 36 | * @throws ClassCastException if the class of the specified element prevents it from being added 37 | * to this queue 38 | * @throws NullPointerException if the specified element is null and this queue does not permit 39 | * null elements 40 | * @throws IllegalArgumentException if some property of this element prevents it from being added 41 | * to this queue 42 | */ 43 | boolean offer(E e); 44 | 45 | /** 46 | * Inserts the specified element into this queue, waiting if necessary for space to become 47 | * available. 48 | * 49 | * @param e the element to add 50 | * @throws InterruptedException if interrupted while waiting 51 | * @throws ClassCastException if the class of the specified element prevents it from being added 52 | * to this queue 53 | * @throws NullPointerException if the specified element is null 54 | * @throws IllegalArgumentException if some property of the specified element prevents it from 55 | * being added to this queue 56 | */ 57 | void put(E e) throws InterruptedException; 58 | 59 | /** 60 | * Inserts the specified element into this queue, waiting up to the specified wait time if 61 | * necessary for space to become available. 62 | * 63 | * @param e the element to add 64 | * @param timeout how long to wait before giving up, in units of {@code unit} 65 | * @param unit a {@link TimeUnit} determining how to interpret the {@code timeout} parameter 66 | * @return {@code true} if successful, or {@code false} if the specified waiting time elapses 67 | * before space is available 68 | * @throws InterruptedException if interrupted while waiting 69 | * @throws ClassCastException if the class of the specified element prevents it from being added 70 | * to this queue 71 | * @throws NullPointerException if the specified element is null 72 | * @throws IllegalArgumentException if some property of the specified element prevents it from 73 | * being added to this queue 74 | */ 75 | boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; 76 | 77 | /** 78 | * Returns the number of additional elements that this queue can ideally (in the absence of memory 79 | * or resource constraints) accept without blocking, or {@code Integer.MAX_VALUE} if there is no 80 | * intrinsic limit. 81 | * 82 | *

Note that you cannot always tell if an attempt to insert an element will succeed by 83 | * inspecting {@code remainingCapacity} because it may be the case that another thread is about to 84 | * insert or remove an element. 85 | * 86 | * @return the remaining capacity 87 | */ 88 | int remainingCapacity(); 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/lbmq/Pollable.java: -------------------------------------------------------------------------------- 1 | package lbmq; 2 | 3 | import java.util.Collection; 4 | import java.util.NoSuchElementException; 5 | import java.util.concurrent.BlockingQueue; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | /** This interface captures the "head side" of the {@link BlockingQueue} interface */ 9 | public interface Pollable { 10 | 11 | /** 12 | * Retrieves and removes the head of this queue. This method differs from {@link #poll poll} only 13 | * in that it throws an exception if this queue is empty. 14 | * 15 | * @return the head of this queue 16 | * @throws NoSuchElementException if this queue is empty 17 | */ 18 | E remove(); 19 | 20 | /** 21 | * Retrieves and removes the head of this queue, or returns {@code null} if this queue is empty. 22 | * 23 | * @return the head of this queue, or {@code null} if this queue is empty 24 | */ 25 | E poll(); 26 | 27 | /** 28 | * Retrieves, but does not remove, the head of this queue. This method differs from {@link #peek 29 | * peek} only in that it throws an exception if this queue is empty. 30 | * 31 | * @return the head of this queue 32 | * @throws NoSuchElementException if this queue is empty 33 | */ 34 | E element(); 35 | 36 | /** 37 | * Retrieves, but does not remove, the head of this queue, or returns {@code null} if this queue 38 | * is empty. 39 | * 40 | * @return the head of this queue, or {@code null} if this queue is empty 41 | */ 42 | E peek(); 43 | 44 | /** 45 | * Retrieves and removes the head of this queue, waiting if necessary until an element becomes 46 | * available. 47 | * 48 | * @return the head of this queue 49 | * @throws InterruptedException if interrupted while waiting 50 | */ 51 | E take() throws InterruptedException; 52 | 53 | /** 54 | * Retrieves and removes the head of this queue, waiting up to the specified wait time if 55 | * necessary for an element to become available. 56 | * 57 | * @param timeout how long to wait before giving up, in units of {@code unit} 58 | * @param unit a {@code TimeUnit} determining how to interpret the {@code timeout} parameter 59 | * @return the head of this queue, or {@code null} if the specified waiting time elapses before an 60 | * element is available 61 | * @throws InterruptedException if interrupted while waiting 62 | */ 63 | E poll(long timeout, TimeUnit unit) throws InterruptedException; 64 | 65 | /** 66 | * Removes all available elements from this queue and adds them to the given collection. This 67 | * operation may be more efficient than repeatedly polling this queue. A failure encountered while 68 | * attempting to add elements to collection {@code c} may result in elements being in neither, 69 | * either or both collections when the associated exception is thrown. Attempts to drain a queue 70 | * to itself result in {@link IllegalArgumentException}. Further, the behavior of this operation 71 | * is undefined if the specified collection is modified while the operation is in progress. 72 | * 73 | * @param c the collection to transfer elements into 74 | * @return the number of elements transferred 75 | * @throws UnsupportedOperationException if addition of elements is not supported by the specified 76 | * collection 77 | * @throws ClassCastException if the class of an element of this queue prevents it from being 78 | * added to the specified collection 79 | * @throws NullPointerException if the specified collection is null 80 | * @throws IllegalArgumentException if the specified collection is this queue, or some property of 81 | * an element of this queue prevents it from being added to the specified collection 82 | */ 83 | int drainTo(Collection c); 84 | 85 | /** 86 | * Removes at most the given number of available elements from this queue and adds them to the 87 | * given collection. A failure encountered while attempting to add elements to collection {@code 88 | * c} may result in elements being in neither, either or both collections when the associated 89 | * exception is thrown. Attempts to drain a queue to itself result in {@link 90 | * IllegalArgumentException}. Further, the behavior of this operation is undefined if the 91 | * specified collection is modified while the operation is in progress. 92 | * 93 | * @param c the collection to transfer elements into 94 | * @param maxElements the maximum number of elements to transfer 95 | * @return the number of elements transferred 96 | * @throws UnsupportedOperationException if addition of elements is not supported by the specified 97 | * collection 98 | * @throws ClassCastException if the class of an element of this queue prevents it from being 99 | * added to the specified collection 100 | * @throws NullPointerException if the specified collection is null 101 | * @throws IllegalArgumentException if the specified collection is this queue, or some property of 102 | * an element of this queue prevents it from being added to the specified collection 103 | */ 104 | int drainTo(Collection c, int maxElements); 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Linked Blocking Multi Queue 2 | 3 | _Linked Blocking Multi Queue_ is a concurrent collection that extends the existing [Java concurrent collection library](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html), offering an optionally-bounded blocking "multi-queue" based on linked nodes. That is, essentially, a data structure with several tails but one head, that allows a reader, crucially, to block on more than one queue. 4 | 5 | [![Build Status](https://github.com/marianobarrios/linked-blocking-multi-queue/actions/workflows/main.yml/badge.svg)](https://github.com/marianobarrios/linked-blocking-multi-queue/actions) 6 | 7 | [![Maven Central](https://img.shields.io/maven-central/v/com.github.marianobarrios/lbmq.svg?label=Maven%20Central)](https://central.sonatype.com/search?namespace=com.github.marianobarrios&name=lbmq&sort=name) 8 | [![javadoc](https://javadoc.io/badge2/com.github.marianobarrios/lbmq/javadoc.svg)](https://javadoc.io/doc/com.github.marianobarrios/linked-blocking-multi-queue) 9 | 10 | ## Rationale 11 | 12 | A notorious limitation of Java blocking primitives is that a given thread can only block on one synchronizing object at a time. Blocking on several resources is a generally useful technique, already available in selectors (for channels) in Java. It is also common in other languages. This library offers a collection that can be used when a queue consumer (or consumers) needs to block on several queues. 13 | 14 | Features: 15 | 16 | - Priorities for different sub-queues 17 | - Customizable priority evaluation (by default, fair (round-robin) selection of elements among same-priority sub-queues). 18 | - Mid-flight addition and removal of sub-queues. 19 | - Mid-flight change of sub-queue status (enabled/disabled). 20 | 21 | ## Use case 22 | 23 | As mentioned, this class essentially allows a consumer to efficiently block a single thread on a set of queues, until one becomes available. 24 | 25 | Multiple queues (instead of just one collecting everything) are usually necessary when: 26 | 27 | - Not all elements need the same capacity limit. 28 | - Not all elements have the same priority. 29 | - Among the same priority, round-robin (fair) consumption is desired (avoiding that prolific producers starve occasional ones). 30 | - Some subset of enqueued elements may need to be discarded or suspended, while keeping the rest. 31 | 32 | ## Example 33 | 34 | The multi-queue has a no-argument constructor. The class as two type arguments. The first one is the type of the queue key, that is, the type of the values used as keys to identify each sub-queue. The second is the element type. Sub-queues are created from it in a second step: 35 | 36 | ```java 37 | LinkedBlockingMultiQueue q = new LinkedBlockingMultiQueue<>(); 38 | q.addSubQueue(1 /* key */, 10 /* priority */); 39 | q.addSubQueue(2 /* key */, 10 /* priority */, 10000 /* capacity */); 40 | LinkedBlockingMultiQueue.SubQueue sq1 = q.getSubQueue(1); 41 | LinkedBlockingMultiQueue.SubQueue sq2 = q.getSubQueue(2); 42 | ``` 43 | 44 | Then it is possible to offer and poll: 45 | 46 | ```java 47 | sq1.offer("x1"); 48 | q.poll(); // "x1" 49 | sq2.offer("x2"); 50 | q.poll(); // "x2" 51 | ``` 52 | 53 | ## Features 54 | 55 | _Linked Blocking Multi Queue_ is an optionally-bounded blocking "multi-queue" based on linked nodes, defining multi-queue as a set of queues that are connected at the heads and have independent tails (the head of the queue is that element that has been on the queue the longest time, the tail of the queue is that element that has been on the queue the shortest time). New elements are added at the tail of one of the queues, and the queue retrieval operations obtain elements from the head of some of the queues, according to a policy that is described below. 56 | 57 | The factory method for sub-queues has an optional capacity argument, as a way to prevent excessive queue expansion. The capacity, if unspecified, is equal to `Integer.MAX_VALUE`. Linked nodes are dynamically created upon each insertion unless this would bring the queue above capacity. 58 | 59 | ### Priorities 60 | 61 | Sub-queues can have different priorities, meaning that elements from higher priority queues will be offered first to consumers. Inside the same priority queues are drained round-robin. 62 | 63 | ### Enabling, disabling, adding and removing queues 64 | `` 65 | A special feature is that individual queues can be enabled or disabled. A disabled queue is not considered for polling (in the event that all the queue are disabled, any blocking operation would do so trying to read, as if all the queues were empty). Elements are taken from the set of enabled queues ()obeying the established priority). 66 | 67 | A disabled queue accepts new elements normally until it reaches the maximum capacity (if any). 68 | 69 | Individual queues can be enabled or disabled (and also added or removed) at any time. 70 | 71 | ## Compatibility 72 | 73 | Not being actually a linear queue, this class does not implement the `Collection` or `Queue` interfaces. The traditional queue interface is split in the traits: `Offerable` and `Pollable`. Sub-queues do however implement Collection. 74 | 75 | ## Implementation notes 76 | 77 | This implementation is inspired by the 78 | [LinkedBlockingQueue](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/LinkedBlockingQueue.html), made by Doug Lea with assistance from members of [JCP JSR-166 Expert Group](https://jcp.org/en/jsr/detail?id=166). 79 | 80 | Each sub-queue uses, as does the `LinkedBlockingQueue`, a variant of the "two lock queue" algorithm. The `putLock` gates entry to `put` (and `offer`), and has an associated condition for waiting puts. The `takeLock`, on the other hand, is unique and shared among all the sub-queues. 81 | 82 | Each subqueue has a "count" field, that is maintained as an atomic to avoid needing to get both locks in most cases. Also, to minimize need for puts to get takeLock and vice-versa, cascading notifies are used. When a put notices that it has enabled at least one take, it signals taker. That taker in turn signals others if more items have been entered since the signal. And symmetrically for takes signaling puts. 83 | 84 | The possibility of disabling sub-queues introduces the necessity of an additional centralized atomic count field, which is also updated in every operation and represents, at any time, how many elements can be taken before exhausting the queue. 85 | 86 | Operations such as `remove(Object)` and iterators acquire both the corresponding putLock and the takeLock. 87 | 88 | Visibility between writers and readers is provided as follows: 89 | 90 | Whenever an element is enqueued, the `putLock` is acquired and count updated. A subsequent reader guarantees visibility to the enqueued Node by either acquiring the `putLock` (via `fullyLock`) or by acquiring the `takeLock`, and then reading 91 | `n = count.get()`; this gives visibility to the first `n` items. 92 | 93 | To implement weakly consistent iterators, it appears we need to keep all Nodes GC-reachable from a predecessor dequeued Node. That would cause two problems: 94 | 95 | - Allow a rogue Iterator to cause unbounded memory retention 96 | 97 | - Cause cross-generational linking of old Nodes to new Nodes if a Node was tenured while live, which generational garbage collectors have a hard time dealing with, causing repeated major collections. However, only non-deleted Nodes need to be reachable from dequeued Nodes, and reachability does not necessarily have to be of the kind understood by the garbage collector. We use the trick of linking a Node that has just been dequeued to itself. Such a self-link implicitly means to advance to 98 | `head.next`. 99 | 100 | ### Requirements 101 | 102 | Linked Blocking Multi Queue requires Java 8 or newer. 103 | 104 | ### Size and Dependencies 105 | 106 | The library has no dependencies. The main jar file size is below 20 KB. 107 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /src/test/java/lbmq/TestCase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Derived from work made by Doug Lea with assistance from members of JCP JSR-166 Expert Group 3 | * (https://jcp.org/en/jsr/detail?id=166). The original work is in the public domain, as explained at 4 | * http://creativecommons.org/publicdomain/zero/1.0/ 5 | */ 6 | package lbmq; 7 | 8 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 9 | import static java.util.concurrent.TimeUnit.NANOSECONDS; 10 | import static org.junit.jupiter.api.Assertions.*; 11 | 12 | import java.util.Iterator; 13 | import java.util.NoSuchElementException; 14 | import java.util.concurrent.CountDownLatch; 15 | import java.util.concurrent.CyclicBarrier; 16 | import java.util.concurrent.ExecutorService; 17 | import java.util.concurrent.TimeoutException; 18 | import java.util.concurrent.atomic.AtomicReference; 19 | import org.junit.jupiter.api.AfterAll; 20 | import org.opentest4j.AssertionFailedError; 21 | 22 | /** 23 | * Base class for the LinkedBlockingMultiQueue tests. Defines some constants, utility methods and 24 | * classes, as well as a simple framework for helping to make sure that assertions failing in 25 | * generated threads cause the associated test that generated them to itself fail (which JUnit does 26 | * not otherwise arrange). The rules for creating such tests are: 27 | * 28 | *

    29 | *
  1. All delays and timeouts must use one of the constants {@code SHORT_DELAY_MS}, {@code 30 | * SMALL_DELAY_MS}, {@code MEDIUM_DELAY_MS}, {@code LONG_DELAY_MS}. The idea here is that a 31 | * SHORT is always discriminable from zero time, and always allows enough time for the small 32 | * amounts of computation (creating a thread, calling a few methods, etc) needed to reach a 33 | * timeout point. Similarly, a SMALL is always discriminable as larger than SHORT and smaller 34 | * than MEDIUM. And so on. These constants are set to conservative values, but even so, if 35 | * there is ever any doubt, they can all be increased in one spot to rerun tests on slower 36 | * platforms. 37 | *
  2. All threads generated must be joined inside each test case method (or {@code fail} to do 38 | * so) before returning from the method. The {@code joinPool} method can be used to do this 39 | * when using Executors. 40 | *
41 | * 42 | *

Other notes 43 | * 44 | *

    45 | *
  • These tests are "conformance tests", and do not attempt to test throughput, latency, 46 | * scalability or other performance factors (see the separate "jtreg" tests for a set intended 47 | * to check these for the most central aspects of functionality.) So, most tests use the 48 | * smallest sensible numbers of threads, collection sizes, etc needed to check basic 49 | * conformance. 50 | *
51 | */ 52 | public class TestCase { 53 | 54 | // Delays for timing-dependent tests, in milliseconds. 55 | 56 | public static long SHORT_DELAY_MS = 50; 57 | public static long SMALL_DELAY_MS = SHORT_DELAY_MS * 5; 58 | public static long MEDIUM_DELAY_MS = SHORT_DELAY_MS * 10; 59 | public static long LONG_DELAY_MS = SHORT_DELAY_MS * 200; 60 | 61 | /** 62 | * Returns a timeout in milliseconds to be used in tests that verify that operations block or time 63 | * out. 64 | */ 65 | long timeoutMillis() { 66 | return SHORT_DELAY_MS / 4; 67 | } 68 | 69 | /** The first exception encountered if any threadAssertXXX method fails. */ 70 | private static final AtomicReference threadFailure = new AtomicReference<>(null); 71 | 72 | /** 73 | * Records an exception so that it can be rethrown later in the test harness thread, triggering a 74 | * test case failure. Only the first failure is recorded; subsequent calls to this method from 75 | * within the same test have no effect. 76 | */ 77 | public void threadRecordFailure(Throwable t) { 78 | threadFailure.compareAndSet(null, t); 79 | } 80 | 81 | /** 82 | * Extra checks that get done for all test cases. 83 | * 84 | *

Triggers test case failure if any thread assertions have failed, by rethrowing, in the test 85 | * harness thread, any exception recorded earlier by threadRecordFailure. 86 | * 87 | *

Triggers test case failure if interrupt status is set in the main thread. 88 | */ 89 | @AfterAll 90 | public static void tearDown() throws Exception { 91 | Throwable t = threadFailure.getAndSet(null); 92 | if (t != null) { 93 | if (t instanceof Error) { 94 | throw (Error) t; 95 | } else if (t instanceof RuntimeException) { 96 | throw (RuntimeException) t; 97 | } else if (t instanceof Exception) { 98 | throw (Exception) t; 99 | } else { 100 | throw new AssertionFailedError(t.toString(), t); 101 | } 102 | } 103 | if (Thread.interrupted()) { 104 | throw new AssertionFailedError("interrupt status set in main thread"); 105 | } 106 | } 107 | 108 | /** 109 | * Just like assertTrue(b), but additionally recording (using threadRecordFailure) any 110 | * AssertionFailedError thrown, so that the current testcase will fail. 111 | */ 112 | public void threadAssertTrue(boolean b) { 113 | try { 114 | assertTrue(b); 115 | } catch (AssertionFailedError t) { 116 | threadRecordFailure(t); 117 | throw t; 118 | } 119 | } 120 | 121 | /** 122 | * Records the given exception using {@link #threadRecordFailure}, then rethrows the exception, 123 | * wrapping it in an AssertionFailedError if necessary. 124 | */ 125 | public void threadUnexpectedException(Throwable t) { 126 | threadRecordFailure(t); 127 | t.printStackTrace(); 128 | if (t instanceof RuntimeException) { 129 | throw (RuntimeException) t; 130 | } else if (t instanceof Error) { 131 | throw (Error) t; 132 | } else { 133 | throw new AssertionFailedError("unexpected exception: " + t, t); 134 | } 135 | } 136 | 137 | /** 138 | * Delays, via Thread.sleep, for the given millisecond delay, but if the sleep is shorter than 139 | * specified, may re-sleep or yield until time elapses. 140 | */ 141 | static void delay(long millis) throws InterruptedException { 142 | long startTime = System.nanoTime(); 143 | long ns = millis * 1000 * 1000; 144 | for (; ; ) { 145 | if (millis > 0L) { 146 | Thread.sleep(millis); 147 | } else { 148 | // too short to sleep 149 | Thread.yield(); 150 | } 151 | long d = ns - (System.nanoTime() - startTime); 152 | if (d > 0L) { 153 | millis = d / (1000 * 1000); 154 | } else break; 155 | } 156 | } 157 | 158 | /** Waits out termination of a thread pool or fails doing so. */ 159 | void joinPool(ExecutorService exec) { 160 | try { 161 | exec.shutdown(); 162 | if (!exec.awaitTermination(2 * LONG_DELAY_MS, MILLISECONDS)) { 163 | fail("ExecutorService " + exec + " did not terminate in a timely manner"); 164 | } 165 | } catch (SecurityException ok) { 166 | // Allowed in case test doesn't have privileges 167 | } catch (InterruptedException fail) { 168 | fail("Unexpected InterruptedException"); 169 | } 170 | } 171 | 172 | /** 173 | * Checks that thread does not terminate within the default millisecond delay of {@code 174 | * timeoutMillis()}. 175 | */ 176 | void assertThreadStaysAlive(Thread thread) { 177 | assertThreadStaysAlive(thread, timeoutMillis()); 178 | } 179 | 180 | /** Checks that thread does not terminate within the given millisecond delay. */ 181 | void assertThreadStaysAlive(Thread thread, long millis) { 182 | try { 183 | // No need to optimize the failing case via Thread.join. 184 | delay(millis); 185 | assertTrue(thread.isAlive()); 186 | } catch (InterruptedException fail) { 187 | fail("Unexpected InterruptedException"); 188 | } 189 | } 190 | 191 | /** Fails with message "should throw exception". */ 192 | public void shouldThrow() { 193 | fail("Should throw exception"); 194 | } 195 | 196 | /** The number of elements to place in collections, arrays, etc. */ 197 | public static final int SIZE = 20; 198 | 199 | // Some convenient Integer constants 200 | 201 | public static final Integer zero = 0; 202 | public static final Integer one = 1; 203 | public static final Integer two = 2; 204 | public static final Integer three = 3; 205 | public static final Integer four = 4; 206 | public static final Integer five = 5; 207 | public static final Integer six = 6; 208 | public static final Integer seven = 7; 209 | public static final Integer eight = 8; 210 | public static final Integer nine = 9; 211 | 212 | /** 213 | * Spin-waits up to the specified number of milliseconds for the given thread to enter a wait 214 | * state: BLOCKED, WAITING, or TIMED_WAITING. 215 | */ 216 | void waitForThreadToEnterWaitState(Thread thread, long timeoutMillis) { 217 | long startTime = System.nanoTime(); 218 | for (; ; ) { 219 | Thread.State s = thread.getState(); 220 | if (s == Thread.State.BLOCKED || s == Thread.State.WAITING || s == Thread.State.TIMED_WAITING) { 221 | return; 222 | } else if (s == Thread.State.TERMINATED) { 223 | fail("Unexpected thread termination"); 224 | } else if (millisElapsedSince(startTime) > timeoutMillis) { 225 | threadAssertTrue(thread.isAlive()); 226 | return; 227 | } 228 | Thread.yield(); 229 | } 230 | } 231 | 232 | /** 233 | * Returns the number of milliseconds since time given by startNanoTime, which must have been 234 | * previously returned from a call to {@link System#nanoTime()}. 235 | */ 236 | public static long millisElapsedSince(long startNanoTime) { 237 | return NANOSECONDS.toMillis(System.nanoTime() - startNanoTime); 238 | } 239 | 240 | /** Returns a new started daemon Thread running the given runnable. */ 241 | public Thread newStartedThread(Runnable runnable) { 242 | Thread t = new Thread(runnable); 243 | t.setDaemon(true); 244 | t.start(); 245 | return t; 246 | } 247 | 248 | /** 249 | * Waits for the specified time (in milliseconds) for the thread to terminate (using {@link 250 | * Thread#join(long)}), else interrupts the thread (in the hope that it may terminate later) and 251 | * fails. 252 | */ 253 | void awaitTermination(Thread t, long timeoutMillis) { 254 | try { 255 | t.join(timeoutMillis); 256 | } catch (InterruptedException fail) { 257 | threadUnexpectedException(fail); 258 | } finally { 259 | if (t.getState() != Thread.State.TERMINATED) { 260 | t.interrupt(); 261 | fail("Test timed out"); 262 | } 263 | } 264 | } 265 | 266 | /** 267 | * Waits for LONG_DELAY_MS milliseconds for the thread to terminate (using {@link 268 | * Thread#join(long)}), else interrupts the thread (in the hope that it may terminate later) and 269 | * fails. 270 | */ 271 | void awaitTermination(Thread t) { 272 | awaitTermination(t, LONG_DELAY_MS); 273 | } 274 | 275 | // Some convenient Runnable classes 276 | 277 | public abstract class CheckedRunnable implements Runnable { 278 | protected abstract void realRun() throws Throwable; 279 | 280 | public final void run() { 281 | try { 282 | realRun(); 283 | } catch (Throwable fail) { 284 | threadUnexpectedException(fail); 285 | } 286 | } 287 | } 288 | 289 | public void await(CountDownLatch latch) { 290 | try { 291 | assertTrue(latch.await(LONG_DELAY_MS, MILLISECONDS)); 292 | } catch (Throwable fail) { 293 | threadUnexpectedException(fail); 294 | } 295 | } 296 | 297 | /** 298 | * A CyclicBarrier that uses timed await and fails with AssertionFailedErrors instead of throwing 299 | * checked exceptions. 300 | */ 301 | public static class CheckedBarrier extends CyclicBarrier { 302 | public CheckedBarrier(int parties) { 303 | super(parties); 304 | } 305 | 306 | public int await() { 307 | try { 308 | return super.await(2 * LONG_DELAY_MS, MILLISECONDS); 309 | } catch (TimeoutException timedOut) { 310 | throw new AssertionFailedError("timed out"); 311 | } catch (Exception fail) { 312 | AssertionFailedError afe = new AssertionFailedError("Unexpected exception: " + fail); 313 | afe.initCause(fail); 314 | throw afe; 315 | } 316 | } 317 | } 318 | 319 | public void assertIteratorExhausted(Iterator it) { 320 | try { 321 | it.next(); 322 | shouldThrow(); 323 | } catch (NoSuchElementException success) { 324 | // expected 325 | } 326 | assertFalse(it.hasNext()); 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/main/java/lbmq/LinkedBlockingMultiQueue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Derived from work made by Doug Lea with assistance from members of JCP JSR-166 Expert Group 3 | * (https://jcp.org/en/jsr/detail?id=166). The original work is in the public domain, as explained at 4 | * http://creativecommons.org/publicdomain/zero/1.0/ 5 | */ 6 | package lbmq; 7 | 8 | import java.lang.reflect.Array; 9 | import java.util.ArrayList; 10 | import java.util.Collection; 11 | import java.util.Iterator; 12 | import java.util.NoSuchElementException; 13 | import java.util.concurrent.ConcurrentHashMap; 14 | import java.util.concurrent.TimeUnit; 15 | import java.util.concurrent.atomic.AtomicInteger; 16 | import java.util.concurrent.locks.Condition; 17 | import java.util.concurrent.locks.ReentrantLock; 18 | 19 | /** 20 | * An optionally-bounded blocking "multi-queue" based on linked nodes. A multi-queue is actually a 21 | * set of queues that are connected at the heads and have independent tails (the head of the queue 22 | * is that element that has been on the queue the longest time. The tail of the queue is that 23 | * element that has been on the queue the shortest time). New elements are added at the tail of one 24 | * of the queues, and the queue retrieval operations obtain elements from the head of some of the 25 | * queues, according to a policy that is described below. 26 | * 27 | *

This class essentially allows a consumer to efficiently block a single thread on a set of 28 | * queues, until one becomes available. The special feature is that individual queues can be enabled 29 | * or disabled. A disabled queue is not considered for polling (in the event that all the queue are 30 | * disabled, any blocking operation would do so trying to read, as if all the queues were empty). 31 | * Elements are taken from the set of enabled queues, obeying the established priority (queues with 32 | * the same priority are served round-robin). 33 | * 34 | *

A disabled queue accepts new elements normally until it reaches the maximum capacity (if any). 35 | * 36 | *

Individual queues can be added, removed, enabled or disabled at any time. 37 | * 38 | *

The optional capacity bound constructor argument serves as a way to prevent excessive queue 39 | * expansion. The capacity, if unspecified, is equal to Int.MaxVaue. Linked nodes are dynamically 40 | * created upon each insertion unless this would bring the queue above capacity. 41 | * 42 | *

Not being actually a linear queue, this class does not implement the {@code Collection} or 43 | * {@code Queue} interfaces. The traditional queue interface is split in the traits: {@code 44 | * Offerable} and {@code Pollable}. Sub-queues do however implement Collection. 45 | * 46 | * @see java.util.concurrent.LinkedBlockingQueue 47 | */ 48 | public class LinkedBlockingMultiQueue extends AbstractPollable { 49 | 50 | /* 51 | * This implementation is inspired by the LinkedBlockingQueue, made by Doug Lea with assistance from members of JCP 52 | * JSR-166 Expert Group (https://jcp.org/en/jsr/detail?id=166). 53 | * 54 | * Each sub-queue uses, as does the LinkedBlockingQueue, a variant of the "two lock queue" algorithm. The putLock 55 | * gates entry to put (and offer), and has an associated condition for waiting puts. The takeLock, on the other 56 | * hand, is unique and shared among all the sub-queues. 57 | * 58 | * Each subqueue has a "count" field, that is maintained as an atomic to avoid needing to get both locks in most 59 | * cases. Also, to minimize need for puts to get takeLock and vice-versa, cascading notifies are used. When a put 60 | * notices that it has enabled at least one take, it signals taker. That taker in turn signals others if more items 61 | * have been entered since the signal. And symmetrically for takes signaling puts. 62 | * 63 | * The possibility of disabling sub-queues introduces the necessity of an additional centralized atomic count field, 64 | * which is also updated in every operation and represents, at any time, how many elements can be taken before 65 | * exhausting the queue. 66 | * 67 | * Operations such as remove(Object) and iterators acquire both the corresponding putLock and the takeLock. 68 | * 69 | * Visibility between writers and readers is provided as follows: 70 | * 71 | * Whenever an element is enqueued, the putLock is acquired and count updated. A subsequent reader guarantees 72 | * visibility to the enqueued Node by either acquiring the putLock (via fullyLock) or by acquiring the takeLock, and 73 | * then reading n = count.get(); this gives visibility to the first n items. 74 | * 75 | * To implement weakly consistent iterators, it appears we need to keep all Nodes GC-reachable from a predecessor 76 | * dequeued Node. That would cause two problems: 77 | * 78 | * - allow a rogue Iterator to cause unbounded memory retention 79 | * 80 | * - cause cross-generational linking of old Nodes to new Nodes if a Node was tenured while live, which generational 81 | * GCs have a hard time dealing with, causing repeated major collections. However, only non-deleted Nodes need to be 82 | * reachable from dequeued Nodes, and reachability does not necessarily have to be of the kind understood by the GC. 83 | * We use the trick of linking a Node that has just been dequeued to itself. Such a self-link implicitly means to 84 | * advance to head.next. 85 | */ 86 | 87 | private final ConcurrentHashMap subQueues = new ConcurrentHashMap<>(); 88 | 89 | /** Lock held by take, poll, etc */ 90 | private final ReentrantLock takeLock = new ReentrantLock(); 91 | 92 | /** Wait queue for waiting takes */ 93 | private final Condition notEmpty = takeLock.newCondition(); 94 | 95 | /** Current number of elements in enabled sub-queues */ 96 | private final AtomicInteger totalCount = new AtomicInteger(); 97 | 98 | /** A list of priority groups. Group consists of multiple queues. */ 99 | private final ArrayList priorityGroups = new ArrayList<>(); 100 | 101 | /** Allows to choose the next subQueue to be used. */ 102 | private final SubQueueSelection subQueueSelection; 103 | 104 | /** Constructor. The default {@link DefaultSubQueueSelection} will be used. */ 105 | public LinkedBlockingMultiQueue() { 106 | this(new DefaultSubQueueSelection<>()); 107 | } 108 | 109 | /** 110 | * Constructor. 111 | * 112 | * @param subQueueSelection an implementation of {@link SubQueueSelection} 113 | */ 114 | public LinkedBlockingMultiQueue(SubQueueSelection subQueueSelection) { 115 | this.subQueueSelection = subQueueSelection; 116 | this.subQueueSelection.setPriorityGroups(this.priorityGroups); 117 | } 118 | 119 | /** Set of sub-queues with the same priority */ 120 | public class PriorityGroup { 121 | 122 | final int priority; 123 | final ArrayList queues = new ArrayList<>(0); 124 | 125 | PriorityGroup(int priority) { 126 | this.priority = priority; 127 | } 128 | 129 | int nextIdx = 0; 130 | 131 | void addQueue(SubQueue subQueue) { 132 | queues.add(subQueue); 133 | subQueue.priorityGroup = this; 134 | } 135 | 136 | void removeQueue(SubQueue removed) { 137 | Iterator it = queues.iterator(); 138 | while (it.hasNext()) { 139 | SubQueue subQueue = it.next(); 140 | if (subQueue.key == removed.key) { 141 | removed.putLock.lock(); 142 | try { 143 | it.remove(); 144 | if (nextIdx == queues.size()) { 145 | nextIdx = 0; 146 | } 147 | if (subQueue.enabled) { 148 | totalCount.getAndAdd(-removed.size()); 149 | } 150 | return; 151 | } finally { 152 | removed.putLock.unlock(); 153 | } 154 | } 155 | } 156 | } 157 | 158 | SubQueue getNextSubQueue() { 159 | // assert takeLock.isHeldByCurrentThread(); 160 | int startIdx = nextIdx; 161 | ArrayList queues = this.queues; 162 | do { 163 | SubQueue child = queues.get(nextIdx); 164 | nextIdx += 1; 165 | if (nextIdx == queues.size()) { 166 | nextIdx = 0; 167 | } 168 | if (child.enabled && !child.isEmpty()) { 169 | return child; 170 | } 171 | } while (nextIdx != startIdx); 172 | return null; 173 | } 174 | 175 | int drainTo(Collection c, int maxElements) { 176 | // assert takeLock.isHeldByCurrentThread(); 177 | int drained = 0; 178 | int emptyQueues = 0; 179 | do { 180 | SubQueue child = queues.get(nextIdx); 181 | nextIdx += 1; 182 | if (nextIdx == queues.size()) { 183 | nextIdx = 0; 184 | } 185 | if (child.enabled && !child.isEmpty()) { 186 | emptyQueues = 0; 187 | c.add(child.dequeue()); 188 | drained += 1; 189 | int oldSize = child.count.getAndDecrement(); 190 | if (oldSize == child.capacity) { 191 | child.signalNotFull(); 192 | } 193 | } else { 194 | emptyQueues += 1; 195 | } 196 | } while (drained < maxElements && emptyQueues < queues.size()); 197 | return drained; 198 | } 199 | 200 | E peek() { 201 | // assert takeLock.isHeldByCurrentThread(); 202 | int startIdx = nextIdx; 203 | do { 204 | SubQueue child = queues.get(nextIdx); 205 | if (child.enabled && !child.isEmpty()) { 206 | return child.head.next.item; 207 | } else { 208 | nextIdx += 1; 209 | if (nextIdx == queues.size()) { 210 | nextIdx = 0; 211 | } 212 | } 213 | } while (nextIdx != startIdx); 214 | return null; 215 | } 216 | } 217 | 218 | /** 219 | * Add a sub queue if absent 220 | * 221 | * @param key the key used to identify the queue 222 | * @param priority the queue priority, a lower number means higher priority 223 | * @return the previous queue associated with the specified key, or {@code null} if there was no 224 | * queue for the key 225 | */ 226 | public SubQueue addSubQueue(K key, int priority) { 227 | return addSubQueue(key, priority, Integer.MAX_VALUE); 228 | } 229 | 230 | /** 231 | * Add a sub-queue if absent 232 | * 233 | * @param key the key used to identify the queue 234 | * @param priority the queue priority, a lower number means higher priority 235 | * @param capacity the capacity of the new sub-queue 236 | * @return the previous queue associated with the specified key, or {@code null} if there was no 237 | * queue for the key 238 | */ 239 | public SubQueue addSubQueue(K key, int priority, int capacity) { 240 | SubQueue subQueue = new SubQueue(key, capacity); 241 | takeLock.lock(); 242 | try { 243 | SubQueue old = subQueues.putIfAbsent(key, subQueue); 244 | if (old == null) { 245 | int i = 0; 246 | boolean added = false; 247 | for (PriorityGroup pg : priorityGroups) { 248 | if (pg.priority == priority) { 249 | pg.addQueue(subQueue); 250 | added = true; 251 | break; 252 | } else if (pg.priority > priority) { 253 | PriorityGroup newPg = new PriorityGroup(priority); 254 | priorityGroups.add(i, newPg); 255 | newPg.addQueue(subQueue); 256 | added = true; 257 | break; 258 | } 259 | i += 1; 260 | } 261 | if (!added) { 262 | PriorityGroup newPg = new PriorityGroup(priority); 263 | priorityGroups.add(newPg); 264 | newPg.addQueue(subQueue); 265 | } 266 | } 267 | return old; 268 | } finally { 269 | takeLock.unlock(); 270 | } 271 | } 272 | 273 | /** 274 | * Remove a sub-queue 275 | * 276 | * @param key the key f the sub-queue that should be removed 277 | * @return the removed SubQueue or null if the key was not in the map 278 | */ 279 | public SubQueue removeSubQueue(K key) { 280 | takeLock.lock(); 281 | try { 282 | SubQueue removed = subQueues.remove(key); 283 | if (removed != null) { 284 | removed.priorityGroup.removeQueue(removed); 285 | if (removed.priorityGroup.queues.isEmpty()) { 286 | this.priorityGroups.remove(removed.priorityGroup); 287 | } 288 | } 289 | return removed; 290 | } finally { 291 | takeLock.unlock(); 292 | } 293 | } 294 | 295 | /** 296 | * Gets a sub-queue 297 | * 298 | * @param key the key f the sub-queue that should be returned 299 | * @return the sub-queue with the corresponding key or null if it does not exist 300 | */ 301 | public SubQueue getSubQueue(K key) { 302 | return subQueues.get(key); 303 | } 304 | 305 | /** 306 | * Signals a waiting take. Called only from put/offer (which do not otherwise ordinarily lock 307 | * takeLock.) 308 | */ 309 | private void signalNotEmpty() { 310 | takeLock.lock(); 311 | try { 312 | notEmpty.signal(); 313 | } finally { 314 | takeLock.unlock(); 315 | } 316 | } 317 | 318 | public E poll(long timeout, TimeUnit unit) throws InterruptedException { 319 | long remaining = unit.toNanos(timeout); 320 | SubQueue subQueue; 321 | E element; 322 | int oldSize; 323 | takeLock.lockInterruptibly(); 324 | try { 325 | while (totalCount.get() == 0) { 326 | if (remaining <= 0) { 327 | return null; 328 | } 329 | remaining = notEmpty.awaitNanos(remaining); 330 | } 331 | // at this point we know there is an element 332 | subQueue = subQueueSelection.getNext(); 333 | element = subQueue.dequeue(); 334 | oldSize = subQueue.count.getAndDecrement(); 335 | if (totalCount.getAndDecrement() > 1) { 336 | // sub-queue still has elements, notify next poller 337 | notEmpty.signal(); 338 | } 339 | } finally { 340 | takeLock.unlock(); 341 | } 342 | if (oldSize == subQueue.capacity) { 343 | // we just took an element from a full queue, notify any blocked offers 344 | subQueue.signalNotFull(); 345 | } 346 | return element; 347 | } 348 | 349 | public E take() throws InterruptedException { 350 | SubQueue subQueue; 351 | int oldSize; 352 | E element; 353 | takeLock.lockInterruptibly(); 354 | try { 355 | while (totalCount.get() == 0) { 356 | notEmpty.await(); 357 | } 358 | // at this point we know there is an element 359 | subQueue = subQueueSelection.getNext(); 360 | element = subQueue.dequeue(); 361 | oldSize = subQueue.count.getAndDecrement(); 362 | if (totalCount.getAndDecrement() > 1) { 363 | // sub-queue still has elements, notify next poller 364 | notEmpty.signal(); 365 | } 366 | } finally { 367 | takeLock.unlock(); 368 | } 369 | if (oldSize == subQueue.capacity) { 370 | // we just took an element from a full queue, notify any blocked offers 371 | subQueue.signalNotFull(); 372 | } 373 | return element; 374 | } 375 | 376 | public E poll() { 377 | SubQueue subQueue; 378 | E element; 379 | int oldSize; 380 | takeLock.lock(); 381 | try { 382 | if (totalCount.get() == 0) { 383 | return null; 384 | } 385 | // at this point we know there is an element 386 | subQueue = subQueueSelection.getNext(); 387 | element = subQueue.dequeue(); 388 | oldSize = subQueue.count.getAndDecrement(); 389 | if (totalCount.getAndDecrement() > 1) { 390 | // sub-queue still has elements, notify next poller 391 | notEmpty.signal(); 392 | } 393 | } finally { 394 | takeLock.unlock(); 395 | } 396 | if (oldSize == subQueue.capacity) { 397 | // we just took an element from a full queue, notify any blocked offers 398 | subQueue.signalNotFull(); 399 | } 400 | return element; 401 | } 402 | 403 | public E peek() { 404 | takeLock.lock(); 405 | try { 406 | if (totalCount.get() == 0) { 407 | return null; 408 | } else { 409 | return subQueueSelection.peek(); 410 | } 411 | } finally { 412 | takeLock.unlock(); 413 | } 414 | } 415 | 416 | /** 417 | * Returns the total size of this multi-queue, that is, the sum of the sizes of all the enabled 418 | * sub-queues. 419 | * 420 | * @return the total size of this multi-queue 421 | */ 422 | public int totalSize() { 423 | return totalCount.get(); 424 | } 425 | 426 | /** 427 | * Returns whether this multi-queue is empty, that is, whether there is any element ready to be 428 | * taken from the head. 429 | * 430 | * @return whether this multi-queue is empty. 431 | */ 432 | public boolean isEmpty() { 433 | return totalSize() == 0; 434 | } 435 | 436 | public int drainTo(Collection c) { 437 | return drainTo(c, Integer.MAX_VALUE); 438 | } 439 | 440 | public int drainTo(Collection c, int maxElements) { 441 | if (c == null) { 442 | throw new NullPointerException(); 443 | } 444 | if (maxElements <= 0) { 445 | return 0; 446 | } 447 | takeLock.lock(); 448 | try { 449 | int n = Math.min(maxElements, totalCount.get()); 450 | // ordered iteration, begin with lower index (highest priority) 451 | int drained = 0; 452 | for (int i = 0; i < priorityGroups.size() && drained < n; i++) { 453 | drained += priorityGroups.get(i).drainTo(c, n - drained); 454 | } 455 | // assert drained == n; 456 | totalCount.getAndAdd(-drained); 457 | return drained; 458 | } finally { 459 | takeLock.unlock(); 460 | } 461 | } 462 | 463 | /** 464 | * Counts the priority groups currently registered in {@link LinkedBlockingMultiQueue}. Suitable 465 | * for debugging and testing. 466 | * 467 | * @return the number of priority groups currently registered 468 | */ 469 | public int getPriorityGroupsCount() { 470 | return priorityGroups.size(); 471 | } 472 | 473 | /** 474 | * Represent a sub-queue inside a multi-queue. Instances of this class are just like any blocking 475 | * queue except that elements cannot be taken from their heads. 476 | */ 477 | public class SubQueue extends AbstractOfferable { 478 | 479 | private final K key; 480 | private final int capacity; 481 | private PriorityGroup priorityGroup; 482 | 483 | SubQueue(K key, int capacity) { 484 | if (capacity <= 0) throw new IllegalArgumentException(); 485 | this.key = key; 486 | this.capacity = capacity; 487 | } 488 | 489 | private final ReentrantLock putLock = new ReentrantLock(); 490 | private final Condition notFull = putLock.newCondition(); 491 | 492 | private final AtomicInteger count = new AtomicInteger(); 493 | private boolean enabled = true; 494 | 495 | public int remainingCapacity() { 496 | return capacity - count.get(); 497 | } 498 | 499 | /** Head of linked list. Invariant: head.item == null */ 500 | private Node head = new Node<>(null); 501 | 502 | /** Tail of linked list. Invariant: last.next == null */ 503 | private Node last = head; 504 | 505 | /** 506 | * Atomically removes all the elements from this queue. The queue will be empty after this call returns. 507 | */ 508 | public void clear() { 509 | fullyLock(); 510 | try { 511 | Node h = head; 512 | Node p = h.next; 513 | while (p != null) { 514 | h.next = h; 515 | p.item = null; // help GC 516 | h = p; 517 | p = h.next; 518 | } 519 | head = last; 520 | int oldCapacity = count.getAndSet(0); 521 | if (oldCapacity == capacity) { 522 | notFull.signal(); 523 | } 524 | if (enabled) { 525 | totalCount.getAndAdd(-oldCapacity); 526 | } 527 | } finally { 528 | fullyUnlock(); 529 | } 530 | } 531 | 532 | /** 533 | * Enable or disable this sub-queue. Enabled queues' elements are taken from the common head of 534 | * the multi-queue. Elements from disabled queues are never taken. Elements can be added to a 535 | * queue regardless of this status (if there is enough remaining capacity). 536 | * 537 | * @param status true to enable, false to disable 538 | */ 539 | public void enable(boolean status) { 540 | fullyLock(); 541 | try { 542 | boolean notChanged = status == enabled; 543 | if (notChanged) { 544 | return; 545 | } 546 | enabled = status; 547 | if (status) { 548 | // potentially unblock waiting polls 549 | int c = count.get(); 550 | if (c > 0) { 551 | totalCount.getAndAdd(c); 552 | notEmpty.signal(); 553 | } 554 | } else { 555 | totalCount.getAndAdd(-count.get()); 556 | } 557 | } finally { 558 | fullyUnlock(); 559 | } 560 | } 561 | 562 | /** 563 | * Returns whether this sub-queue is enabled 564 | * 565 | * @return true is this sub-queue is enabled, false if is disabled. 566 | */ 567 | public boolean isEnabled() { 568 | takeLock.lock(); 569 | try { 570 | return enabled; 571 | } finally { 572 | takeLock.unlock(); 573 | } 574 | } 575 | 576 | private void signalNotFull() { 577 | putLock.lock(); 578 | try { 579 | notFull.signal(); 580 | } finally { 581 | putLock.unlock(); 582 | } 583 | } 584 | 585 | private void enqueue(Node node) { 586 | last.next = node; 587 | last = node; 588 | } 589 | 590 | /** 591 | * Return the number of elements in this sub queue. This method returns the actual number of 592 | * elements, regardless of whether the queue is enabled or not. 593 | */ 594 | public int size() { 595 | return count.get(); 596 | } 597 | 598 | /** 599 | * Return whether the queue is empty. This method bases its return value in the actual number of 600 | * elements, regardless of whether the queue is enabled or not. 601 | */ 602 | public boolean isEmpty() { 603 | return size() == 0; 604 | } 605 | 606 | public void put(E e) throws InterruptedException { 607 | if (e == null) { 608 | throw new NullPointerException(); 609 | } 610 | long oldSize = -1; 611 | /* 612 | * As this method never fails to insert, it is more efficient to pre-create the node outside the lock, to 613 | * reduce contention 614 | */ 615 | Node node = new Node<>(e); 616 | putLock.lockInterruptibly(); 617 | try { 618 | /* 619 | * Note that count is used in wait guard even though it is not protected by lock. This works because 620 | * count can only decrease at this point (all other puts are shut out by lock), and we (or some other 621 | * waiting put) are signaled if it ever changes from capacity. Similarly for all other uses of count in 622 | * other wait guards. 623 | */ 624 | while (count.get() == capacity) { 625 | notFull.await(); 626 | } 627 | enqueue(node); 628 | if (count.getAndIncrement() + 1 < capacity) { 629 | // queue not full after adding, notify next offerer 630 | notFull.signal(); 631 | } 632 | if (enabled) { 633 | oldSize = totalCount.getAndIncrement(); 634 | } 635 | } finally { 636 | putLock.unlock(); 637 | } 638 | if (oldSize == 0) { 639 | // just added an element to an empty queue, notify pollers 640 | signalNotEmpty(); 641 | } 642 | } 643 | 644 | public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { 645 | if (e == null) { 646 | throw new NullPointerException(); 647 | } 648 | long nanos = unit.toNanos(timeout); 649 | long oldSize = -1; 650 | putLock.lockInterruptibly(); 651 | try { 652 | while (count.get() == capacity) { 653 | if (nanos <= 0) { 654 | return false; 655 | } 656 | nanos = notFull.awaitNanos(nanos); 657 | } 658 | enqueue(new Node<>(e)); 659 | if (count.getAndIncrement() + 1 < capacity) { 660 | // queue not full after adding, notify next offerer 661 | notFull.signal(); 662 | } 663 | if (enabled) oldSize = totalCount.getAndIncrement(); 664 | } finally { 665 | putLock.unlock(); 666 | } 667 | if (oldSize == 0) { 668 | // just added an element to an empty queue, notify pollers 669 | signalNotEmpty(); 670 | } 671 | return true; 672 | } 673 | 674 | public boolean offer(E e) { 675 | if (e == null) { 676 | throw new NullPointerException(); 677 | } 678 | long oldSize = -1; 679 | if (count.get() == capacity) { 680 | return false; 681 | } 682 | putLock.lock(); 683 | try { 684 | if (count.get() == capacity) { 685 | return false; 686 | } 687 | enqueue(new Node<>(e)); 688 | if (count.getAndIncrement() + 1 < capacity) { 689 | // queue not full after adding, notify next offerer 690 | notFull.signal(); 691 | } 692 | if (enabled) oldSize = totalCount.getAndIncrement(); 693 | } finally { 694 | putLock.unlock(); 695 | } 696 | if (oldSize == 0) { 697 | // just added an element to an empty queue, notify pollers 698 | signalNotEmpty(); 699 | } 700 | return true; 701 | } 702 | 703 | public boolean remove(Object o) { 704 | if (o == null) { 705 | return false; 706 | } 707 | fullyLock(); 708 | try { 709 | for (Node trail = head, p = trail.next; p != null; trail = p, p = p.next) { 710 | if (o.equals(p.item)) { 711 | unlink(p, trail); 712 | return true; 713 | } 714 | } 715 | return false; 716 | } finally { 717 | fullyUnlock(); 718 | } 719 | } 720 | 721 | public boolean contains(Object o) { 722 | if (o == null) { 723 | return false; 724 | } 725 | fullyLock(); 726 | try { 727 | for (Node p = head.next; p != null; p = p.next) { 728 | if (o.equals(p.item)) { 729 | return true; 730 | } 731 | } 732 | return false; 733 | } finally { 734 | fullyUnlock(); 735 | } 736 | } 737 | 738 | /** Unlinks interior Node p with predecessor trail. */ 739 | void unlink(Node p, Node trail) { 740 | // assert isFullyLocked(); 741 | // p.next is not changed, to allow iterators that are traversing p to maintain their 742 | // weak-consistency 743 | // guarantee. 744 | p.item = null; 745 | trail.next = p.next; 746 | if (last == p) { 747 | last = trail; 748 | } 749 | if (count.getAndDecrement() == capacity) { 750 | notFull.signal(); 751 | } 752 | if (enabled) { 753 | totalCount.getAndDecrement(); 754 | } 755 | } 756 | 757 | /** Locks to prevent both puts and takes. */ 758 | private void fullyLock() { 759 | takeLock.lock(); 760 | putLock.lock(); 761 | } 762 | 763 | /** Unlocks to allow both puts and takes. */ 764 | private void fullyUnlock() { 765 | putLock.unlock(); 766 | takeLock.unlock(); 767 | } 768 | 769 | /* Tells whether both locks are held by current thread. */ 770 | // private boolean isFullyLocked() { 771 | // return putLock.isHeldByCurrentThread() && takeLock.isHeldByCurrentThread(); 772 | // } 773 | 774 | /** 775 | * Removes a node from head of queue. 776 | * 777 | * @return the node 778 | */ 779 | private E dequeue() { 780 | // assert takeLock.isHeldByCurrentThread(); 781 | // assert size() > 0; 782 | Node h = head; 783 | Node first = h.next; 784 | h.next = h; // help GC 785 | head = first; 786 | E x = first.item; 787 | first.item = null; 788 | return x; 789 | } 790 | 791 | public String toString() { 792 | fullyLock(); 793 | try { 794 | Node p = head.next; 795 | if (p == null) { 796 | return "[]"; 797 | } 798 | StringBuilder sb = new StringBuilder(); 799 | sb.append('['); 800 | for (; ; ) { 801 | E e = p.item; 802 | sb.append(e == this ? "(this Collection)" : e); 803 | p = p.next; 804 | if (p == null) { 805 | return sb.append(']').toString(); 806 | } 807 | sb.append(", "); 808 | } 809 | } finally { 810 | fullyUnlock(); 811 | } 812 | } 813 | 814 | public Object[] toArray() { 815 | fullyLock(); 816 | try { 817 | int size = count.get(); 818 | Object[] a = new Object[size]; 819 | int k = 0; 820 | for (Node p = head.next; p != null; p = p.next) { 821 | a[k++] = p.item; 822 | } 823 | return a; 824 | } finally { 825 | fullyUnlock(); 826 | } 827 | } 828 | 829 | @SuppressWarnings("unchecked") 830 | public T[] toArray(T[] a) { 831 | fullyLock(); 832 | try { 833 | int size = count.get(); 834 | if (a.length < size) { 835 | a = (T[]) Array.newInstance(a.getClass().getComponentType(), size); 836 | } 837 | int k = 0; 838 | for (Node p = head.next; p != null; p = p.next) { 839 | a[k++] = (T) p.item; 840 | } 841 | if (a.length > k) { 842 | a[k] = null; 843 | } 844 | return a; 845 | } finally { 846 | fullyUnlock(); 847 | } 848 | } 849 | 850 | /** 851 | * Returns an iterator over the elements in this queue in proper sequence. The elements will be 852 | * returned in order from first (head) to last (tail). 853 | * 854 | *

The returned iterator is weakly 855 | * consistent. 856 | * 857 | * @return an iterator over the elements in this queue in proper sequence 858 | */ 859 | public Iterator iterator() { 860 | return new Itr(); 861 | } 862 | 863 | private class Itr implements Iterator { 864 | /** 865 | * Basic weakly-consistent iterator. At all times hold the next item to hand out so that if 866 | * hasNext() reports true, we will still have it to return even if lost race with a take, etc. 867 | */ 868 | private Node current; 869 | 870 | private Node lastRet; 871 | private E currentElement; 872 | 873 | Itr() { 874 | fullyLock(); 875 | try { 876 | current = head.next; 877 | if (current != null) { 878 | currentElement = current.item; 879 | } 880 | } finally { 881 | fullyUnlock(); 882 | } 883 | } 884 | 885 | public boolean hasNext() { 886 | return current != null; 887 | } 888 | 889 | /** 890 | * Returns the next live successor of p, or null if no such. 891 | * 892 | *

Unlike other traversal methods, iterators need to handle both: - dequeued nodes (p.next 893 | * == p) - (possibly multiple) interior removed nodes (p.item == null) 894 | */ 895 | private Node nextNode(Node p) { 896 | for (; ; ) { 897 | Node s = p.next; 898 | if (s == p) { 899 | return head.next; 900 | } 901 | if (s == null || s.item != null) { 902 | return s; 903 | } 904 | p = s; 905 | } 906 | } 907 | 908 | public E next() { 909 | fullyLock(); 910 | try { 911 | if (current == null) { 912 | throw new NoSuchElementException(); 913 | } 914 | E x = currentElement; 915 | lastRet = current; 916 | current = nextNode(current); 917 | currentElement = (current == null) ? null : current.item; 918 | return x; 919 | } finally { 920 | fullyUnlock(); 921 | } 922 | } 923 | 924 | public void remove() { 925 | if (lastRet == null) { 926 | throw new IllegalStateException(); 927 | } 928 | fullyLock(); 929 | try { 930 | Node node = lastRet; 931 | lastRet = null; 932 | for (Node trail = head, p = trail.next; p != null; trail = p, p = p.next) { 933 | if (p == node) { 934 | unlink(p, trail); 935 | break; 936 | } 937 | } 938 | } finally { 939 | fullyUnlock(); 940 | } 941 | } 942 | } 943 | } 944 | 945 | private static class Node { 946 | 947 | E item; 948 | 949 | /* 950 | * One of: 951 | * - the real successor Node 952 | * - this Node, meaning the successor is head.next 953 | * - null, meaning there is no successor (this is the last node) 954 | */ 955 | Node next = null; 956 | 957 | Node(E item) { 958 | this.item = item; 959 | } 960 | } 961 | 962 | /** Allows to choose the next subQueue. */ 963 | public interface SubQueueSelection { 964 | 965 | /** 966 | * Returns the next subQueue to be used. 967 | * 968 | * @return a subQueue 969 | */ 970 | LinkedBlockingMultiQueue.SubQueue getNext(); 971 | 972 | /** 973 | * Returns the next element from the queue but keeps it in the queue. 974 | * 975 | * @return the next element from the queue 976 | */ 977 | E peek(); 978 | 979 | /** 980 | * Sets priority groups. 981 | * 982 | * @param priorityGroups priority groups 983 | */ 984 | void setPriorityGroups(ArrayList.PriorityGroup> priorityGroups); 985 | } 986 | } 987 | -------------------------------------------------------------------------------- /src/test/java/lbmq/LinkedBlockingMultiQueueTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Derived from work made by Doug Lea with assistance from members of JCP JSR-166 Expert Group 3 | * (https://jcp.org/en/jsr/detail?id=166). The original work is in the public domain, as explained at 4 | * http://creativecommons.org/publicdomain/zero/1.0/ 5 | */ 6 | package lbmq; 7 | 8 | import static java.util.concurrent.TimeUnit.MILLISECONDS; 9 | import static org.junit.jupiter.api.Assertions.*; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.Collection; 14 | import java.util.Iterator; 15 | import java.util.NoSuchElementException; 16 | import java.util.concurrent.CountDownLatch; 17 | import java.util.concurrent.ExecutorService; 18 | import java.util.concurrent.Executors; 19 | import org.junit.jupiter.api.Test; 20 | 21 | public class LinkedBlockingMultiQueueTest extends TestCase { 22 | 23 | public enum QueueKey { 24 | A, 25 | B, 26 | C 27 | } 28 | 29 | public static LinkedBlockingMultiQueue createSingleQueue(int n) { 30 | LinkedBlockingMultiQueue q = new LinkedBlockingMultiQueue<>(); 31 | q.addSubQueue(QueueKey.A /* key*/, 1 /* priority */, n /* capacity */); 32 | return q; 33 | } 34 | 35 | public static LinkedBlockingMultiQueue createSingleQueue() { 36 | LinkedBlockingMultiQueue q = new LinkedBlockingMultiQueue<>(); 37 | q.addSubQueue(QueueKey.A /* key*/, 1 /* priority */); 38 | return q; 39 | } 40 | 41 | public static LinkedBlockingMultiQueue createMultiQueue() { 42 | LinkedBlockingMultiQueue q = new LinkedBlockingMultiQueue<>(); 43 | q.addSubQueue(QueueKey.A /* key*/, 1 /* priority */); 44 | q.addSubQueue(QueueKey.B /* key*/, 2 /* priority */); 45 | q.addSubQueue(QueueKey.C /* key*/, 2 /* priority */); 46 | return q; 47 | } 48 | 49 | /** Returns a new queue of given size containing consecutive Integers 0 ... n. */ 50 | public static LinkedBlockingMultiQueue populatedSingleQueue(int n) { 51 | LinkedBlockingMultiQueue q = createSingleQueue(n); 52 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 53 | assertTrue(q.isEmpty()); 54 | assertTrue(sq.isEmpty()); 55 | for (int i = 0; i < n; i++) { 56 | assertTrue(sq.offer(i)); 57 | } 58 | assertFalse(q.isEmpty()); 59 | assertFalse(sq.isEmpty()); 60 | assertEquals(0, sq.remainingCapacity()); 61 | assertEquals(n, q.totalSize()); 62 | assertEquals(n, sq.size()); 63 | return q; 64 | } 65 | 66 | /** Returns a new queue of given size containing consecutive Integers 0 ... n. */ 67 | public static LinkedBlockingMultiQueue populatedMultiQueue() { 68 | LinkedBlockingMultiQueue q = createMultiQueue(); 69 | LinkedBlockingMultiQueue.SubQueue qa = q.getSubQueue(QueueKey.A); 70 | LinkedBlockingMultiQueue.SubQueue qb = q.getSubQueue(QueueKey.B); 71 | LinkedBlockingMultiQueue.SubQueue qc = q.getSubQueue(QueueKey.C); 72 | assertTrue(q.isEmpty()); 73 | assertTrue(qa.isEmpty()); 74 | assertTrue(qb.isEmpty()); 75 | assertTrue(qc.isEmpty()); 76 | assertTrue(qa.offer(zero)); 77 | assertTrue(qa.offer(one)); 78 | assertTrue(qa.offer(two)); 79 | assertTrue(qb.offer(three)); 80 | assertTrue(qc.offer(four)); 81 | assertTrue(qb.offer(five)); 82 | assertTrue(qc.offer(six)); 83 | assertTrue(qb.offer(seven)); 84 | assertTrue(qc.offer(eight)); 85 | assertFalse(q.isEmpty()); 86 | assertFalse(qa.isEmpty()); 87 | assertFalse(qb.isEmpty()); 88 | assertFalse(qc.isEmpty()); 89 | assertEquals(9, q.totalSize()); 90 | assertEquals(3, qa.size()); 91 | assertEquals(3, qb.size()); 92 | assertEquals(3, qc.size()); 93 | return q; 94 | } 95 | 96 | /** A new queue has the indicated capacity, or Integer.MAX_VALUE if none given */ 97 | @Test 98 | public void testConstructor1() { 99 | assertEquals(SIZE, createSingleQueue(SIZE).getSubQueue(QueueKey.A).remainingCapacity()); 100 | assertEquals( 101 | Integer.MAX_VALUE, createSingleQueue().getSubQueue(QueueKey.A).remainingCapacity()); 102 | } 103 | 104 | /** Constructor throws IllegalArgumentException if capacity argument nonpositive */ 105 | @Test 106 | public void testConstructor2() { 107 | assertThrows(IllegalArgumentException.class, () -> createSingleQueue(0)); 108 | ; 109 | } 110 | 111 | /** 112 | * Returns an element suitable for insertion in the collection. Override for collections with 113 | * unusual element types. 114 | */ 115 | protected Integer makeElement(int i) { 116 | return i; 117 | } 118 | 119 | /** offer(null) throws NullPointerException */ 120 | @Test 121 | public void testOfferNull() { 122 | final LinkedBlockingMultiQueue q = createSingleQueue(); 123 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 124 | assertThrows(NullPointerException.class, () -> sq.offer(null)); 125 | } 126 | 127 | /** add(null) throws NullPointerException */ 128 | @Test 129 | public void testAddNull() { 130 | final LinkedBlockingMultiQueue q = createSingleQueue(); 131 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 132 | assertThrows(NullPointerException.class, () -> sq.add(null)); 133 | } 134 | 135 | /** timed offer(null) throws NullPointerException */ 136 | @Test 137 | public void testTimedOfferNull() throws InterruptedException { 138 | final LinkedBlockingMultiQueue q = createSingleQueue(); 139 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 140 | long startTime = System.nanoTime(); 141 | try { 142 | sq.offer(null, LONG_DELAY_MS, MILLISECONDS); 143 | shouldThrow(); 144 | } catch (NullPointerException success) { 145 | // expected 146 | } 147 | assertTrue(millisElapsedSince(startTime) < LONG_DELAY_MS); 148 | } 149 | 150 | /** put(null) throws NullPointerException */ 151 | @Test 152 | public void testPutNull() { 153 | final LinkedBlockingMultiQueue q = createSingleQueue(); 154 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 155 | assertThrows(NullPointerException.class, () -> sq.put(null)); 156 | } 157 | 158 | /** put(null) throws NullPointerException */ 159 | @Test 160 | public void testAddAllNull() { 161 | final LinkedBlockingMultiQueue q = createSingleQueue(); 162 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 163 | assertThrows(NullPointerException.class, () -> sq.addAll(null)); 164 | } 165 | 166 | /** addAll of a collection with null elements throws NullPointerException */ 167 | @Test 168 | public void testAddAllNullElements() { 169 | final LinkedBlockingMultiQueue q = createSingleQueue(); 170 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 171 | final Collection elements = Arrays.asList(new Integer[SIZE]); 172 | assertThrows(NullPointerException.class, () -> sq.addAll(elements)); 173 | } 174 | 175 | /** toArray(null) throws NullPointerException */ 176 | @Test 177 | public void testToArray_NullArray() { 178 | LinkedBlockingMultiQueue q = createSingleQueue(); 179 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 180 | assertThrows(NullPointerException.class, () -> sq.toArray((Object[]) null)); 181 | } 182 | 183 | /** drainTo(null) throws NullPointerException */ 184 | @Test 185 | public void testDrainToNull() { 186 | LinkedBlockingMultiQueue q = createSingleQueue(); 187 | assertThrows(NullPointerException.class, () -> q.drainTo(null)); 188 | } 189 | 190 | /** drainTo(null, n) throws NullPointerException */ 191 | @Test 192 | public void testDrainToNullN() { 193 | LinkedBlockingMultiQueue q = createSingleQueue(); 194 | assertThrows(NullPointerException.class, () -> q.drainTo(null, 0)); 195 | } 196 | 197 | /** drainTo(c, n) returns 0 and does nothing when n <= 0 */ 198 | @Test 199 | public void testDrainToNonPositiveMaxElements() { 200 | LinkedBlockingMultiQueue q = createSingleQueue(); 201 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 202 | final int[] ns = {0, -1, -42, Integer.MIN_VALUE}; 203 | for (int n : ns) { 204 | assertEquals(0, q.drainTo(new ArrayList<>(), n)); 205 | } 206 | if (sq.remainingCapacity() > 0) { 207 | // Not SynchronousQueue, that is 208 | Integer one = makeElement(1); 209 | sq.add(one); 210 | for (int n : ns) { 211 | assertEquals(0, q.drainTo(new ArrayList<>(), n)); 212 | } 213 | assertEquals(1, sq.size()); 214 | assertEquals(1, q.totalSize()); 215 | assertSame(one, q.poll()); 216 | } 217 | } 218 | 219 | /** timed poll before a delayed offer times out; after offer succeeds; on interruption throws */ 220 | @Test 221 | public void testTimedPollWithOffer() throws InterruptedException { 222 | final LinkedBlockingMultiQueue q = createSingleQueue(); 223 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 224 | final CheckedBarrier barrier = new CheckedBarrier(2); 225 | final Integer zero = makeElement(0); 226 | Thread t = newStartedThread(new CheckedRunnable() { 227 | public void realRun() throws InterruptedException { 228 | long startTime = System.nanoTime(); 229 | assertNull(q.poll(timeoutMillis(), MILLISECONDS)); 230 | assertTrue(millisElapsedSince(startTime) >= timeoutMillis()); 231 | barrier.await(); 232 | assertSame(zero, q.poll(LONG_DELAY_MS, MILLISECONDS)); 233 | 234 | Thread.currentThread().interrupt(); 235 | try { 236 | q.poll(LONG_DELAY_MS, MILLISECONDS); 237 | shouldThrow(); 238 | } catch (InterruptedException success) { 239 | // expected 240 | } 241 | assertFalse(Thread.interrupted()); 242 | 243 | barrier.await(); 244 | try { 245 | q.poll(LONG_DELAY_MS, MILLISECONDS); 246 | shouldThrow(); 247 | } catch (InterruptedException success) { 248 | // expected 249 | } 250 | assertFalse(Thread.interrupted()); 251 | } 252 | }); 253 | 254 | barrier.await(); 255 | long startTime = System.nanoTime(); 256 | assertTrue(sq.offer(zero, LONG_DELAY_MS, MILLISECONDS)); 257 | assertTrue(millisElapsedSince(startTime) < LONG_DELAY_MS); 258 | 259 | barrier.await(); 260 | assertThreadStaysAlive(t); 261 | t.interrupt(); 262 | awaitTermination(t); 263 | } 264 | 265 | /** timed poll before a delayed offer times out; after offer succeeds; on interruption throws */ 266 | @Test 267 | public void testTimedPollWithOfferMultiDisabled() { 268 | final LinkedBlockingMultiQueue q = createSingleQueue(); 269 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 270 | final CheckedBarrier barrier = new CheckedBarrier(2); 271 | final Integer zero = makeElement(0); 272 | assertTrue(sq.offer(zero)); 273 | sq.enable(false); 274 | Thread t = newStartedThread(new CheckedRunnable() { 275 | public void realRun() throws InterruptedException { 276 | long startTime1 = System.nanoTime(); 277 | assertNull(q.poll(timeoutMillis(), MILLISECONDS)); 278 | assertTrue(millisElapsedSince(startTime1) >= timeoutMillis()); 279 | barrier.await(); 280 | long startTime2 = System.nanoTime(); 281 | assertSame(zero, q.poll(LONG_DELAY_MS, MILLISECONDS)); 282 | assertTrue(millisElapsedSince(startTime2) < SHORT_DELAY_MS); 283 | } 284 | }); 285 | barrier.await(); 286 | sq.enable(true); 287 | awaitTermination(t); 288 | } 289 | 290 | /** timed poll before a delayed offer times out, after offer succeeds; on interruption throws */ 291 | @Test 292 | public void testTimedPollWithOfferMultiAdded() { 293 | final LinkedBlockingMultiQueue q = createSingleQueue(); 294 | final LinkedBlockingMultiQueue.SubQueue qa = q.getSubQueue(QueueKey.A); 295 | final CheckedBarrier barrier = new CheckedBarrier(2); 296 | final Integer zero = makeElement(0); 297 | assertTrue(qa.offer(zero)); 298 | qa.enable(false); 299 | Thread t = newStartedThread(new CheckedRunnable() { 300 | public void realRun() throws InterruptedException { 301 | long startTime1 = System.nanoTime(); 302 | assertNull(q.poll(timeoutMillis(), MILLISECONDS)); 303 | assertTrue(millisElapsedSince(startTime1) >= timeoutMillis()); 304 | barrier.await(); 305 | long startTime2 = System.nanoTime(); 306 | assertSame(zero, q.poll(LONG_DELAY_MS, MILLISECONDS)); 307 | assertTrue(millisElapsedSince(startTime2) < SHORT_DELAY_MS); 308 | } 309 | }); 310 | barrier.await(); 311 | q.addSubQueue(QueueKey.B, 100); 312 | final LinkedBlockingMultiQueue.SubQueue qb = q.getSubQueue(QueueKey.B); 313 | assertTrue(qb.offer(zero)); 314 | awaitTermination(t); 315 | } 316 | 317 | /** take() blocks interruptibly when empty */ 318 | @Test 319 | public void testTakeFromEmptyBlocksInterruptibly() { 320 | final LinkedBlockingMultiQueue q = createSingleQueue(); 321 | final CountDownLatch threadStarted = new CountDownLatch(1); 322 | Thread t = newStartedThread(new CheckedRunnable() { 323 | public void realRun() { 324 | threadStarted.countDown(); 325 | try { 326 | q.take(); 327 | shouldThrow(); 328 | } catch (InterruptedException success) { 329 | // expected 330 | } 331 | assertFalse(Thread.interrupted()); 332 | } 333 | }); 334 | 335 | await(threadStarted); 336 | assertThreadStaysAlive(t); 337 | t.interrupt(); 338 | awaitTermination(t); 339 | } 340 | 341 | /** take() throws InterruptedException immediately if interrupted before waiting */ 342 | @Test 343 | public void testTakeFromEmptyAfterInterrupt() { 344 | final LinkedBlockingMultiQueue q = createSingleQueue(); 345 | Thread t = newStartedThread(new CheckedRunnable() { 346 | public void realRun() { 347 | Thread.currentThread().interrupt(); 348 | try { 349 | q.take(); 350 | shouldThrow(); 351 | } catch (InterruptedException success) { 352 | // expected 353 | } 354 | assertFalse(Thread.interrupted()); 355 | } 356 | }); 357 | 358 | awaitTermination(t); 359 | } 360 | 361 | /** timed poll() blocks interruptibly when empty */ 362 | @Test 363 | public void testTimedPollFromEmptyBlocksInterruptibly() { 364 | final LinkedBlockingMultiQueue q = createSingleQueue(); 365 | final CountDownLatch threadStarted = new CountDownLatch(1); 366 | Thread t = newStartedThread(new CheckedRunnable() { 367 | public void realRun() { 368 | threadStarted.countDown(); 369 | try { 370 | q.poll(2 * LONG_DELAY_MS, MILLISECONDS); 371 | shouldThrow(); 372 | } catch (InterruptedException success) { 373 | // expected 374 | } 375 | assertFalse(Thread.interrupted()); 376 | } 377 | }); 378 | 379 | await(threadStarted); 380 | assertThreadStaysAlive(t); 381 | t.interrupt(); 382 | awaitTermination(t); 383 | } 384 | 385 | /** timed poll() throws InterruptedException immediately if interrupted before waiting */ 386 | @Test 387 | public void testTimedPollFromEmptyAfterInterrupt() { 388 | final LinkedBlockingMultiQueue q = createSingleQueue(); 389 | Thread t = newStartedThread(new CheckedRunnable() { 390 | public void realRun() { 391 | Thread.currentThread().interrupt(); 392 | try { 393 | q.poll(2 * LONG_DELAY_MS, MILLISECONDS); 394 | shouldThrow(); 395 | } catch (InterruptedException success) { 396 | // expected 397 | } 398 | assertFalse(Thread.interrupted()); 399 | } 400 | }); 401 | 402 | awaitTermination(t); 403 | } 404 | 405 | /** 406 | * remove(x) removes x and returns true if present TODO: move to superclass CollectionTest.java 407 | */ 408 | @Test 409 | public void testRemoveElement() { 410 | final LinkedBlockingMultiQueue q = createSingleQueue(); 411 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 412 | final int size = Math.min(sq.remainingCapacity(), SIZE); 413 | final Integer[] elts = new Integer[size]; 414 | assertFalse(sq.contains(makeElement(99))); 415 | assertFalse(sq.remove(makeElement(99))); 416 | checkEmpty(q); 417 | for (int i = 0; i < size; i++) { 418 | sq.add(elts[i] = makeElement(i)); 419 | } 420 | for (int i = 1; i < size; i += 2) { 421 | for (int pass = 0; pass < 2; pass++) { 422 | assertEquals((pass == 0), sq.contains(elts[i])); 423 | assertEquals((pass == 0), sq.remove(elts[i])); 424 | assertFalse(sq.contains(elts[i])); 425 | assertTrue(sq.contains(elts[i - 1])); 426 | if (i < size - 1) assertTrue(sq.contains(elts[i + 1])); 427 | } 428 | } 429 | if (size > 0) assertTrue(sq.contains(elts[0])); 430 | for (int i = size - 2; i >= 0; i -= 2) { 431 | assertTrue(sq.contains(elts[i])); 432 | assertFalse(sq.contains(elts[i + 1])); 433 | assertTrue(sq.remove(elts[i])); 434 | assertFalse(sq.contains(elts[i])); 435 | assertFalse(sq.remove(elts[i + 1])); 436 | assertFalse(sq.contains(elts[i + 1])); 437 | } 438 | checkEmpty(q); 439 | checkEmpty(sq); 440 | } 441 | 442 | /** Queue transitions from empty to full when elements added */ 443 | @Test 444 | public void testEmptyFull() { 445 | LinkedBlockingMultiQueue q = createSingleQueue(2); 446 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 447 | assertTrue(q.isEmpty()); 448 | assertTrue(sq.isEmpty()); 449 | assertEquals(2, sq.remainingCapacity()); 450 | assertTrue(sq.offer(one)); 451 | assertFalse(q.isEmpty()); 452 | assertFalse(sq.isEmpty()); 453 | assertTrue(sq.offer(two)); 454 | assertFalse(q.isEmpty()); 455 | assertFalse(sq.isEmpty()); 456 | assertEquals(0, sq.remainingCapacity()); 457 | assertFalse(sq.offer(three)); 458 | } 459 | 460 | /** remainingCapacity decreases on add, increases on remove */ 461 | @Test 462 | public void testRemainingCapacity() { 463 | LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 464 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 465 | for (int i = 0; i < SIZE; ++i) { 466 | assertEquals(i, sq.remainingCapacity()); 467 | assertEquals(SIZE, sq.size() + sq.remainingCapacity()); 468 | assertEquals(i, q.remove().intValue()); 469 | } 470 | for (int i = 0; i < SIZE; ++i) { 471 | assertEquals(SIZE - i, sq.remainingCapacity()); 472 | assertEquals(SIZE, sq.size() + sq.remainingCapacity()); 473 | assertTrue(sq.offer(i)); 474 | } 475 | } 476 | 477 | /** Offer succeeds if not full; fails if full */ 478 | @Test 479 | public void testOffer() { 480 | LinkedBlockingMultiQueue q = createSingleQueue(1); 481 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 482 | assertTrue(sq.offer(zero)); 483 | assertFalse(sq.offer(one)); 484 | } 485 | 486 | /** add succeeds if not full; throws IllegalStateException if full */ 487 | @Test 488 | public void testAdd() { 489 | LinkedBlockingMultiQueue q = createSingleQueue(SIZE); 490 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 491 | for (int i = 0; i < SIZE; ++i) { 492 | assertTrue(sq.add(i)); 493 | } 494 | assertEquals(0, sq.remainingCapacity()); 495 | assertThrows(IllegalStateException.class, () -> sq.add(SIZE)); 496 | } 497 | 498 | /** addAll(this) throws IllegalArgumentException */ 499 | @Test 500 | public void testAddAllSelf() { 501 | LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 502 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 503 | assertThrows(IllegalArgumentException.class, () -> sq.addAll(sq)); 504 | } 505 | 506 | /** 507 | * addAll of a collection with any null elements throws NPE after possibly adding some elements 508 | */ 509 | @Test 510 | public void testAddAll3() { 511 | LinkedBlockingMultiQueue q = createSingleQueue(SIZE); 512 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 513 | Integer[] ints = new Integer[SIZE]; 514 | for (int i = 0; i < SIZE - 1; ++i) { 515 | ints[i] = i; 516 | } 517 | Collection elements = Arrays.asList(ints); 518 | assertThrows(NullPointerException.class, () -> sq.addAll(elements)); 519 | } 520 | 521 | /** addAll throws IllegalStateException if not enough room */ 522 | @Test 523 | public void testAddAll4() { 524 | LinkedBlockingMultiQueue q = createSingleQueue(SIZE - 1); 525 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 526 | Integer[] ints = new Integer[SIZE]; 527 | for (int i = 0; i < SIZE; ++i) { 528 | ints[i] = i; 529 | } 530 | Collection elements = Arrays.asList(ints); 531 | assertThrows(IllegalStateException.class, () -> sq.addAll(elements)); 532 | } 533 | 534 | /** Queue contains all elements, in traversal order, of successful addAll */ 535 | @Test 536 | public void testAddAll5() { 537 | Integer[] empty = new Integer[0]; 538 | Integer[] ints = new Integer[SIZE]; 539 | for (int i = 0; i < SIZE; ++i) { 540 | ints[i] = i; 541 | } 542 | LinkedBlockingMultiQueue q = createSingleQueue(SIZE); 543 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 544 | assertFalse(sq.addAll(Arrays.asList(empty))); 545 | assertTrue(sq.addAll(Arrays.asList(ints))); 546 | for (int i = 0; i < SIZE; ++i) { 547 | assertEquals(ints[i], q.poll()); 548 | } 549 | } 550 | 551 | /** all elements successfully put are contained */ 552 | @Test 553 | public void testPut() throws InterruptedException { 554 | LinkedBlockingMultiQueue q = createSingleQueue(SIZE); 555 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 556 | for (int i = 0; i < SIZE; ++i) { 557 | Integer x = i; 558 | sq.put(x); 559 | assertTrue(sq.contains(x)); 560 | } 561 | assertEquals(0, sq.remainingCapacity()); 562 | } 563 | 564 | /** put blocks interruptibly if full */ 565 | @Test 566 | public void testBlockingPut() throws InterruptedException { 567 | LinkedBlockingMultiQueue q = createSingleQueue(SIZE); 568 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 569 | final CountDownLatch pleaseInterrupt = new CountDownLatch(1); 570 | Thread t = newStartedThread(new CheckedRunnable() { 571 | public void realRun() throws InterruptedException { 572 | for (int i = 0; i < SIZE; ++i) { 573 | sq.put(i); 574 | } 575 | assertEquals(SIZE, sq.size()); 576 | assertEquals(0, sq.remainingCapacity()); 577 | Thread.currentThread().interrupt(); 578 | try { 579 | sq.put(99); 580 | shouldThrow(); 581 | } catch (InterruptedException success) { 582 | // expected 583 | } 584 | assertFalse(Thread.interrupted()); 585 | 586 | pleaseInterrupt.countDown(); 587 | try { 588 | sq.put(99); 589 | shouldThrow(); 590 | } catch (InterruptedException success) { 591 | // expected 592 | } 593 | assertFalse(Thread.interrupted()); 594 | } 595 | }); 596 | await(pleaseInterrupt); 597 | assertThreadStaysAlive(t); 598 | t.interrupt(); 599 | awaitTermination(t); 600 | assertEquals(SIZE, q.totalSize()); 601 | assertEquals(SIZE, sq.size()); 602 | assertEquals(0, sq.remainingCapacity()); 603 | } 604 | 605 | /** put blocks interruptibly waiting for take when full */ 606 | @Test 607 | public void testPutWithTake() throws InterruptedException { 608 | final int capacity = 2; 609 | final LinkedBlockingMultiQueue q = createSingleQueue(2); 610 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 611 | final CountDownLatch pleaseTake = new CountDownLatch(1); 612 | final CountDownLatch pleaseInterrupt = new CountDownLatch(1); 613 | Thread t = newStartedThread(new CheckedRunnable() { 614 | public void realRun() throws InterruptedException { 615 | for (int i = 0; i < capacity; i++) { 616 | sq.put(i); 617 | } 618 | pleaseTake.countDown(); 619 | sq.put(86); 620 | pleaseInterrupt.countDown(); 621 | try { 622 | sq.put(99); 623 | shouldThrow(); 624 | } catch (InterruptedException success) { 625 | // expected 626 | } 627 | assertFalse(Thread.interrupted()); 628 | } 629 | }); 630 | await(pleaseTake); 631 | assertEquals(0, sq.remainingCapacity()); 632 | assertEquals(0, q.take().intValue()); 633 | await(pleaseInterrupt); 634 | assertThreadStaysAlive(t); 635 | t.interrupt(); 636 | awaitTermination(t); 637 | assertEquals(0, sq.remainingCapacity()); 638 | } 639 | 640 | /** timed offer times out if full and elements not taken */ 641 | @Test 642 | public void testTimedOffer() { 643 | final LinkedBlockingMultiQueue q = createSingleQueue(2); 644 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 645 | final CountDownLatch pleaseInterrupt = new CountDownLatch(1); 646 | Thread t = newStartedThread(new CheckedRunnable() { 647 | public void realRun() throws InterruptedException { 648 | sq.put(new Object()); 649 | sq.put(new Object()); 650 | long startTime = System.nanoTime(); 651 | assertFalse(sq.offer(new Object(), timeoutMillis(), MILLISECONDS)); 652 | assertTrue(millisElapsedSince(startTime) >= timeoutMillis()); 653 | pleaseInterrupt.countDown(); 654 | try { 655 | sq.offer(new Object(), 2 * LONG_DELAY_MS, MILLISECONDS); 656 | shouldThrow(); 657 | } catch (InterruptedException success) { 658 | // expected 659 | } 660 | } 661 | }); 662 | await(pleaseInterrupt); 663 | assertThreadStaysAlive(t); 664 | t.interrupt(); 665 | awaitTermination(t); 666 | } 667 | 668 | /** take retrieves elements in FIFO order */ 669 | @Test 670 | public void testTake() throws InterruptedException { 671 | LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 672 | for (int i = 0; i < SIZE; ++i) { 673 | assertEquals(i, q.take().intValue()); 674 | } 675 | } 676 | 677 | @Test 678 | public void testTakeMulti() throws InterruptedException { 679 | LinkedBlockingMultiQueue q = populatedMultiQueue(); 680 | for (int i = 0; i < nine; ++i) { 681 | assertEquals(i, q.take().intValue()); 682 | } 683 | } 684 | 685 | @Test 686 | public void testTakeMultiDisabled() throws InterruptedException { 687 | LinkedBlockingMultiQueue q = populatedMultiQueue(); 688 | LinkedBlockingMultiQueue.SubQueue qa = q.getSubQueue(QueueKey.A); 689 | qa.enable(false); 690 | for (int i = three; i < nine; ++i) { 691 | assertEquals(i, q.take().intValue()); 692 | } 693 | qa.enable(true); 694 | for (int i = 0; i < three; ++i) { 695 | assertEquals(i, q.take().intValue()); 696 | } 697 | } 698 | 699 | /** Take removes existing elements until empty, then blocks interruptibly */ 700 | @Test 701 | public void testBlockingTake() throws InterruptedException { 702 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 703 | final CountDownLatch pleaseInterrupt = new CountDownLatch(1); 704 | Thread t = newStartedThread(new CheckedRunnable() { 705 | public void realRun() throws InterruptedException { 706 | for (int i = 0; i < SIZE; ++i) { 707 | assertEquals(i, q.take().intValue()); 708 | } 709 | Thread.currentThread().interrupt(); 710 | try { 711 | q.take(); 712 | shouldThrow(); 713 | } catch (InterruptedException success) { 714 | // expected 715 | } 716 | assertFalse(Thread.interrupted()); 717 | pleaseInterrupt.countDown(); 718 | try { 719 | q.take(); 720 | shouldThrow(); 721 | } catch (InterruptedException success) { 722 | // expected 723 | } 724 | assertFalse(Thread.interrupted()); 725 | } 726 | }); 727 | await(pleaseInterrupt); 728 | assertThreadStaysAlive(t); 729 | t.interrupt(); 730 | awaitTermination(t); 731 | } 732 | 733 | /** poll succeeds unless empty */ 734 | @Test 735 | public void testPoll() { 736 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 737 | for (int i = 0; i < SIZE; ++i) { 738 | assertEquals(i, q.poll().intValue()); 739 | } 740 | assertNull(q.poll()); 741 | } 742 | 743 | @Test 744 | public void testPollMulti() { 745 | final LinkedBlockingMultiQueue q = populatedMultiQueue(); 746 | for (int i = 0; i < nine; ++i) { 747 | assertEquals(i, q.poll().intValue()); 748 | } 749 | assertNull(q.poll()); 750 | } 751 | 752 | @Test 753 | public void testPollMultiDisabled() { 754 | LinkedBlockingMultiQueue q = populatedMultiQueue(); 755 | LinkedBlockingMultiQueue.SubQueue qa = q.getSubQueue(QueueKey.A); 756 | qa.enable(false); 757 | for (int i = three; i < nine; ++i) { 758 | assertEquals(i, q.poll().intValue()); 759 | } 760 | assertNull(q.poll()); 761 | qa.enable(true); 762 | for (int i = 0; i < three; ++i) { 763 | assertEquals(i, q.poll().intValue()); 764 | } 765 | assertNull(q.poll()); 766 | } 767 | 768 | /** timed poll with zero timeout succeeds when non-empty, else times out */ 769 | @Test 770 | public void testTimedPoll0() throws InterruptedException { 771 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 772 | for (int i = 0; i < SIZE; ++i) { 773 | assertEquals(i, q.poll(0, MILLISECONDS).intValue()); 774 | } 775 | assertNull(q.poll(0, MILLISECONDS)); 776 | } 777 | 778 | /** timed poll with nonzero timeout succeeds when non-empty, else times out */ 779 | @Test 780 | public void testTimedPoll() throws InterruptedException { 781 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 782 | for (int i = 0; i < SIZE; ++i) { 783 | long startTime = System.nanoTime(); 784 | assertEquals(i, (int) q.poll(LONG_DELAY_MS, MILLISECONDS)); 785 | assertTrue(millisElapsedSince(startTime) < LONG_DELAY_MS); 786 | } 787 | long startTime = System.nanoTime(); 788 | assertNull(q.poll(timeoutMillis(), MILLISECONDS)); 789 | assertTrue(millisElapsedSince(startTime) >= timeoutMillis()); 790 | checkEmpty(q); 791 | } 792 | 793 | /** Interrupted timed poll throws InterruptedException instead of returning timeout status */ 794 | @Test 795 | public void testInterruptedTimedPoll() throws InterruptedException { 796 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 797 | final CountDownLatch aboutToWait = new CountDownLatch(1); 798 | Thread t = newStartedThread(new CheckedRunnable() { 799 | public void realRun() throws InterruptedException { 800 | for (int i = 0; i < SIZE; ++i) { 801 | long t0 = System.nanoTime(); 802 | assertEquals(i, (int) q.poll(LONG_DELAY_MS, MILLISECONDS)); 803 | assertTrue(millisElapsedSince(t0) < SMALL_DELAY_MS); 804 | } 805 | long t0 = System.nanoTime(); 806 | aboutToWait.countDown(); 807 | try { 808 | q.poll(MEDIUM_DELAY_MS, MILLISECONDS); 809 | shouldThrow(); 810 | } catch (InterruptedException success) { 811 | assertTrue(millisElapsedSince(t0) < MEDIUM_DELAY_MS); 812 | } 813 | } 814 | }); 815 | aboutToWait.await(); 816 | waitForThreadToEnterWaitState(t, SMALL_DELAY_MS); 817 | t.interrupt(); 818 | awaitTermination(t, MEDIUM_DELAY_MS); 819 | checkEmpty(q); 820 | } 821 | 822 | /** peek returns next element, or null if empty */ 823 | @Test 824 | public void testPeek() { 825 | LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 826 | for (int i = 0; i < SIZE; ++i) { 827 | assertEquals(i, q.peek().intValue()); 828 | assertEquals(i, q.poll().intValue()); 829 | assertTrue(q.peek() == null || !q.peek().equals(i)); 830 | } 831 | assertNull(q.peek()); 832 | } 833 | 834 | /** peek returns next element, or null if empty */ 835 | @Test 836 | public void testPeekMulti() { 837 | LinkedBlockingMultiQueue q = populatedMultiQueue(); 838 | for (int i = 0; i < nine; ++i) { 839 | assertEquals(i, q.peek().intValue()); 840 | assertEquals(i, q.poll().intValue()); 841 | assertTrue(q.peek() == null || !q.peek().equals(i)); 842 | } 843 | assertNull(q.peek()); 844 | } 845 | 846 | /** peek returns next element, or null if empty */ 847 | @Test 848 | public void testPeekMultiDisabled() { 849 | LinkedBlockingMultiQueue q = populatedMultiQueue(); 850 | LinkedBlockingMultiQueue.SubQueue qa = q.getSubQueue(QueueKey.A); 851 | qa.enable(false); 852 | for (int i = three; i < nine; ++i) { 853 | assertEquals(i, q.peek().intValue()); 854 | assertEquals(i, q.poll().intValue()); 855 | assertTrue(q.peek() == null || !q.peek().equals(i)); 856 | } 857 | assertNull(q.peek()); 858 | qa.enable(true); 859 | for (int i = 0; i < three; ++i) { 860 | assertEquals(i, q.peek().intValue()); 861 | assertEquals(i, q.poll().intValue()); 862 | assertTrue(q.peek() == null || !q.peek().equals(i)); 863 | } 864 | assertNull(q.peek()); 865 | } 866 | 867 | /** element returns next element, or throws NSEE if empty */ 868 | @Test 869 | public void testElement() { 870 | LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 871 | for (int i = 0; i < SIZE; ++i) { 872 | assertEquals(i, q.element().intValue()); 873 | assertEquals(i, q.poll().intValue()); 874 | } 875 | assertThrows(NoSuchElementException.class, () -> q.element()); 876 | } 877 | 878 | /** element returns next element, or throws NSEE if empty */ 879 | @Test 880 | public void testElementMulti() { 881 | LinkedBlockingMultiQueue q = populatedMultiQueue(); 882 | for (int i = 0; i < nine; ++i) { 883 | assertEquals(i, q.element().intValue()); 884 | assertEquals(i, q.poll().intValue()); 885 | } 886 | assertThrows(NoSuchElementException.class, () -> q.element()); 887 | } 888 | 889 | /** element returns next element, or throws NSEE if empty */ 890 | @Test 891 | public void testElementMultiDisabled() { 892 | final LinkedBlockingMultiQueue q = populatedMultiQueue(); 893 | LinkedBlockingMultiQueue.SubQueue qa = q.getSubQueue(QueueKey.A); 894 | qa.enable(false); 895 | for (int i = three; i < nine; ++i) { 896 | assertEquals(i, q.element().intValue()); 897 | assertEquals(i, q.poll().intValue()); 898 | } 899 | try { 900 | q.element(); 901 | shouldThrow(); 902 | } catch (NoSuchElementException success) { 903 | // expected 904 | } 905 | qa.enable(true); 906 | for (int i = 0; i < three; ++i) { 907 | assertEquals(i, q.element().intValue()); 908 | assertEquals(i, q.poll().intValue()); 909 | } 910 | try { 911 | q.element(); 912 | shouldThrow(); 913 | } catch (NoSuchElementException success) { 914 | // expected 915 | } 916 | } 917 | 918 | /** removes next element, or throws NSEE if empty */ 919 | @Test 920 | public void testRemove() { 921 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 922 | for (int i = 0; i < SIZE; ++i) { 923 | assertEquals(i, q.remove().intValue()); 924 | } 925 | assertThrows(NoSuchElementException.class, () -> q.remove()); 926 | } 927 | 928 | /** removes next element, or throws NSEE if empty */ 929 | @Test 930 | public void testRemoveMulti() { 931 | final LinkedBlockingMultiQueue q = populatedMultiQueue(); 932 | for (int i = 0; i < nine; ++i) { 933 | assertEquals(i, q.remove().intValue()); 934 | } 935 | assertThrows(NoSuchElementException.class, () -> q.remove()); 936 | } 937 | 938 | /** An add following remove(x) succeeds */ 939 | @Test 940 | public void testRemoveElementAndAdd() throws InterruptedException { 941 | final LinkedBlockingMultiQueue q = createSingleQueue(); 942 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 943 | assertTrue(sq.add(1)); 944 | assertTrue(sq.add(2)); 945 | assertTrue(sq.remove(1)); 946 | assertTrue(sq.remove(2)); 947 | assertTrue(sq.add(3)); 948 | assertNotNull(q.take()); 949 | } 950 | 951 | /** contains(x) reports true when elements added but not yet removed */ 952 | @Test 953 | public void testContains() { 954 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 955 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 956 | for (int i = 0; i < SIZE; ++i) { 957 | assertTrue(sq.contains(i)); 958 | q.poll(); 959 | assertFalse(sq.contains(i)); 960 | } 961 | } 962 | 963 | /** clear removes all elements */ 964 | @Test 965 | public void testClear() { 966 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 967 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 968 | sq.clear(); 969 | assertTrue(q.isEmpty()); 970 | assertTrue(sq.isEmpty()); 971 | assertEquals(0, q.totalSize()); 972 | assertEquals(0, sq.size()); 973 | assertEquals(SIZE, sq.remainingCapacity()); 974 | sq.add(one); 975 | assertFalse(q.isEmpty()); 976 | assertFalse(sq.isEmpty()); 977 | assertTrue(sq.contains(one)); 978 | sq.clear(); 979 | assertTrue(q.isEmpty()); 980 | assertTrue(sq.isEmpty()); 981 | } 982 | 983 | /** containsAll(c) is true when c contains a subset of elements */ 984 | @Test 985 | public void testContainsAll() { 986 | final LinkedBlockingMultiQueue q1 = populatedSingleQueue(SIZE); 987 | final LinkedBlockingMultiQueue.SubQueue sq1 = q1.getSubQueue(QueueKey.A); 988 | final LinkedBlockingMultiQueue q2 = createSingleQueue(); 989 | final LinkedBlockingMultiQueue.SubQueue sq2 = q2.getSubQueue(QueueKey.A); 990 | for (int i = 0; i < SIZE; ++i) { 991 | assertTrue(sq1.containsAll(sq2)); 992 | assertFalse(sq2.containsAll(sq1)); 993 | sq2.add(i); 994 | } 995 | assertTrue(sq2.containsAll(sq1)); 996 | } 997 | 998 | /** retainAll(c) retains only those elements of c and reports true if changed */ 999 | @Test 1000 | public void testRetainAll() { 1001 | final LinkedBlockingMultiQueue q1 = populatedSingleQueue(SIZE); 1002 | final LinkedBlockingMultiQueue.SubQueue sq1 = q1.getSubQueue(QueueKey.A); 1003 | final LinkedBlockingMultiQueue q2 = populatedSingleQueue(SIZE); 1004 | final LinkedBlockingMultiQueue.SubQueue sq2 = q2.getSubQueue(QueueKey.A); 1005 | for (int i = 0; i < SIZE; ++i) { 1006 | boolean changed = sq1.retainAll(sq2); 1007 | if (i == 0) { 1008 | assertFalse(changed); 1009 | } else { 1010 | assertTrue(changed); 1011 | } 1012 | assertTrue(sq1.containsAll(sq2)); 1013 | assertEquals(SIZE - i, q1.totalSize()); 1014 | assertEquals(SIZE - i, sq1.size()); 1015 | q2.remove(); 1016 | } 1017 | } 1018 | 1019 | /** removeAll(c) removes only those elements of c and reports true if changed */ 1020 | @Test 1021 | public void testRemoveAll() { 1022 | for (int i = 1; i < SIZE; ++i) { 1023 | final LinkedBlockingMultiQueue q1 = populatedSingleQueue(SIZE); 1024 | final LinkedBlockingMultiQueue.SubQueue sq1 = q1.getSubQueue(QueueKey.A); 1025 | final LinkedBlockingMultiQueue q2 = populatedSingleQueue(i); 1026 | final LinkedBlockingMultiQueue.SubQueue sq2 = q2.getSubQueue(QueueKey.A); 1027 | assertTrue(sq1.removeAll(sq2)); 1028 | assertEquals(SIZE - i, q1.totalSize()); 1029 | assertEquals(SIZE - i, sq1.size()); 1030 | for (int j = 0; j < i; ++j) { 1031 | Integer x = q2.remove(); 1032 | assertFalse(sq1.contains(x)); 1033 | } 1034 | } 1035 | } 1036 | 1037 | /** toArray contains all elements in FIFO order */ 1038 | @Test 1039 | public void testToArray() { 1040 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 1041 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 1042 | Object[] o = sq.toArray(); 1043 | for (int i = 0; i < o.length; i++) { 1044 | assertSame(o[i], q.poll()); 1045 | } 1046 | } 1047 | 1048 | /** toArray(a) contains all elements in FIFO order */ 1049 | @Test 1050 | public void testToArray2() { 1051 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 1052 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 1053 | Integer[] ints = new Integer[SIZE]; 1054 | Integer[] array = sq.toArray(ints); 1055 | assertSame(ints, array); 1056 | for (int i = 0; i < ints.length; i++) { 1057 | assertSame(ints[i], q.poll()); 1058 | } 1059 | } 1060 | 1061 | /** toArray(incompatible array type) throws ArrayStoreException */ 1062 | @Test 1063 | public void testToArray1_BadArg() { 1064 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 1065 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 1066 | assertThrows(ArrayStoreException.class, () -> sq.toArray(new String[10])); 1067 | } 1068 | 1069 | /** iterator iterates through all elements */ 1070 | @Test 1071 | public void testIterator() throws InterruptedException { 1072 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 1073 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 1074 | Iterator it = sq.iterator(); 1075 | int i; 1076 | for (i = 0; it.hasNext(); i++) { 1077 | assertTrue(sq.contains(it.next())); 1078 | } 1079 | assertEquals(i, SIZE); 1080 | assertIteratorExhausted(it); 1081 | 1082 | it = sq.iterator(); 1083 | for (i = 0; it.hasNext(); i++) { 1084 | assertEquals(it.next(), q.take()); 1085 | } 1086 | assertEquals(i, SIZE); 1087 | assertIteratorExhausted(it); 1088 | } 1089 | 1090 | /** iterator of empty collection has no elements */ 1091 | @Test 1092 | public void testEmptyIterator() { 1093 | assertIteratorExhausted(createSingleQueue().getSubQueue(QueueKey.A).iterator()); 1094 | } 1095 | 1096 | /** iterator.remove removes current element */ 1097 | @Test 1098 | public void testIteratorRemove() { 1099 | final LinkedBlockingMultiQueue q = createSingleQueue(3); 1100 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 1101 | sq.add(two); 1102 | sq.add(one); 1103 | sq.add(three); 1104 | 1105 | Iterator it = sq.iterator(); 1106 | it.next(); 1107 | it.remove(); 1108 | 1109 | it = sq.iterator(); 1110 | assertSame(it.next(), one); 1111 | assertSame(it.next(), three); 1112 | assertFalse(it.hasNext()); 1113 | } 1114 | 1115 | /** iterator ordering is FIFO */ 1116 | @Test 1117 | public void testIteratorOrdering() { 1118 | final LinkedBlockingMultiQueue q = createSingleQueue(3); 1119 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 1120 | sq.add(one); 1121 | sq.add(two); 1122 | sq.add(three); 1123 | assertEquals(0, sq.remainingCapacity()); 1124 | int k = 0; 1125 | for (int value : sq) { 1126 | assertEquals(++k, value); 1127 | } 1128 | assertEquals(3, k); 1129 | } 1130 | 1131 | /** Modifications do not cause iterators to fail */ 1132 | @Test 1133 | public void testWeaklyConsistentIteration() { 1134 | final LinkedBlockingMultiQueue q = createSingleQueue(3); 1135 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 1136 | sq.add(one); 1137 | sq.add(two); 1138 | sq.add(three); 1139 | for (int ignored : sq) { 1140 | q.remove(); 1141 | } 1142 | assertEquals(0, sq.size()); 1143 | assertEquals(0, q.totalSize()); 1144 | } 1145 | 1146 | /** toString contains toStrings of elements */ 1147 | @Test 1148 | public void testToString() { 1149 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 1150 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 1151 | String s = sq.toString(); 1152 | for (int i = 0; i < SIZE; ++i) { 1153 | assertTrue(s.contains(String.valueOf(i))); 1154 | } 1155 | } 1156 | 1157 | /** offer transfers elements across Executor tasks */ 1158 | @Test 1159 | public void testOfferInExecutor() { 1160 | final LinkedBlockingMultiQueue q = createSingleQueue(2); 1161 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 1162 | sq.add(one); 1163 | sq.add(two); 1164 | ExecutorService executor = Executors.newFixedThreadPool(2); 1165 | final CheckedBarrier threadsStarted = new CheckedBarrier(2); 1166 | executor.execute(new CheckedRunnable() { 1167 | public void realRun() throws InterruptedException { 1168 | assertFalse(sq.offer(three)); 1169 | threadsStarted.await(); 1170 | assertTrue(sq.offer(three, LONG_DELAY_MS, MILLISECONDS)); 1171 | assertEquals(0, sq.remainingCapacity()); 1172 | } 1173 | }); 1174 | executor.execute(new CheckedRunnable() { 1175 | public void realRun() throws InterruptedException { 1176 | threadsStarted.await(); 1177 | assertSame(one, q.take()); 1178 | } 1179 | }); 1180 | joinPool(executor); 1181 | } 1182 | 1183 | /** timed poll retrieves elements across Executor threads */ 1184 | @Test 1185 | public void testPollInExecutor() { 1186 | final LinkedBlockingMultiQueue q = createSingleQueue(2); 1187 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 1188 | final CheckedBarrier threadsStarted = new CheckedBarrier(2); 1189 | ExecutorService executor = Executors.newFixedThreadPool(2); 1190 | executor.execute(new CheckedRunnable() { 1191 | public void realRun() throws InterruptedException { 1192 | assertNull(q.poll()); 1193 | threadsStarted.await(); 1194 | assertSame(one, q.poll(LONG_DELAY_MS, MILLISECONDS)); 1195 | checkEmpty(q); 1196 | } 1197 | }); 1198 | executor.execute(new CheckedRunnable() { 1199 | public void realRun() throws InterruptedException { 1200 | threadsStarted.await(); 1201 | sq.put(one); 1202 | } 1203 | }); 1204 | joinPool(executor); 1205 | } 1206 | 1207 | /** drainTo(c) empties queue into another collection c */ 1208 | @Test 1209 | public void testDrainTo() { 1210 | final LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 1211 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 1212 | ArrayList l = new ArrayList<>(); 1213 | q.drainTo(l); 1214 | assertEquals(0, q.totalSize()); 1215 | assertEquals(0, sq.size()); 1216 | assertEquals(SIZE, l.size()); 1217 | for (int i = 0; i < SIZE; ++i) { 1218 | assertEquals(l.get(i), Integer.valueOf(i)); 1219 | } 1220 | sq.add(zero); 1221 | sq.add(one); 1222 | assertFalse(q.isEmpty()); 1223 | assertTrue(sq.contains(zero)); 1224 | assertTrue(sq.contains(one)); 1225 | l.clear(); 1226 | q.drainTo(l); 1227 | assertEquals(0, q.totalSize()); 1228 | assertEquals(0, sq.size()); 1229 | assertEquals(2, l.size()); 1230 | for (int i = 0; i < 2; ++i) { 1231 | assertEquals(l.get(i), Integer.valueOf(i)); 1232 | } 1233 | } 1234 | 1235 | /** drainTo(c) empties queue into another collection c */ 1236 | @Test 1237 | public void testDrainToMulti() { 1238 | LinkedBlockingMultiQueue q = populatedMultiQueue(); 1239 | ArrayList l = new ArrayList<>(); 1240 | q.drainTo(l); 1241 | assertEquals(0, q.totalSize()); 1242 | assertEquals(nine.intValue(), l.size()); 1243 | for (int i = 0; i < nine; ++i) { 1244 | assertEquals(l.get(i), Integer.valueOf(i)); 1245 | } 1246 | } 1247 | 1248 | /** drainTo(c) empties queue into another collection c */ 1249 | @Test 1250 | public void testDrainToMultiDisabled() { 1251 | LinkedBlockingMultiQueue q = populatedMultiQueue(); 1252 | LinkedBlockingMultiQueue.SubQueue qa = q.getSubQueue(QueueKey.A); 1253 | ArrayList l1 = new ArrayList<>(); 1254 | qa.enable(false); 1255 | q.drainTo(l1); 1256 | assertEquals(0, q.totalSize()); 1257 | assertEquals(six.intValue(), l1.size()); 1258 | for (int i = three; i < nine; ++i) { 1259 | assertEquals(l1.get(i - three), Integer.valueOf(i)); 1260 | } 1261 | qa.enable(true); 1262 | assertEquals(3, q.totalSize()); 1263 | ArrayList l2 = new ArrayList<>(); 1264 | q.drainTo(l2); 1265 | assertEquals(0, q.totalSize()); 1266 | assertEquals(three.intValue(), l2.size()); 1267 | for (int i = 0; i < three; ++i) { 1268 | assertEquals(l2.get(i), Integer.valueOf(i)); 1269 | } 1270 | } 1271 | 1272 | /** drainTo empties full queue, unblocking a waiting put. */ 1273 | @Test 1274 | public void testDrainToWithActivePut() throws InterruptedException { 1275 | LinkedBlockingMultiQueue q = populatedSingleQueue(SIZE); 1276 | LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 1277 | Thread t = new Thread(new CheckedRunnable() { 1278 | public void realRun() throws InterruptedException { 1279 | sq.put(SIZE + 1); 1280 | } 1281 | }); 1282 | t.start(); 1283 | ArrayList l = new ArrayList<>(); 1284 | q.drainTo(l); 1285 | assertTrue(l.size() >= SIZE); 1286 | for (int i = 0; i < SIZE; ++i) { 1287 | assertEquals(l.get(i), Integer.valueOf(i)); 1288 | } 1289 | t.join(); 1290 | assertTrue(q.totalSize() + l.size() >= SIZE); 1291 | assertTrue(sq.size() + l.size() >= SIZE); 1292 | } 1293 | 1294 | /** drainTo(c, n) empties first min(n, size) elements of queue into c */ 1295 | @Test 1296 | public void testDrainToN() { 1297 | final LinkedBlockingMultiQueue q = createSingleQueue(); 1298 | final LinkedBlockingMultiQueue.SubQueue sq = q.getSubQueue(QueueKey.A); 1299 | for (int i = 0; i < SIZE + 2; ++i) { 1300 | for (int j = 0; j < SIZE; j++) { 1301 | assertTrue(sq.offer(j)); 1302 | } 1303 | ArrayList l = new ArrayList<>(); 1304 | q.drainTo(l, i); 1305 | int k = Math.min(i, SIZE); 1306 | assertEquals(k, l.size()); 1307 | assertEquals(SIZE - k, q.totalSize()); 1308 | assertEquals(SIZE - k, sq.size()); 1309 | for (int j = 0; j < k; ++j) { 1310 | assertEquals(l.get(j), Integer.valueOf(j)); 1311 | } 1312 | do { 1313 | // empty 1314 | } while (q.poll() != null); 1315 | } 1316 | } 1317 | 1318 | /** remove(null), contains(null) always return false */ 1319 | @Test 1320 | public void testNeverContainsNull() { 1321 | final LinkedBlockingMultiQueue q1 = createSingleQueue(); 1322 | final LinkedBlockingMultiQueue.SubQueue sq1 = q1.getSubQueue(QueueKey.A); 1323 | assertFalse(sq1.contains(null)); 1324 | assertFalse(sq1.remove(null)); 1325 | final LinkedBlockingMultiQueue q2 = populatedSingleQueue(2); 1326 | final LinkedBlockingMultiQueue.SubQueue sq2 = q2.getSubQueue(QueueKey.A); 1327 | assertFalse(sq2.contains(null)); 1328 | assertFalse(sq2.remove(null)); 1329 | } 1330 | 1331 | @Test 1332 | public void testAddRemoveSubQueues() { 1333 | LinkedBlockingMultiQueue q = new LinkedBlockingMultiQueue<>(); 1334 | assertEquals(0, q.totalSize()); 1335 | q.addSubQueue(QueueKey.A, 1 /* priority */); 1336 | final LinkedBlockingMultiQueue.SubQueue qa = q.getSubQueue(QueueKey.A); 1337 | qa.offer(one); 1338 | assertEquals(1, q.totalSize()); 1339 | q.removeSubQueue(QueueKey.A); 1340 | assertEquals(0, q.totalSize()); 1341 | q.addSubQueue(QueueKey.B, 1 /* priority */); 1342 | final LinkedBlockingMultiQueue.SubQueue qb = q.getSubQueue(QueueKey.B); 1343 | qb.offer(one); 1344 | assertEquals(1, q.totalSize()); 1345 | qb.enable(false); 1346 | assertEquals(0, q.totalSize()); 1347 | assertEquals(qb, q.removeSubQueue(QueueKey.B)); 1348 | assertEquals(0, q.totalSize()); 1349 | } 1350 | 1351 | @Test 1352 | public void testRemoveQueueOfTheSamePriorityDecrementsNextSubQueuePointer() { 1353 | LinkedBlockingMultiQueue q = new LinkedBlockingMultiQueue<>(); 1354 | q.addSubQueue(QueueKey.A, 0); 1355 | q.addSubQueue(QueueKey.B, 0); 1356 | 1357 | q.getSubQueue(QueueKey.A).offer(1); 1358 | q.getSubQueue(QueueKey.B).offer(2); 1359 | 1360 | Integer firstPoll = q.poll(); 1361 | q.removeSubQueue(QueueKey.A); 1362 | Integer secondPoll = q.poll(); 1363 | 1364 | assertEquals(1, (long) firstPoll); 1365 | assertEquals(2, (long) secondPoll); 1366 | } 1367 | 1368 | @Test 1369 | public void testThatPriorityGroupIsRemovedWhenItDoesNotContainAnySubQueue() { 1370 | LinkedBlockingMultiQueue q = new LinkedBlockingMultiQueue<>(); 1371 | q.addSubQueue(QueueKey.A, 0); 1372 | q.addSubQueue(QueueKey.B, 1); 1373 | 1374 | q.getSubQueue(QueueKey.B).offer(2); 1375 | 1376 | q.removeSubQueue(QueueKey.A); 1377 | 1378 | Integer poll = q.poll(); 1379 | 1380 | assertEquals(2, (long) poll); 1381 | } 1382 | 1383 | @Test 1384 | public void testAddPrioritySubQueueWhenLowerPriorityQueueExists() { 1385 | LinkedBlockingMultiQueue q = new LinkedBlockingMultiQueue<>(); 1386 | q.addSubQueue(QueueKey.A, 1); 1387 | q.addSubQueue(QueueKey.B, 0); 1388 | assertEquals(2, q.getPriorityGroupsCount()); 1389 | } 1390 | 1391 | /** {@link lbmq.LinkedBlockingMultiQueue.SubQueue#enable(boolean)} must be idempotent */ 1392 | @Test 1393 | public void testThatTotalSizeMustNotChangeEnablingAlreadyEnabledQueues() { 1394 | final LinkedBlockingMultiQueue subject = new LinkedBlockingMultiQueue<>(); 1395 | subject.addSubQueue(QueueKey.A, 0); 1396 | final LinkedBlockingMultiQueue.SubQueue subQueue = subject.getSubQueue(QueueKey.A); 1397 | subQueue.add(100); 1398 | 1399 | assertEquals(1, subQueue.size()); 1400 | assertEquals(1, subject.totalSize()); 1401 | 1402 | subQueue.enable(true); 1403 | 1404 | assertEquals(1, subQueue.size()); 1405 | assertEquals(1, subject.totalSize()); 1406 | } 1407 | 1408 | /** {@link lbmq.LinkedBlockingMultiQueue.SubQueue#enable(boolean)} must be idempotent */ 1409 | @Test 1410 | public void testThatTotalSizeMustNotChangeDisablingAlreadyDisabledQueues() { 1411 | final LinkedBlockingMultiQueue subject = new LinkedBlockingMultiQueue<>(); 1412 | subject.addSubQueue(QueueKey.A, 0); 1413 | final LinkedBlockingMultiQueue.SubQueue subQueue = subject.getSubQueue(QueueKey.A); 1414 | subQueue.add(100); 1415 | 1416 | assertTrue(subQueue.isEnabled()); 1417 | assertEquals(1, subQueue.size()); 1418 | assertEquals(1, subject.totalSize()); 1419 | 1420 | subQueue.enable(false); 1421 | 1422 | assertEquals(1, subQueue.size()); 1423 | assertEquals(0, subject.totalSize()); 1424 | 1425 | subQueue.enable(false); 1426 | 1427 | assertEquals(1, subQueue.size()); 1428 | assertEquals(0, subject.totalSize()); 1429 | } 1430 | 1431 | void checkEmpty(LinkedBlockingMultiQueue q) { 1432 | try { 1433 | assertTrue(q.isEmpty()); 1434 | assertEquals(0, q.totalSize()); 1435 | 1436 | assertNull(q.peek()); 1437 | 1438 | assertNull(q.poll()); 1439 | assertNull(q.poll(0, MILLISECONDS)); 1440 | 1441 | try { 1442 | q.element(); 1443 | shouldThrow(); 1444 | } catch (NoSuchElementException success) { 1445 | // expected 1446 | } 1447 | try { 1448 | q.remove(); 1449 | shouldThrow(); 1450 | } catch (NoSuchElementException success) { 1451 | // expected 1452 | } 1453 | } catch (InterruptedException fail) { 1454 | threadUnexpectedException(fail); 1455 | } 1456 | } 1457 | 1458 | void checkEmpty(LinkedBlockingMultiQueue.SubQueue q) { 1459 | assertTrue(q.isEmpty()); 1460 | assertEquals(0, q.size()); 1461 | 1462 | assertEquals(q.toString(), "[]"); 1463 | 1464 | assertArrayEquals(q.toArray(), new Object[0]); 1465 | assertFalse(q.iterator().hasNext()); 1466 | 1467 | try { 1468 | q.iterator().next(); 1469 | shouldThrow(); 1470 | } catch (NoSuchElementException success) { 1471 | // expected 1472 | } 1473 | } 1474 | } 1475 | --------------------------------------------------------------------------------