├── src ├── test │ └── java │ │ └── bes │ │ ├── injector │ │ ├── microbench │ │ │ ├── ExecutorType.java │ │ │ ├── ExecutorPlus.java │ │ │ ├── InjectorPlus.java │ │ │ ├── BlockingForkJoinPool.java │ │ │ ├── BlockingThreadPoolExecutor.java │ │ │ ├── DisruptorExecutor.java │ │ │ └── ExecutorBenchmark.java │ │ └── InjectorBurnTest.java │ │ └── concurrent │ │ ├── WaitQueueBurnTest.java │ │ └── SimpleCollectionBurnTest.java └── main │ └── java │ └── bes │ ├── injector │ ├── Work.java │ ├── Injector.java │ ├── InjectionExecutor.java │ └── Worker.java │ └── concurrent │ ├── SimpleCollection.java │ └── WaitQueue.java ├── microbench ├── pom.xml └── LICENSE.txt /src/test/java/bes/injector/microbench/ExecutorType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package bes.injector.microbench; 20 | 21 | public enum ExecutorType 22 | { 23 | INJECTOR, JDK, FJP, DISRUPTOR_SPIN, DISRUPTOR_BLOCK 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/bes/injector/microbench/ExecutorPlus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package bes.injector.microbench; 20 | 21 | import java.util.concurrent.Executor; 22 | 23 | public interface ExecutorPlus extends Executor 24 | { 25 | public void maybeExecuteInline(Runnable runnable); 26 | } 27 | -------------------------------------------------------------------------------- /microbench: -------------------------------------------------------------------------------- 1 | if [ $# -eq 0 ] ; then 2 | echo "usage: microbench ..." 3 | echo "for jmh params: forks,warmups,warmupLength,measurements,measurementLength,producerThreads=" 4 | echo "for test params: opSleep=/[,...]" 5 | echo " opWork=[,...]" 6 | echo " tasks=:[,:...] (ratio of input gate and executor queue size to number of threads)" 7 | echo " type=[,...]" 8 | echo " threads=[,...]" 9 | echo " executorChainLength=[,...]" 10 | echo "e.g. microbench forks=1 warmups=5 warmupLength=1 measurements=5 measurementLength=1 producerThreads=1" 11 | echo " opSleep=0/0 opWork=1,10,100 tasks=0.5:1,1:1,2:2,4:4 type=INJECTOR,JDK threads=32,128,512 executorChainLength=1" 12 | echo " (these are the default parameters; any omitted will take the value presented, except opSleep which includes 10/0.1 for illustrative purposes)" 13 | exit 1 14 | fi 15 | mvn -DskipTests=true package 16 | mvn dependency:copy-dependencies 17 | java -cp "target/*:target/dependency/*" bes.injector.microbench.ExecutorBenchmark $* 18 | -------------------------------------------------------------------------------- /src/test/java/bes/injector/microbench/InjectorPlus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package bes.injector.microbench; 20 | 21 | import bes.injector.Injector; 22 | import bes.injector.InjectionExecutor; 23 | 24 | public class InjectorPlus extends Injector 25 | { 26 | public InjectorPlus(String poolName) 27 | { 28 | super(poolName); 29 | } 30 | 31 | public static class InjectionExecutorPlus extends InjectionExecutor implements ExecutorPlus 32 | { 33 | protected InjectionExecutorPlus(int maxWorkers, int maxTasksQueued) 34 | { 35 | super(maxWorkers, maxTasksQueued); 36 | } 37 | } 38 | 39 | public InjectionExecutorPlus newExecutor(int maxWorkers, int maxTasksQueued) 40 | { 41 | return addExecutor(new InjectionExecutorPlus(maxWorkers, maxTasksQueued)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/bes/injector/microbench/BlockingForkJoinPool.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package bes.injector.microbench; 20 | 21 | import java.util.concurrent.ForkJoinPool; 22 | import java.util.concurrent.Semaphore; 23 | 24 | public class BlockingForkJoinPool extends ForkJoinPool implements ExecutorPlus 25 | { 26 | final Semaphore permits; 27 | public BlockingForkJoinPool(int threads, int maxTasksQueued) 28 | { 29 | super(threads); 30 | permits = new Semaphore(maxTasksQueued); 31 | } 32 | 33 | public void execute(final Runnable task) 34 | { 35 | permits.acquireUninterruptibly(); 36 | super.execute(new Runnable() 37 | { 38 | public void run() 39 | { 40 | try 41 | { 42 | task.run(); 43 | } 44 | finally 45 | { 46 | permits.release(); 47 | } 48 | } 49 | }); 50 | } 51 | 52 | public void maybeExecuteInline(Runnable runnable) 53 | { 54 | execute(runnable); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/bes/injector/microbench/BlockingThreadPoolExecutor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package bes.injector.microbench; 20 | 21 | import java.util.concurrent.BlockingQueue; 22 | import java.util.concurrent.LinkedBlockingQueue; 23 | import java.util.concurrent.RejectedExecutionHandler; 24 | import java.util.concurrent.ThreadFactory; 25 | import java.util.concurrent.ThreadPoolExecutor; 26 | import java.util.concurrent.TimeUnit; 27 | 28 | public class BlockingThreadPoolExecutor extends ThreadPoolExecutor implements ExecutorPlus 29 | { 30 | 31 | public BlockingThreadPoolExecutor(int threads, int maxTasksQueued) 32 | { 33 | this(threads, new LinkedBlockingQueue(maxTasksQueued)); 34 | } 35 | 36 | public BlockingThreadPoolExecutor(int threads, BlockingQueue queue) 37 | { 38 | super(threads, threads, 60L, TimeUnit.SECONDS, queue, 39 | new ThreadFactory(){ 40 | public Thread newThread(Runnable r) 41 | { 42 | Thread thread = new Thread(r); 43 | thread.setDaemon(true); 44 | return thread; 45 | } 46 | }, 47 | new RejectedExecutionHandler() 48 | { 49 | public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) 50 | { 51 | try 52 | { 53 | executor.getQueue().offer(r, 1L, TimeUnit.MILLISECONDS); 54 | } 55 | catch (InterruptedException e) 56 | { 57 | } 58 | } 59 | }); 60 | } 61 | 62 | public void maybeExecuteInline(Runnable runnable) 63 | { 64 | execute(runnable); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/bes/concurrent/WaitQueueBurnTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package bes.concurrent; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Iterator; 23 | import java.util.LinkedList; 24 | import java.util.List; 25 | import java.util.concurrent.CountDownLatch; 26 | import java.util.concurrent.ThreadLocalRandom; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | import org.junit.Test; 30 | 31 | public class WaitQueueBurnTest 32 | { 33 | 34 | @Test 35 | public void testRemovals() throws InterruptedException 36 | { 37 | testRemovals(TimeUnit.MINUTES.toNanos(2L)); 38 | } 39 | 40 | public void testRemovals(long duration) throws InterruptedException 41 | { 42 | for (int i = 0 ; i < 7 ; i++) 43 | { 44 | final int maxSignalsPerThread = 4 << i; 45 | final int threadCount = 8; 46 | System.out.println(String.format("Testing up to %d signals for %dm", maxSignalsPerThread * threadCount, 47 | TimeUnit.NANOSECONDS.toMinutes(duration / 7))); 48 | final WaitQueue q = new WaitQueue(); 49 | final long until = System.nanoTime() + (duration / 7); 50 | final CountDownLatch latch = new CountDownLatch(threadCount); 51 | for (int t = 0 ; t < threadCount ; t++) 52 | { 53 | new Thread(new Runnable() 54 | { 55 | public void run() 56 | { 57 | ThreadLocalRandom rand = ThreadLocalRandom.current(); 58 | List signals = new ArrayList<>(); 59 | while (System.nanoTime() < until) 60 | { 61 | int targetSize = rand.nextInt(maxSignalsPerThread); 62 | while (signals.size() < targetSize) 63 | signals.add(q.register()); 64 | while (signals.size() > targetSize) 65 | signals.remove(rand.nextInt(signals.size())).cancel(); 66 | if (!q.present(signals.iterator())) 67 | System.err.println("fail"); 68 | q.hasWaiters(); 69 | } 70 | latch.countDown(); 71 | } 72 | }).start(); 73 | } 74 | latch.await(); 75 | } 76 | } 77 | 78 | public static void main(String[] args) throws InterruptedException 79 | { 80 | new WaitQueueBurnTest().testRemovals(TimeUnit.HOURS.toNanos(2L)); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/bes/injector/microbench/DisruptorExecutor.java: -------------------------------------------------------------------------------- 1 | package bes.injector.microbench; 2 | 3 | import com.lmax.disruptor.EventFactory; 4 | import com.lmax.disruptor.IgnoreExceptionHandler; 5 | import com.lmax.disruptor.RingBuffer; 6 | import com.lmax.disruptor.Sequence; 7 | import com.lmax.disruptor.SequenceBarrier; 8 | import com.lmax.disruptor.WaitStrategy; 9 | import com.lmax.disruptor.WorkHandler; 10 | import com.lmax.disruptor.WorkProcessor; 11 | 12 | import java.util.List; 13 | import java.util.concurrent.AbstractExecutorService; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.Executors; 16 | import java.util.concurrent.ThreadFactory; 17 | import java.util.concurrent.TimeUnit; 18 | 19 | public class DisruptorExecutor extends AbstractExecutorService implements ExecutorPlus 20 | { 21 | 22 | final RingBuffer ringBuffer; 23 | final WorkProcessor[] workProcessors; 24 | final ExecutorService workExec; 25 | 26 | public DisruptorExecutor(int threadCount, int bufferSize, WaitStrategy waitStrategy) 27 | { 28 | ringBuffer = RingBuffer.createMultiProducer(new EventFactory() 29 | { 30 | 31 | @Override 32 | public RContainer newInstance() 33 | { 34 | return new RContainer(); 35 | } 36 | }, bufferSize, waitStrategy); 37 | SequenceBarrier sequenceBarrier = ringBuffer.newBarrier(); 38 | Sequence workSequence = new Sequence(-1); 39 | workProcessors = new WorkProcessor[threadCount]; 40 | for (int i = 0 ; i < threadCount ; i++) 41 | { 42 | workProcessors[i] = new WorkProcessor(ringBuffer, sequenceBarrier, 43 | handler, new IgnoreExceptionHandler(), workSequence); 44 | } 45 | workExec = Executors.newFixedThreadPool(workProcessors.length, new ThreadFactory() 46 | { 47 | public Thread newThread(Runnable r) 48 | { 49 | Thread t = new Thread(r); 50 | t.setDaemon(true); 51 | return t; 52 | } 53 | }); 54 | for (WorkProcessor p : workProcessors) 55 | workExec.execute(p); 56 | } 57 | 58 | @Override 59 | public void shutdown() 60 | { 61 | throw new UnsupportedOperationException(); 62 | } 63 | 64 | @Override 65 | public List shutdownNow() 66 | { 67 | throw new UnsupportedOperationException(); 68 | } 69 | 70 | @Override 71 | public boolean isShutdown() 72 | { 73 | throw new UnsupportedOperationException(); 74 | } 75 | 76 | @Override 77 | public boolean isTerminated() 78 | { 79 | throw new UnsupportedOperationException(); 80 | } 81 | 82 | @Override 83 | public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException 84 | { 85 | throw new UnsupportedOperationException(); 86 | } 87 | 88 | @Override 89 | public void execute(Runnable command) 90 | { 91 | long sequence = ringBuffer.next(); 92 | RContainer event = ringBuffer.get(sequence); 93 | event.runnable = command; 94 | ringBuffer.publish(sequence); 95 | } 96 | 97 | public void maybeExecuteInline(Runnable runnable) 98 | { 99 | execute(runnable); 100 | } 101 | 102 | private static final class RContainer 103 | { 104 | volatile Runnable runnable; 105 | } 106 | 107 | static final WorkHandler handler = new WorkHandler() 108 | { 109 | @Override 110 | public void onEvent(RContainer event) throws Exception 111 | { 112 | Runnable r = event.runnable; 113 | if (r != null) 114 | r.run(); 115 | } 116 | }; 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/bes/concurrent/SimpleCollectionBurnTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package bes.concurrent; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Iterator; 23 | import java.util.List; 24 | import java.util.concurrent.CountDownLatch; 25 | import java.util.concurrent.ThreadLocalRandom; 26 | import java.util.concurrent.TimeUnit; 27 | 28 | import org.junit.Test; 29 | 30 | public class SimpleCollectionBurnTest 31 | { 32 | 33 | @Test 34 | public void testRemovals() throws InterruptedException 35 | { 36 | testRemovals(TimeUnit.MINUTES.toNanos(2L)); 37 | } 38 | 39 | public void testRemovals(long duration) throws InterruptedException 40 | { 41 | for (int i = 0 ; i < 7 ; i++) 42 | { 43 | final int maxSignalsPerThread = 4 << i; 44 | final int threadCount = 8; 45 | System.out.println(String.format("Testing up to %d items for %dm", maxSignalsPerThread * threadCount, 46 | TimeUnit.NANOSECONDS.toMinutes(duration / 7))); 47 | final SimpleCollection c = new SimpleCollection(); 48 | final long until = System.nanoTime() + (duration / 7); 49 | final CountDownLatch latch = new CountDownLatch(threadCount); 50 | for (int t = 0 ; t < threadCount ; t++) 51 | { 52 | new Thread(new Runnable() 53 | { 54 | public void run() 55 | { 56 | ThreadLocalRandom rand = ThreadLocalRandom.current(); 57 | List items = new ArrayList<>(); 58 | while (System.nanoTime() < until) 59 | { 60 | int targetSize = rand.nextInt(maxSignalsPerThread); 61 | while (items.size() < targetSize) 62 | items.add(c.add(new Object())); 63 | while (items.size() > targetSize) 64 | items.remove(rand.nextInt(items.size())).remove(); 65 | if (!present(c, items.iterator())) 66 | System.err.println("fail"); 67 | } 68 | latch.countDown(); 69 | } 70 | }).start(); 71 | } 72 | latch.await(); 73 | } 74 | } 75 | 76 | static boolean present(SimpleCollection c, Iterator check) 77 | { 78 | if (!check.hasNext()) 79 | return true; 80 | Object cur = check.next().item; 81 | for (Object o : c) 82 | { 83 | if (o == cur) 84 | { 85 | if (!check.hasNext()) 86 | return true; 87 | cur = check.next().item; 88 | } 89 | } 90 | return false; 91 | } 92 | 93 | public static void main(String[] args) throws InterruptedException 94 | { 95 | new SimpleCollectionBurnTest().testRemovals(TimeUnit.HOURS.toNanos(2L)); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/bes/injector/Work.java: -------------------------------------------------------------------------------- 1 | package bes.injector;/* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://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 | 19 | /** 20 | * Represents, and communicates changes to, a worker's work state - there are three non-actively-working 21 | * states (STOP_SIGNALLED, STOPPED, AND SPINNING) and two working states: WORKING, and (ASSIGNED), the last 22 | * being represented by a non-static instance with its "assigned" executor set. 23 | * 24 | * STOPPED: indicates the worker is descheduled, and whilst accepts work in this state (causing it to 25 | * be rescheduled) it will generally not be considered for work until all other worker threads are busy. 26 | * In this state we should be present in the injector.descheduled collection, and should be parked 27 | * valid transitions -> (ASSIGNED)|SPINNING 28 | * 29 | * STOP_SIGNALLED: the worker has been asked to deschedule itself, but has not yet done so; only entered from a SPINNING 30 | * state, and generally communicated by and to itself, but maybe set from any worker. this state may be 31 | * preempted and replaced with (ASSIGNED) or SPINNING 32 | * In this state we should be present in the injector.descheduled collection 33 | * valid transitions -> (ASSIGNED)|STOPPED|SPINNING 34 | * 35 | * SPINNING: indicates the worker has no work to perform, so is performing friendly wait-based-spinning 36 | * until it either is (ASSIGNED) some work (by itself or another thread), or sent STOP_SIGNALLED 37 | * In this state we _may_ be in the injector.spinning collection (but only if we are in the middle of a sleep) 38 | * valid transitions -> (ASSIGNED)|STOP_SIGNALLED|SPINNING 39 | * 40 | * (ASSIGNED): asks the worker to perform some work against the specified executor, and preassigns a task permit 41 | * from that executor so that in this state there is always work to perform. 42 | * In general a worker assigns itself this state, but sometimes it may assign another worker the state 43 | * either if there is work outstanding and no-spinning threads, or there is a race to self-assign 44 | * valid transition -> WORKING 45 | * 46 | * WORKING: indicates the worker is actively processing an executor's task queue; in this state it accepts 47 | * no state changes/communications, except from itself; it usually exits this mode into SPINNING, 48 | * but if work is immediately available on another executor it self-triggers (ASSIGNED) 49 | * valid transitions -> SPINNING|(ASSIGNED) 50 | */ 51 | 52 | final class Work 53 | { 54 | static final Work STOP_SIGNALLED = new Work(); 55 | static final Work STOPPED = new Work(); 56 | static final Work SPINNING = new Work(); 57 | static final Work WORKING = new Work(); 58 | static final Work DEAD = new Work(); 59 | 60 | final InjectionExecutor assigned; 61 | 62 | Work(InjectionExecutor executor) 63 | { 64 | this.assigned = executor; 65 | } 66 | 67 | private Work() 68 | { 69 | this.assigned = null; 70 | } 71 | 72 | boolean canAssign(boolean self) 73 | { 74 | // we can assign work if there isn't new work already assigned (or we're dead) and either 75 | // 1) we are assigning to ourselves 76 | // 2) the worker we are assigning to is not already in the middle of WORKING 77 | return !(isDead() | isAssigned()) && (self || !isWorking()); 78 | } 79 | 80 | boolean isSpinning() 81 | { 82 | return this == Work.SPINNING; 83 | } 84 | 85 | boolean isWorking() 86 | { 87 | return this == Work.WORKING; 88 | } 89 | 90 | boolean isStop() 91 | { 92 | return this == Work.STOP_SIGNALLED; 93 | } 94 | 95 | boolean isStopped() 96 | { 97 | return this == Work.STOPPED; 98 | } 99 | 100 | boolean isDead() 101 | { 102 | return this == Work.DEAD; 103 | } 104 | 105 | boolean isAssigned() 106 | { 107 | return assigned != null; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/bes/concurrent/SimpleCollection.java: -------------------------------------------------------------------------------- 1 | package bes.concurrent; 2 | 3 | import java.util.Iterator; 4 | import java.util.NoSuchElementException; 5 | import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; 6 | 7 | public final class SimpleCollection implements Iterable 8 | { 9 | 10 | // dummy head pointer, tail pointer only useful for addition. 11 | // both are only <= their true position, and can be reached 12 | // from any node (deleted or not) prior to them via a chain 13 | // of next links. 14 | private volatile Node head, tail; 15 | 16 | public SimpleCollection() 17 | { 18 | head = tail = new Node(null); 19 | } 20 | 21 | public Node add(E item) 22 | { 23 | Node insert = new Node(item); 24 | Node tail = this.tail; 25 | while (true) 26 | { 27 | Node next = tail.next; 28 | if (next != null) 29 | { 30 | tail = next; 31 | } 32 | else 33 | { 34 | insert.prev = tail; 35 | if (nextUpdater.compareAndSet(tail, null, insert)) 36 | { 37 | // no need for atomicity; if updates are out of order, chain simply needs to be walked forwards 38 | this.tail = insert; 39 | return insert; 40 | } 41 | else 42 | { 43 | tail = tail.next; 44 | } 45 | } 46 | } 47 | } 48 | 49 | public Iterator iterator() 50 | { 51 | return new Iterator() 52 | { 53 | Node cur = head; 54 | Object curv = null; 55 | { setNext(true); } 56 | 57 | private boolean setNext(boolean isHead) 58 | { 59 | boolean adjacent = true; 60 | 61 | Node p = cur, n = p.next; 62 | while (n != null) 63 | { 64 | Object item = n.item; 65 | if (item != null) 66 | { 67 | if (!adjacent) 68 | { 69 | // edit out a chain of deleted items 70 | cur.next = n; 71 | n.prev = cur; 72 | if (isHead) 73 | head = p; 74 | } 75 | 76 | // stash value and position 77 | curv = item; 78 | cur = n; 79 | return true; 80 | } 81 | 82 | if (isHead) 83 | prevUpdater.lazySet(p, null); 84 | p = n; 85 | n = n.next; 86 | adjacent = false; 87 | } 88 | return false; 89 | } 90 | 91 | public boolean hasNext() 92 | { 93 | return curv != null || setNext(false); 94 | } 95 | 96 | public E next() 97 | { 98 | if (curv == null && !setNext(false)) 99 | throw new NoSuchElementException(); 100 | Object r = curv; 101 | curv = null; 102 | return (E) r; 103 | } 104 | 105 | public void remove() 106 | { 107 | cur.remove(); 108 | } 109 | }; 110 | } 111 | 112 | public final class Node 113 | { 114 | volatile Object item; 115 | volatile Node next, prev; 116 | 117 | public Node(Object item) 118 | { 119 | this.item = item; 120 | } 121 | 122 | public boolean isDeleted() 123 | { 124 | return item == null; 125 | } 126 | 127 | // mark ourselves deleted, and attempt to edit ourselves out of the list 128 | public void remove() 129 | { 130 | item = null; 131 | // in case the list has some phantom elements, we try to remove ourselves 132 | // and any contiguous adjacent range of deleted nodes 133 | 134 | // start by finding our live predecessor 135 | Node p = prev; 136 | while (true) 137 | { 138 | if (p == null) 139 | { 140 | // we have no live predecessor; hasWaiters() will remove us 141 | cleanupHead(); 142 | return; 143 | } 144 | if (!p.isDeleted()) 145 | break; 146 | p = p.prev; 147 | } 148 | 149 | // we walk forwards from our live predecessor to find the next live node, 150 | // since the forward chaining is our source of truth (prev is only a helping hand) 151 | Node n = p.next; 152 | // if we are the tail of the list, we cannot be removed 153 | if (n == null) 154 | return; 155 | Node n2; 156 | while (n.isDeleted() && (n2 = n.next) != null) 157 | n = n2; 158 | p.next = n; 159 | } 160 | } 161 | 162 | void cleanupHead() 163 | { 164 | Node ph = head, h = ph, n; 165 | while ((n = h.next) != null && n.isDeleted()) 166 | { 167 | prevUpdater.lazySet(n, null); 168 | h = n; 169 | } 170 | if (ph != h) 171 | head = h; 172 | } 173 | 174 | private static final AtomicReferenceFieldUpdater nextUpdater 175 | = AtomicReferenceFieldUpdater.newUpdater(SimpleCollection.Node.class, SimpleCollection.Node.class, "next"); 176 | private static final AtomicReferenceFieldUpdater prevUpdater 177 | = AtomicReferenceFieldUpdater.newUpdater(SimpleCollection.Node.class, SimpleCollection.Node.class, "prev"); 178 | } 179 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | bes.injector 8 | injector 9 | 1.0-SNAPSHOT 10 | jar 11 | 12 | 13 | 14 | junit 15 | junit 16 | 4.11 17 | test 18 | 19 | 20 | org.openjdk.jmh 21 | jmh-core 22 | ${jmh.version} 23 | provided 24 | 25 | 26 | org.openjdk.jmh 27 | jmh-generator-annprocess 28 | ${jmh.version} 29 | provided 30 | 31 | 32 | com.lmax 33 | disruptor 34 | 3.0.1 35 | provided 36 | 37 | 38 | 39 | 40 | UTF-8 41 | 1.0 42 | 1.7 43 | benchmarks 44 | 45 | 46 | 47 | 48 | 49 | org.apache.maven.plugins 50 | maven-jar-plugin 51 | 2.4 52 | 53 | 54 | 55 | test-jar 56 | 57 | 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-compiler-plugin 62 | 3.1 63 | 64 | ${javac.target} 65 | ${javac.target} 66 | ${javac.target} 67 | 68 | 69 | 70 | org.apache.maven.plugins 71 | maven-shade-plugin 72 | 2.2 73 | 74 | 75 | package 76 | 77 | shade 78 | 79 | 80 | ${uberjar.name} 81 | 82 | 83 | org.openjdk.jmh.Main 84 | 85 | 86 | 87 | 88 | 92 | *:* 93 | 94 | META-INF/*.SF 95 | META-INF/*.DSA 96 | META-INF/*.RSA 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | maven-clean-plugin 109 | 2.5 110 | 111 | 112 | maven-deploy-plugin 113 | 2.8.1 114 | 115 | 116 | maven-install-plugin 117 | 2.5.1 118 | 119 | 120 | maven-jar-plugin 121 | 2.4 122 | 123 | 124 | maven-javadoc-plugin 125 | 2.9.1 126 | 127 | 128 | maven-resources-plugin 129 | 2.6 130 | 131 | 132 | maven-site-plugin 133 | 3.3 134 | 135 | 136 | maven-source-plugin 137 | 2.2.1 138 | 139 | 140 | maven-surefire-plugin 141 | 2.17 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /src/main/java/bes/injector/Injector.java: -------------------------------------------------------------------------------- 1 | package bes.injector;/* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://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 | 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.concurrent.ConcurrentSkipListMap; 22 | import java.util.concurrent.CopyOnWriteArrayList; 23 | import java.util.concurrent.ThreadFactory; 24 | import java.util.concurrent.atomic.AtomicInteger; 25 | import java.util.concurrent.atomic.AtomicLong; 26 | 27 | /** 28 | * A pool of worker threads that are shared between all Executors created with it. Each executor is treated as a distinct 29 | * unit, with its own concurrency and task queue limits, but the threads that service the tasks on each executor are 30 | * free to hop between all other executors at will. 31 | * 32 | * To keep producers from incurring unnecessary delays, once an executor is "spun up" (i.e. is processing tasks at a steady 33 | * rate), adding tasks to the executor often involves only placing the task on the work queue and updating the 34 | * task permits (which imposes our max queue length constraints). Only when it cannot be guaranteed the task will be serviced 35 | * promptly does the producer have to signal a thread itself to perform the work. 36 | * 37 | * We do this by scheduling only if there are no 'spinning' workers on this Injector, or if the task queue is full 38 | * (since we have to block in this case anyway) 39 | * 40 | * The worker threads schedule themselves as far as possible: when they are assigned a task, they will attempt to spawn 41 | * a partner worker to service any other work outstanding on the queue (if any); once they have finished the task they 42 | * will either take another (if any remaining) and repeat this, or they will attempt to assign themselves to another executor 43 | * that does have tasks remaining. If both fail, it will enter a non-busy-spinning phase, where it will sleep for a short 44 | * random interval (based upon the number of threads in this mode, so that the total amount of non-sleeping time remains 45 | * approximately fixed regardless of the number of spinning threads), and upon waking up will again try to assign themselves 46 | * an executor with outstanding tasks to perform. 47 | */ 48 | public class Injector 49 | { 50 | 51 | // the name assigned to workers in the injector, and the id suffix 52 | final AtomicLong workerId = new AtomicLong(); 53 | final AtomicInteger workerCount = new AtomicInteger(); 54 | 55 | // the collection of executors serviced by this injector 56 | final List executors = new CopyOnWriteArrayList<>(); 57 | 58 | // the number of workers currently in a spinning state 59 | final AtomicInteger spinningCount = new AtomicInteger(); 60 | // see Worker.maybeStop() - used to self coordinate stopping of threads 61 | final AtomicLong stopCheck = new AtomicLong(); 62 | // the collection of threads that are (most likely) in a spinning state - new workers are scheduled from here first 63 | final ConcurrentSkipListMap spinning = new ConcurrentSkipListMap(); 64 | // the collection of threads that have been asked to stop/deschedule - new workers are scheduled from here last 65 | final ConcurrentSkipListMap descheduled = new ConcurrentSkipListMap(); 66 | 67 | final ThreadFactory threadFactory; 68 | 69 | public Injector(final String poolName) 70 | { 71 | threadFactory = new ThreadFactory() 72 | { 73 | public Thread newThread(Runnable r) 74 | { 75 | Thread thread = new Thread(r, poolName + "-Worker-" + ((Worker) r).workerId); 76 | thread.setDaemon(true); 77 | return thread; 78 | } 79 | }; 80 | } 81 | 82 | public Injector(ThreadFactory threadFactory) 83 | { 84 | this.threadFactory = threadFactory; 85 | } 86 | 87 | void schedule(Work work, boolean internal) 88 | { 89 | // we try to hand-off our work to the spinning queue before the descheduled queue, even though we expect it to be empty 90 | // all we're doing here is hoping to find a worker without work to do, but it doesn't matter too much what we find; 91 | // we atomically set the task so even if this were a collection of all workers it would be safe, and if they are both 92 | // empty we schedule a new thread 93 | Map.Entry e; 94 | while (null != (e = spinning.pollFirstEntry()) || null != (e = descheduled.pollFirstEntry())) 95 | if (e.getValue().assign(work, false)) 96 | return; 97 | 98 | if (!work.isStop()) 99 | { 100 | try 101 | { 102 | new Worker(workerId.incrementAndGet(), work, this, threadFactory); 103 | workerCount.incrementAndGet(); 104 | } 105 | catch (Throwable t) 106 | { 107 | // the only safe thing to do is to return our permits and 108 | // let the running workers pick up the work when they get a chance 109 | 110 | if (work.assigned != null) 111 | { 112 | work.assigned.returnWorkPermit(); 113 | work.assigned.returnTaskPermit(); 114 | } 115 | 116 | if (!internal) 117 | throw t; 118 | 119 | Thread thread = Thread.currentThread(); 120 | Thread.UncaughtExceptionHandler handler = thread.getUncaughtExceptionHandler(); 121 | if (handler != null) 122 | handler.uncaughtException(thread, t); 123 | } 124 | } 125 | } 126 | 127 | void maybeStartSpinningWorker(boolean internal) 128 | { 129 | // in general the workers manage spinningCount directly; however if it is zero, we increment it atomically 130 | // ourselves to avoid starting a worker unless we have to 131 | int current = spinningCount.get(); 132 | if (current == 0 && spinningCount.compareAndSet(0, 1)) 133 | schedule(Work.SPINNING, internal); 134 | } 135 | 136 | protected E addExecutor(E executor) 137 | { 138 | if (executor.injector != null) 139 | throw new IllegalArgumentException("An InjectionExecutor can only be associated with one Injector!"); 140 | executor.injector = this; 141 | executors.add(executor); 142 | return executor; 143 | } 144 | 145 | public InjectionExecutor newExecutor(int maxWorkers, int maxTasksQueued) 146 | { 147 | InjectionExecutor executor = new InjectionExecutor(maxWorkers, maxTasksQueued); 148 | addExecutor(executor); 149 | return executor; 150 | } 151 | } -------------------------------------------------------------------------------- /src/test/java/bes/injector/microbench/ExecutorBenchmark.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package bes.injector.microbench; 20 | 21 | import java.util.Arrays; 22 | import java.util.HashMap; 23 | import java.util.LinkedHashMap; 24 | import java.util.Map; 25 | import java.util.concurrent.Semaphore; 26 | import java.util.concurrent.ThreadLocalRandom; 27 | import java.util.concurrent.TimeUnit; 28 | import java.util.concurrent.locks.LockSupport; 29 | 30 | import com.lmax.disruptor.BlockingWaitStrategy; 31 | import com.lmax.disruptor.BusySpinWaitStrategy; 32 | import org.openjdk.jmh.annotations.Benchmark; 33 | import org.openjdk.jmh.annotations.BenchmarkMode; 34 | import org.openjdk.jmh.annotations.Fork; 35 | import org.openjdk.jmh.annotations.Measurement; 36 | import org.openjdk.jmh.annotations.Mode; 37 | import org.openjdk.jmh.annotations.OutputTimeUnit; 38 | import org.openjdk.jmh.annotations.Param; 39 | import org.openjdk.jmh.annotations.Scope; 40 | import org.openjdk.jmh.annotations.Setup; 41 | import org.openjdk.jmh.annotations.State; 42 | import org.openjdk.jmh.annotations.Threads; 43 | import org.openjdk.jmh.annotations.Warmup; 44 | import org.openjdk.jmh.infra.Blackhole; 45 | import org.openjdk.jmh.runner.Runner; 46 | import org.openjdk.jmh.runner.RunnerException; 47 | import org.openjdk.jmh.runner.options.ChainedOptionsBuilder; 48 | import org.openjdk.jmh.runner.options.OptionsBuilder; 49 | import org.openjdk.jmh.runner.options.TimeValue; 50 | 51 | @BenchmarkMode(Mode.Throughput) 52 | @OutputTimeUnit(TimeUnit.MILLISECONDS) 53 | @Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) 54 | @Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) 55 | @Fork(1) 56 | @Threads(1) 57 | @State(Scope.Benchmark) 58 | public class ExecutorBenchmark 59 | { 60 | 61 | private ExecutorPlus[] exec; 62 | private Semaphore workGate; 63 | private int opWorkTokens; 64 | private long opSleepNanos; 65 | private float opSleepChance; 66 | 67 | @Param({"128"}) 68 | private int threads; 69 | 70 | @Param({"1:1"}) 71 | // (in):(queued) both as multiples of thread count 72 | private String tasks; 73 | 74 | @Param({"INJECTOR"}) 75 | private String type; 76 | 77 | @Param({"0.1"}) 78 | private double opWork; 79 | 80 | @Param({"-1"}) 81 | private double opWorkRatio; 82 | 83 | @Param({"0/0"}) 84 | // (chance of sleep)/(length of sleep (us)) 85 | private String opSleep; 86 | 87 | @Param({"1"}) 88 | private int executorChainLength; 89 | 90 | @Setup 91 | public void setup() 92 | { 93 | if (opWorkRatio < 0) 94 | throw new IllegalStateException(); 95 | 96 | String[] opSleepArgs = opSleep.split("/"); 97 | opSleepChance = Float.parseFloat(opSleepArgs[0]); 98 | opSleepNanos = Long.parseLong(opSleepArgs[1]) * 1000L; 99 | opWorkTokens = (int) Math.ceil(opWork * opWorkRatio * (1d / executorChainLength)); 100 | 101 | String[] taskArgs = tasks.split(":"); 102 | int concurrentRequests = (int) (threads * Double.parseDouble(taskArgs[0])); 103 | int maxTasksQueued = (int) (threads * Double.parseDouble(taskArgs[1])); 104 | final InjectorPlus injector = new InjectorPlus(""); 105 | exec = new ExecutorPlus[executorChainLength]; 106 | workGate = new Semaphore(concurrentRequests, false); 107 | for (int i = 0 ; i < exec.length ; i++) 108 | { 109 | switch (ExecutorType.valueOf(type)) 110 | { 111 | case INJECTOR: 112 | exec[i] = injector.newExecutor(threads, maxTasksQueued); 113 | break; 114 | case JDK: 115 | exec[i] = new BlockingThreadPoolExecutor(threads, maxTasksQueued); 116 | break; 117 | case FJP: 118 | exec[i] = new BlockingForkJoinPool(threads, maxTasksQueued); 119 | break; 120 | case DISRUPTOR_SPIN: 121 | exec[i] = new DisruptorExecutor(threads, maxTasksQueued, new BusySpinWaitStrategy()); 122 | break; 123 | case DISRUPTOR_BLOCK: 124 | exec[i] = new DisruptorExecutor(threads, maxTasksQueued, new BlockingWaitStrategy()); 125 | break; 126 | } 127 | } 128 | } 129 | 130 | private static double calcWorkRatio(long minMeasurementIntervalNanos) 131 | { 132 | int tokens = 0; 133 | long start = System.nanoTime(); 134 | long end; 135 | do 136 | { 137 | tokens += 10000; 138 | Blackhole.consumeCPU(10000); 139 | } while ((end = System.nanoTime()) - start < minMeasurementIntervalNanos); 140 | return tokens / (((double)(end - start)) / 1000); 141 | } 142 | 143 | @Benchmark 144 | public void test() 145 | { 146 | workGate.acquireUninterruptibly(); 147 | exec[0].execute(new Work(1)); 148 | } 149 | 150 | private final class Work implements Runnable 151 | { 152 | final int executorIndex; 153 | 154 | private Work(int executorIndex) 155 | { 156 | this.executorIndex = executorIndex; 157 | } 158 | 159 | public void run() 160 | { 161 | Blackhole.consumeCPU(opWorkTokens); 162 | if (executorIndex < exec.length) 163 | { 164 | exec[executorIndex].maybeExecuteInline(new Work(executorIndex + 1)); 165 | } 166 | else 167 | { 168 | if (opSleepNanos > 0 && ThreadLocalRandom.current().nextFloat() < opSleepChance) 169 | LockSupport.parkNanos(opSleepNanos); 170 | workGate.release(); 171 | } 172 | } 173 | } 174 | 175 | public static void main(String[] args) throws RunnerException, InterruptedException 176 | { 177 | boolean addPerf = false; 178 | Map jmhParams = new HashMap(); 179 | jmhParams.put("forks", 1); 180 | jmhParams.put("producerThreads", 1); 181 | jmhParams.put("warmups", 5); 182 | jmhParams.put("warmupLength", 1); 183 | jmhParams.put("measurements", 5); 184 | jmhParams.put("measurementLength", 2); 185 | Map benchParams = new LinkedHashMap(); 186 | benchParams.put("opSleep", new String[] { "0/0" }); 187 | benchParams.put("opWork", new String[] { "1", "10", "100" }); 188 | benchParams.put("tasks", new String[] { "0.5:1", "1:1", "4:1", "4:4" }); 189 | benchParams.put("type", new String[] { "INJECTOR", "JDK" }); 190 | benchParams.put("threads", new String[] { "32", "128", "512" }); 191 | benchParams.put("executorChainLength", new String[] { "1" }); 192 | for (String arg : args) 193 | { 194 | if (arg.equals("-perf")) 195 | { 196 | addPerf = true; 197 | continue; 198 | } 199 | String[] split = arg.split("="); 200 | if (split.length != 2) 201 | throw new IllegalArgumentException(arg + " malformed"); 202 | if (jmhParams.containsKey(split[0])) 203 | jmhParams.put(split[0], Integer.parseInt(split[1])); 204 | else if (benchParams.containsKey(split[0])) 205 | benchParams.put(split[0], split[1].split(",")); 206 | else 207 | throw new IllegalArgumentException(arg + " unknown property"); 208 | } 209 | 210 | double workRatio = calcWorkRatio(TimeUnit.SECONDS.toNanos(1)); 211 | 212 | ChainedOptionsBuilder builder = new OptionsBuilder() 213 | .include(".*ExecutorBenchmark.*") 214 | .forks(jmhParams.get("forks")) 215 | .threads(jmhParams.get("producerThreads")) 216 | .warmupIterations(jmhParams.get("warmups")) 217 | .warmupTime(TimeValue.seconds(jmhParams.get("warmupLength"))) 218 | .measurementIterations(jmhParams.get("measurements")) 219 | .measurementTime(TimeValue.seconds(jmhParams.get("measurementLength"))) 220 | .jvmArgs("-dsa", "-da") 221 | .param("opWorkRatio", String.format("%.3f", workRatio)); 222 | 223 | if (addPerf) 224 | builder.addProfiler(org.openjdk.jmh.profile.LinuxPerfProfiler.class); 225 | 226 | System.out.println("Running with:"); 227 | System.out.println(jmhParams); 228 | for (Map.Entry e : benchParams.entrySet()) 229 | { 230 | System.out.println(e.getKey() + ": " + Arrays.toString(e.getValue())); 231 | builder.param(e.getKey(), e.getValue()); 232 | } 233 | new Runner(builder.build()).run(); 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/test/java/bes/injector/InjectorBurnTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://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 | package bes.injector; 19 | 20 | import java.util.BitSet; 21 | import java.util.TreeSet; 22 | import java.util.concurrent.ExecutionException; 23 | import java.util.concurrent.ExecutorService; 24 | import java.util.concurrent.Future; 25 | import java.util.concurrent.TimeUnit; 26 | import java.util.concurrent.TimeoutException; 27 | import java.util.concurrent.locks.LockSupport; 28 | 29 | import org.apache.commons.math3.distribution.WeibullDistribution; 30 | import org.junit.Test; 31 | 32 | public class InjectorBurnTest 33 | { 34 | 35 | private static final class WaitTask implements Runnable 36 | { 37 | final long nanos; 38 | 39 | private WaitTask(long nanos) 40 | { 41 | this.nanos = nanos; 42 | } 43 | 44 | public void run() 45 | { 46 | LockSupport.parkNanos(nanos); 47 | } 48 | } 49 | 50 | private static final class Result implements Comparable 51 | { 52 | final Future future; 53 | final long forecastedCompletion; 54 | 55 | private Result(Future future, long forecastedCompletion) 56 | { 57 | this.future = future; 58 | this.forecastedCompletion = forecastedCompletion; 59 | } 60 | 61 | public int compareTo(Result that) 62 | { 63 | int c = Long.compare(this.forecastedCompletion, that.forecastedCompletion); 64 | if (c != 0) 65 | return c; 66 | c = Integer.compare(this.hashCode(), that.hashCode()); 67 | if (c != 0) 68 | return c; 69 | return Integer.compare(this.future.hashCode(), that.future.hashCode()); 70 | } 71 | } 72 | 73 | private static final class Batch implements Comparable 74 | { 75 | final TreeSet results; 76 | final long timeout; 77 | final int executorIndex; 78 | 79 | private Batch(TreeSet results, long timeout, int executorIndex) 80 | { 81 | this.results = results; 82 | this.timeout = timeout; 83 | this.executorIndex = executorIndex; 84 | } 85 | 86 | public int compareTo(Batch that) 87 | { 88 | int c = Long.compare(this.timeout, that.timeout); 89 | if (c != 0) 90 | return c; 91 | c = Integer.compare(this.results.size(), that.results.size()); 92 | if (c != 0) 93 | return c; 94 | return Integer.compare(this.hashCode(), that.hashCode()); 95 | } 96 | } 97 | 98 | @Test 99 | public void testPromptnessOfExecution() throws InterruptedException, ExecutionException, TimeoutException 100 | { 101 | testPromptnessOfExecution(TimeUnit.MINUTES.toNanos(2L), 0.5f); 102 | } 103 | 104 | private void testPromptnessOfExecution(long intervalNanos, float loadIncrement) throws InterruptedException, ExecutionException, TimeoutException 105 | { 106 | final int executorCount = 4; 107 | int threadCount = 8; 108 | int maxQueued = 1024; 109 | final WeibullDistribution workTime = new WeibullDistribution(3, 200000); 110 | final long minWorkTime = TimeUnit.MICROSECONDS.toNanos(1); 111 | final long maxWorkTime = TimeUnit.MILLISECONDS.toNanos(1); 112 | 113 | final int[] threadCounts = new int[executorCount]; 114 | final WeibullDistribution[] workCount = new WeibullDistribution[executorCount]; 115 | final ExecutorService[] executors = new ExecutorService[executorCount]; 116 | final Injector injector = new Injector(""); 117 | for (int i = 0 ; i < executors.length ; i++) 118 | { 119 | executors[i] = injector.newExecutor(threadCount, maxQueued); 120 | threadCounts[i] = threadCount; 121 | workCount[i] = new WeibullDistribution(2, maxQueued); 122 | threadCount *= 2; 123 | maxQueued *= 2; 124 | } 125 | 126 | long runs = 0; 127 | long events = 0; 128 | final TreeSet pending = new TreeSet(); 129 | final BitSet executorsWithWork = new BitSet(executorCount); 130 | long until = 0; 131 | // basic idea is to go through different levels of load on the executor service; initially is all small batches 132 | // (mostly within max queue size) of very short operations, moving to progressively larger batches 133 | // (beyond max queued size), and longer operations 134 | for (float multiplier = 0f ; multiplier < 2.01f ; ) 135 | { 136 | if (System.nanoTime() > until) 137 | { 138 | System.out.println(String.format("Completed %.0fK batches with %.1fM events", runs * 0.001f, events * 0.000001f)); 139 | events = 0; 140 | until = System.nanoTime() + intervalNanos; 141 | multiplier += loadIncrement; 142 | System.out.println(String.format("Running for %ds with load multiplier %.1f", TimeUnit.NANOSECONDS.toSeconds(intervalNanos), multiplier)); 143 | } 144 | 145 | // wait a random amount of time so we submit new tasks in various stages of 146 | long timeout; 147 | if (pending.isEmpty()) timeout = 0; 148 | else if (Math.random() > 0.98) timeout = Long.MAX_VALUE; 149 | else if (pending.size() == executorCount) timeout = pending.first().timeout; 150 | else timeout = (long) (Math.random() * pending.last().timeout); 151 | 152 | while (!pending.isEmpty() && timeout > System.nanoTime()) 153 | { 154 | Batch first = pending.first(); 155 | boolean complete = false; 156 | try 157 | { 158 | for (Result result : first.results.descendingSet()) 159 | result.future.get(timeout - System.nanoTime(), TimeUnit.NANOSECONDS); 160 | complete = true; 161 | } 162 | catch (TimeoutException e) 163 | { 164 | } 165 | if (!complete && System.nanoTime() > first.timeout) 166 | { 167 | for (Result result : first.results) 168 | if (!result.future.isDone()) 169 | throw new AssertionError(); 170 | complete = true; 171 | } 172 | if (complete) 173 | { 174 | pending.pollFirst(); 175 | executorsWithWork.clear(first.executorIndex); 176 | } 177 | } 178 | 179 | // if we've emptied the executors, give all our threads an opportunity to spin down 180 | if (timeout == Long.MAX_VALUE) 181 | { 182 | try 183 | { 184 | Thread.sleep(10); 185 | } 186 | catch (InterruptedException e) 187 | { 188 | } 189 | } 190 | 191 | // submit a random batch to the first free executor service 192 | int executorIndex = executorsWithWork.nextClearBit(0); 193 | if (executorIndex >= executorCount) 194 | continue; 195 | executorsWithWork.set(executorIndex); 196 | ExecutorService executor = executors[executorIndex]; 197 | TreeSet results = new TreeSet(); 198 | int count = (int) (workCount[executorIndex].sample() * multiplier); 199 | long targetTotalElapsed = 0; 200 | long start = System.nanoTime(); 201 | long baseTime; 202 | if (Math.random() > 0.5) baseTime = 2 * (long) (workTime.sample() * multiplier); 203 | else baseTime = 0; 204 | for (int j = 0 ; j < count ; j++) 205 | { 206 | long time; 207 | if (baseTime == 0) time = (long) (workTime.sample() * multiplier); 208 | else time = (long) (baseTime * Math.random()); 209 | if (time < minWorkTime) 210 | time = minWorkTime; 211 | if (time > maxWorkTime) 212 | time = maxWorkTime; 213 | targetTotalElapsed += time; 214 | Future future = executor.submit(new WaitTask(time)); 215 | results.add(new Result(future, System.nanoTime() + time)); 216 | } 217 | long end = start + (long) Math.ceil(targetTotalElapsed / (double) threadCounts[executorIndex]) 218 | + TimeUnit.MILLISECONDS.toNanos(100L); 219 | long now = System.nanoTime(); 220 | if (runs++ > executorCount && now > end) 221 | throw new AssertionError(); 222 | events += results.size(); 223 | pending.add(new Batch(results, end, executorIndex)); 224 | // System.out.println(String.format("Submitted batch to executor %d with %d items and %d permitted millis", executorIndex, count, TimeUnit.NANOSECONDS.toMillis(end - start))); 225 | } 226 | } 227 | 228 | public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException 229 | { 230 | // do longer test 231 | new InjectorBurnTest().testPromptnessOfExecution(TimeUnit.HOURS.toNanos(2L), 0.1f); 232 | } 233 | 234 | } 235 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/main/java/bes/injector/InjectionExecutor.java: -------------------------------------------------------------------------------- 1 | package bes.injector;/* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://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 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.Queue; 22 | import java.util.concurrent.*; 23 | import java.util.concurrent.atomic.AtomicInteger; 24 | import java.util.concurrent.atomic.AtomicLong; 25 | import java.util.concurrent.locks.LockSupport; 26 | 27 | import bes.concurrent.WaitQueue; 28 | 29 | public class InjectionExecutor extends AbstractExecutorService 30 | { 31 | 32 | /** 33 | * Called directly before the task is executed. 34 | * This method should ensure no exceptions can be thrown during its execution. 35 | */ 36 | protected void beforeExecute(Runnable task) { } 37 | 38 | /** 39 | * Called after the task has been executed, successfully or not. 40 | * This method should ensure no exceptions can be thrown during its execution. 41 | * 42 | * @param task the task that was executed 43 | * @param failure null if success; otherwise the exception that caused the task to fail 44 | */ 45 | protected void afterExecute(Runnable task, Throwable failure) { } 46 | 47 | // non-final so this class can be extended by users, whilst not exposing this internal detail 48 | Injector injector; 49 | 50 | private final int maxWorkers; 51 | private final int maxTasksQueued; 52 | 53 | // stores both a set of work permits and task permits: 54 | // bottom 48 bits are number of queued tasks, in the range [0..maxTasksQueued] (initially 0) 55 | // top 16 bits are number of work permits available in the range [0..maxWorkers] (initially maxWorkers) 56 | private final AtomicLong permits = new AtomicLong(); 57 | 58 | final Queue tasks; 59 | final Work asWork = new Work(this); 60 | 61 | // producers wait on this when there is no room on the queue 62 | private final WaitQueue hasRoom = new WaitQueue(); 63 | private final AtomicLong totalBlocked = new AtomicLong(); 64 | private final AtomicInteger currentlyBlocked = new AtomicInteger(); 65 | 66 | private final CountDownLatch terminated = new CountDownLatch(1); 67 | private volatile boolean shutdown = false; 68 | 69 | protected InjectionExecutor(int maxWorkers, int maxTasksQueued) 70 | { 71 | this(maxWorkers, maxTasksQueued, new ConcurrentLinkedQueue()); 72 | } 73 | protected InjectionExecutor(int maxWorkers, int maxTasksQueued, Queue tasks) 74 | { 75 | if (maxWorkers >= 1 << 16) 76 | throw new IllegalArgumentException("Unsupported thread count: max is 65535"); 77 | this.maxWorkers = maxWorkers; 78 | this.maxTasksQueued = maxTasksQueued; 79 | this.permits.set(maxWorkers * WORK_PERMIT); 80 | this.tasks = tasks; 81 | } 82 | 83 | // schedules another worker for this injector if there is work outstanding and there are no spinning threads that 84 | // will self-assign to it in the immediate future 85 | boolean maybeSchedule(boolean internal) 86 | { 87 | if (injector.spinningCount.get() > 0 || !takeWorkPermit(true)) 88 | return false; 89 | 90 | injector.schedule(asWork, internal); 91 | return true; 92 | } 93 | 94 | /** 95 | * Execute the provided Runnable as soon as a worker becomes available. The task may be executed 96 | * on any of the Injector's shared worker threads, however the caller will not typically schedule 97 | * the thread, only doing so if there is currently no active worker capable of promptly serving it 98 | * in the injector, or this executor's task queue is full. 99 | * 100 | * @param task the task to execute 101 | */ 102 | public void execute(Runnable task) 103 | { 104 | if (task == null) 105 | throw new NullPointerException(); 106 | 107 | // we check the shutdown status initially to ensure we never accept a task submitted after shutdown() 108 | // or shutdownNow() have exited. otherwise, if we are not terminated, a worker could grab it from the queue 109 | // before we repair after the fact 110 | if (shutdown) 111 | throw new RejectedExecutionException(); 112 | 113 | // we add to the queue first, so that when a worker takes a task permit it can be certain there is a task available 114 | // this permits us to schedule threads non-spuriously; it also means work is serviced fairly 115 | tasks.add(task); 116 | 117 | /** 118 | * we recheck the shutdown status after adding to the queue, to ensure we haven't raced. 119 | * if we are shutdown, and we fail to remove ourselves, we were added out of order with another 120 | * task that successfully incremented the task permits before the shutdown flag was set _and_ 121 | * we have already been dequeued by a worker; in this case we cannot reject the task, since it's 122 | * being (or has been) processed, but we also must honour the prior task's successful submission, 123 | * so we have to increment our permit count 124 | */ 125 | if (shutdown && tasks.remove(task)) 126 | throw new RejectedExecutionException(); 127 | 128 | long update; 129 | while (true) 130 | { 131 | long current = this.permits.get(); 132 | update = current + 1; 133 | 134 | // because there is no difference in practical terms between the work permit being added or not 135 | // (the work is already in existence), we always add our permit, but block after the fact if we 136 | // breached the queue limit 137 | if (permits.compareAndSet(current, update)) 138 | break; 139 | } 140 | 141 | if (taskPermits(update) == 1) 142 | { 143 | // we only need to schedule a thread if there are no tasks already waiting to be processed, as the prior 144 | // enqueue that moved the permit count from zero will have already started a spinning worker (if necessary), 145 | // and spinning workers multiply if they encounter work, and only spin down if there is no work available 146 | injector.maybeStartSpinningWorker(false); 147 | } 148 | // we consider any available work permits to count towards our queue limit, since the resource use is the same; 149 | // the work can be considered allocated to each of the available permits, and effectively 'not queued' 150 | else if (taskPermits(update) > maxTasksQueued + workPermits(update)) 151 | { 152 | // register to receive a signal once a task is processed bringing the queue below its threshold 153 | WaitQueue.Signal s = hasRoom.register(); 154 | 155 | // we will only be signalled once the queue drops below full, so this creates equivalent external behaviour 156 | // however the advantage is that we never wake-up spuriously 157 | long latest = permits.get(); 158 | if (taskPermits(latest) > maxTasksQueued + workPermits(latest)) 159 | { 160 | // if we're blocking, we might as well directly schedule a worker if we aren't already at max 161 | if (takeWorkPermit(true)) 162 | injector.schedule(asWork, false); 163 | totalBlocked.incrementAndGet(); 164 | currentlyBlocked.incrementAndGet(); 165 | s.awaitUninterruptibly(); 166 | currentlyBlocked.decrementAndGet(); 167 | } 168 | else // don't propagate our signal when we cancel, just cancel 169 | s.cancel(); 170 | } 171 | } 172 | 173 | /** 174 | * takes permission to perform a task, if any are available; once taken it is guaranteed 175 | * that a proceeding call to tasks.poll() will return some work 176 | */ 177 | boolean takeTaskPermit() 178 | { 179 | while (true) 180 | { 181 | long current = permits.get(); 182 | long update = current - 1; 183 | 184 | if (taskPermits(current) == 0) 185 | return false; 186 | 187 | if (permits.compareAndSet(current, update)) 188 | { 189 | if (taskPermits(update) == maxTasksQueued && hasRoom.hasWaiters()) 190 | hasRoom.signalAll(); 191 | return true; 192 | } 193 | } 194 | } 195 | 196 | // takes a work permit and (optionally) a task permit simultaneously; if one of the two is unavailable, returns false 197 | boolean takeWorkPermit(boolean takeTaskPermit) 198 | { 199 | long delta = WORK_PERMIT + (takeTaskPermit ? 1 : 0); 200 | while (true) 201 | { 202 | long current = permits.get(); 203 | if ((workPermits(current) == 0) | (taskPermits(current) == 0)) 204 | return false; 205 | long update = current - delta; 206 | if (permits.compareAndSet(current, update)) 207 | { 208 | if (takeTaskPermit && taskPermits(update) == maxTasksQueued && hasRoom.hasWaiters()) 209 | hasRoom.signalAll(); 210 | return true; 211 | } 212 | } 213 | } 214 | 215 | boolean hasTasks() 216 | { 217 | return taskPermits(permits.get()) > 0; 218 | } 219 | 220 | // gives up a work permit 221 | void returnWorkPermit() 222 | { 223 | while (true) 224 | { 225 | long current = permits.get(); 226 | long update = current + WORK_PERMIT; 227 | if (permits.compareAndSet(current, update)) 228 | return; 229 | } 230 | } 231 | 232 | // only to be called on encountering an *unexpected* exception (i.e. bug, bad VM state, OOM, etc.) 233 | void returnTaskPermit() 234 | { 235 | while (true) 236 | { 237 | long current = permits.get(); 238 | long update = current + 1; 239 | if (permits.compareAndSet(current, update)) 240 | return; 241 | } 242 | } 243 | 244 | /** 245 | * The calling thread, if there is a work permit available, temporarily becomes a worker 246 | * in this pool and executes the task inline. If there are no work permits available, 247 | * this call behaves like a normal invocation of execute(), placing the task on the queue 248 | * and letting one of the pool's workers handle it when available. 249 | * 250 | * @param task the task to execute 251 | */ 252 | public void maybeExecuteInline(Runnable task) 253 | { 254 | if (!takeWorkPermit(false)) 255 | { 256 | execute(task); 257 | } 258 | else 259 | { 260 | try 261 | { 262 | executeInternal(task); 263 | } 264 | finally 265 | { 266 | returnWorkPermit(); 267 | maybeSchedule(false); 268 | } 269 | } 270 | } 271 | 272 | void executeInternal(Runnable task) 273 | { 274 | Throwable failure = null; 275 | try 276 | { 277 | beforeExecute(task); 278 | task.run(); 279 | } 280 | catch (Throwable t) 281 | { 282 | failure = t; 283 | } 284 | afterExecute(task, failure); 285 | } 286 | 287 | /** 288 | * Stops further tasks from being submitted to the executor. 289 | * This is only guaranteed to stop submissions that were initiated after this call exits; 290 | * any execute() call entered even fractionally before this has a chance of being submitted 291 | * for processing after we exit. 292 | */ 293 | public synchronized void shutdown() 294 | { 295 | shutdown = true; 296 | maybeTerminate(); 297 | } 298 | 299 | /** 300 | * Stops further tasks from being submitted to the executor. 301 | * This is only guaranteed to stop submissions that were initiated after this call exits; 302 | * any execute() call entered even fractionally before this has a chance of being submitted 303 | * for processing after we exit. 304 | * 305 | * @return the tasks that were prevented from executing 306 | */ 307 | public synchronized List shutdownNow() 308 | { 309 | shutdown = true; 310 | List aborted = new ArrayList<>(); 311 | // we busy-spin trying to take a permit if the queue is non-empty, to reduce the window 312 | // in which an execution may be submitted after this call exits successfully 313 | while (!tasks.isEmpty()) 314 | while (takeTaskPermit()) 315 | aborted.add(tasks.poll()); 316 | maybeTerminate(); 317 | return aborted; 318 | } 319 | 320 | // once shutdown = true, this is called after any state change that might 321 | // result in the executor having terminated and, if so, signals termination 322 | void maybeTerminate() 323 | { 324 | while (true) 325 | { 326 | // we read tasks.isEmpty() before reading our permit state; 327 | // the alternative ordering permits a permit to be added 328 | // and a task to be dequeued, and for us not to notice 329 | boolean safeToTerminate = tasks.isEmpty(); 330 | long permits = this.permits.get(); 331 | 332 | // once we see a state with an empty task queue, we know we are _safe_ to terminate 333 | // because all execute() calls that insert to an empty queue after the shutdown flag 334 | // is set are guaranteed to be able to remove themselves without their task executing 335 | 336 | // the only question is: _are_ we terminated? this answers that 337 | if (taskPermits(permits) > 0 || workPermits(permits) < maxWorkers) 338 | return; 339 | 340 | if (safeToTerminate) 341 | { 342 | injector.executors.remove(this); 343 | terminated.countDown(); 344 | return; 345 | } 346 | 347 | // however if we appear to be terminated, but don't have an empty task queue, 348 | // the termination state will resolve shortly but right now cannot be determined. 349 | // so we suspend a brief period to let any threads that raced on execute() to repair 350 | LockSupport.parkNanos(10000); 351 | } 352 | } 353 | 354 | public boolean isShutdown() 355 | { 356 | return shutdown; 357 | } 358 | 359 | public boolean isTerminated() 360 | { 361 | return terminated.getCount() == 0; 362 | } 363 | 364 | public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException 365 | { 366 | terminated.await(timeout, unit); 367 | return isTerminated(); 368 | } 369 | 370 | public long getPendingTasks() 371 | { 372 | return Math.max(0, taskPermits(permits.get())); 373 | } 374 | 375 | public int getActiveTasks() 376 | { 377 | return maxWorkers - (int) workPermits(permits.get()); 378 | } 379 | 380 | public long getAllTimeBlockedProducers() 381 | { 382 | return totalBlocked.get(); 383 | } 384 | 385 | public int getCurrentlyBlockedProducers() 386 | { 387 | return currentlyBlocked.get(); 388 | } 389 | 390 | public int getMaxThreads() 391 | { 392 | return maxWorkers; 393 | } 394 | 395 | private static long WORK_PERMIT = 1L << 48; 396 | private static long TASK_BITMASK = -1L >>> 16; 397 | 398 | private static long taskPermits(long permits) 399 | { 400 | return permits & TASK_BITMASK; 401 | } 402 | 403 | private static long workPermits(long permits) 404 | { 405 | return permits >>> 48; 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /src/main/java/bes/injector/Worker.java: -------------------------------------------------------------------------------- 1 | package bes.injector;/* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://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 | 19 | import java.util.concurrent.ThreadFactory; 20 | import java.util.concurrent.ThreadLocalRandom; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.concurrent.atomic.AtomicReference; 23 | import java.util.concurrent.locks.LockSupport; 24 | 25 | final class Worker extends AtomicReference implements Runnable 26 | { 27 | private static final long TARGET_SLEEP_NANOS = TimeUnit.MICROSECONDS.toNanos(10L); 28 | private static final long MAX_SLEEP_NANOS = TimeUnit.MILLISECONDS.toNanos(1L); 29 | 30 | final Long workerId; 31 | final Thread thread; 32 | final Injector injector; 33 | 34 | // prevStopCheck stores the value of injector.stopCheck after we last incremented it; if it hasn't changed, 35 | // we know nobody else was spinning in the interval, so we increment our soleSpinnerSpinTime accordingly, 36 | // and otherwise we set it to zero; this is then used to terminate the final spinning thread, as the coordinated 37 | // strategy can only work when there are multiple threads spinning (as more sleep time must elapse than real time) 38 | long prevStopCheck = 0; 39 | long soleSpinnerSpinTime = 0; 40 | 41 | Worker(Long workerId, Work initialState, Injector injector, ThreadFactory threadFactory) 42 | { 43 | this.injector = injector; 44 | this.workerId = workerId; 45 | thread = threadFactory.newThread(this); 46 | set(initialState); 47 | thread.start(); 48 | } 49 | 50 | public void run() 51 | { 52 | while (true) 53 | { 54 | /** 55 | * we maintain two important invariants: 56 | * 1) after exiting spinning phase, we ensure at least one more task on _each_ queue will be processed 57 | * promptly after we begin, assuming any are outstanding on any pools. this is to permit producers to 58 | * avoid signalling if there are _any_ spinning threads. we achieve this by simply calling 59 | * maybeSchedule() on each queue if, on decrementing the spin counter, we hit zero. 60 | * 2) before processing a task on a given queue, we attempt to assign another worker to the _same queue 61 | * only_; this allows a producer to skip signalling work if there are task permits already available, 62 | * and in conjunction with invariant (1) ensures that if any thread was spinning when a task was added 63 | * to any executor, that task will be processed immediately if work permits are available 64 | */ 65 | 66 | InjectionExecutor assigned = null; 67 | Runnable task = null; 68 | try 69 | { 70 | while (true) 71 | { 72 | if (isSpinning() && !selfAssign()) 73 | { 74 | doWaitSpin(); 75 | continue; 76 | } 77 | 78 | // if stop was signalled, go to sleep (don't try self-assign; being put to sleep is rare and means 79 | // we're over capacity, so let's obey it whenever we receive it. we don't apply this constraint to 80 | // producers, who may reschedule us before we go to sleep) 81 | if (stop()) 82 | while (isStopped()) 83 | LockSupport.park(); 84 | 85 | // we can be assigned any state from STOPPED, so loop if we don't actually have any tasks assigned 86 | assigned = get().assigned; 87 | if (assigned == null) 88 | continue; 89 | 90 | // if we've been assigned, we have a task permit already taken for us, so get our task 91 | task = assigned.tasks.poll(); 92 | 93 | // once assigned nobody will change our state, so we can simply set it to WORKING 94 | // (which is also a state that will never be interrupted externally) 95 | set(Work.WORKING); 96 | 97 | while (true) 98 | { 99 | assigned.maybeSchedule(true); 100 | Runnable run = task; 101 | task = null; 102 | assigned.executeInternal(run); 103 | 104 | if (!assigned.takeTaskPermit()) 105 | break; 106 | 107 | task = assigned.tasks.poll(); 108 | } 109 | 110 | // return our work permit, and maybe signal shutdown 111 | assigned.returnWorkPermit(); 112 | if (assigned.isShutdown()) 113 | { 114 | InjectionExecutor wasAssigned = assigned; 115 | assigned = null; 116 | wasAssigned.maybeTerminate(); 117 | } 118 | assigned = null; 119 | 120 | // try to immediately reassign ourselves some work; if we fail, start spinning 121 | if (!selfAssign()) 122 | startSpinning(); 123 | } 124 | } 125 | catch (Throwable t) 126 | { 127 | 128 | /** 129 | * in general this should only happen in case of, e.g., OOM or some equivalent dangerous state, 130 | * so there's only so much we can do to ensure correctness. For safety, we let this thread die if possible. 131 | * 132 | * Even then, generally it should not be possible for exceptions to be thrown anywhere between adopting 133 | * a task and attempting to execute it (or beforeExecute()), unless the VM state is badly corrupted, 134 | * so we are only really ensuring our wider injector state is correct, and that work cannot be assigned to us. 135 | * 136 | * In this event we don't care if a waiting task may not be served _promptly_ (i.e. may have to wait for 137 | * another thread to complete its current work before serving the rest of the queue), only that it is served 138 | * eventually, assuming those other threads aren't stuck. If we are the last worker, we try to continue 139 | * where we left off, as otherwise tasks may be left unprocessed. 140 | */ 141 | 142 | Work state = getAndSet(Work.DEAD); 143 | if (assigned != null) 144 | { 145 | assigned.returnWorkPermit(); 146 | if (task != null) 147 | { 148 | assigned.tasks.add(task); 149 | state.assigned.returnTaskPermit(); 150 | } 151 | } 152 | else if (state.isAssigned()) 153 | { 154 | state.assigned.returnWorkPermit(); 155 | state.assigned.returnTaskPermit(); 156 | } 157 | 158 | if (state.isSpinning()) 159 | injector.spinningCount.decrementAndGet(); 160 | 161 | boolean terminate = true; 162 | if (injector.workerCount.decrementAndGet() == 0) 163 | { 164 | // then we check if there's actually any work to do; if not, we terminate 165 | boolean hasWork = false; 166 | for (InjectionExecutor executor : injector.executors) 167 | hasWork |= executor.hasTasks(); 168 | 169 | // finally we check to see no new threads have been started 170 | if (hasWork && injector.workerCount.compareAndSet(0, 1)) 171 | { 172 | set(Work.SPINNING); 173 | injector.spinningCount.incrementAndGet(); 174 | terminate = false; 175 | } 176 | } 177 | 178 | Thread thread = Thread.currentThread(); 179 | Thread.UncaughtExceptionHandler handler = thread.getUncaughtExceptionHandler(); 180 | if (handler != null) 181 | handler.uncaughtException(thread, t); 182 | 183 | if (terminate) 184 | break; 185 | } 186 | } 187 | } 188 | 189 | // try to assign this worker the provided work 190 | // valid states to assign are SPINNING, STOP_SIGNALLED, (ASSIGNED); 191 | // restores invariants of the various states (i.e. spinningCount, descheduled collection and thread park status) 192 | boolean assign(Work work, boolean self) 193 | { 194 | Work state = get(); 195 | while (state.canAssign(self)) 196 | { 197 | if (!compareAndSet(state, work)) 198 | { 199 | state = get(); 200 | continue; 201 | } 202 | // if we were spinning, exit the state (decrement the count); this is valid even if we are already spinning, 203 | // as the assigning thread will have incremented the spinningCount 204 | if (state.isSpinning()) 205 | stopSpinning(); 206 | 207 | // if we're being descheduled, place ourselves in the descheduled collection 208 | if (work.isStop()) 209 | injector.descheduled.put(workerId, this); 210 | 211 | // if we're currently stopped, and the new state is not a stop signal 212 | // (which we can immediately convert to stopped), unpark the worker 213 | if (state.isStopped() && (!work.isStop() || !stop())) 214 | LockSupport.unpark(thread); 215 | return true; 216 | } 217 | return false; 218 | } 219 | 220 | // try to assign ourselves an executor with work available 221 | private boolean selfAssign() 222 | { 223 | // if we aren't permitted to assign in this state, fail 224 | if (!get().canAssign(true)) 225 | return false; 226 | for (InjectionExecutor exec : injector.executors) 227 | { 228 | if (exec.takeWorkPermit(true)) 229 | { 230 | Work work = exec.asWork; 231 | // we successfully started work on this executor, so we must either assign it to ourselves or ... 232 | if (assign(work, true)) 233 | return true; 234 | // ... if we fail, schedule it to another worker 235 | injector.schedule(work, true); 236 | // and return success as we must have already been assigned a task 237 | assert get().isAssigned(); 238 | return true; 239 | } 240 | } 241 | return false; 242 | } 243 | 244 | // we can only call this when our state is WORKING, and no other thread may change our state in this case; 245 | // so in this case only we do not need to CAS. We increment the spinningCount and add ourselves to the spinning 246 | // collection at the same time 247 | private void startSpinning() 248 | { 249 | assert get() == Work.WORKING; 250 | injector.spinningCount.incrementAndGet(); 251 | set(Work.SPINNING); 252 | } 253 | 254 | // exit the spinning state; if there are no remaining spinners, we immediately try and schedule work for all executors 255 | // so that any producer is safe to not spin up a worker when they see a spinning thread (invariant (1) above) 256 | private void stopSpinning() 257 | { 258 | if (injector.spinningCount.decrementAndGet() == 0) 259 | for (InjectionExecutor executor : injector.executors) 260 | executor.maybeSchedule(true); 261 | prevStopCheck = soleSpinnerSpinTime = 0; 262 | } 263 | 264 | // perform a sleep-spin, incrementing injector.stopCheck accordingly 265 | private void doWaitSpin() 266 | { 267 | // pick a random sleep interval based on the number of threads spinning, so that 268 | // we should always have a thread about to wake up, but most threads are sleeping 269 | long sleep, sleepMax; 270 | sleepMax = TARGET_SLEEP_NANOS * injector.spinningCount.get(); 271 | sleepMax = Math.min(MAX_SLEEP_NANOS, sleepMax); 272 | if (TARGET_SLEEP_NANOS >= sleepMax) 273 | sleep = TARGET_SLEEP_NANOS; 274 | else 275 | sleep = ThreadLocalRandom.current().nextLong(TARGET_SLEEP_NANOS, sleepMax); 276 | 277 | long start = System.nanoTime(); 278 | // place ourselves in the spinning collection; if we clash with another thread just exit 279 | Long targetWakeup = start + sleep; 280 | 281 | if (injector.spinning.putIfAbsent(targetWakeup, this) != null) 282 | return; 283 | 284 | LockSupport.parkNanos(sleep); 285 | 286 | // remove ourselves (if haven't been already) - we should be at or near the front, so should be cheap-ish 287 | injector.spinning.remove(targetWakeup, this); 288 | 289 | long end = System.nanoTime(); 290 | long spin = end - start; 291 | long stopCheck = injector.stopCheck.addAndGet(spin); 292 | maybeStop(stopCheck, end); 293 | if (prevStopCheck + spin == stopCheck) 294 | soleSpinnerSpinTime += spin; 295 | else 296 | soleSpinnerSpinTime = 0; 297 | prevStopCheck = stopCheck; 298 | } 299 | 300 | private static final long stopCheckInterval = TimeUnit.MILLISECONDS.toNanos(10L); 301 | 302 | // stops a worker if elapsed real time is less than elapsed spin time, as this implies the equivalent of 303 | // at least one worker achieved nothing in the interval. we achieve this by maintaining a stopCheck which 304 | // is initialised to a negative offset from realtime; as we spin we add to this value, and if we ever exceed 305 | // realtime we have spun too much and deschedule; if we get too far behind realtime, we reset to our initial offset 306 | private void maybeStop(long stopCheck, long now) 307 | { 308 | long delta = now - stopCheck; 309 | if (delta <= 0) 310 | { 311 | // if stopCheck has caught up with present, we've been spinning too much, so if we can atomically 312 | // set it to the past again, we should stop a worker 313 | if (injector.stopCheck.compareAndSet(stopCheck, now - stopCheckInterval)) 314 | { 315 | // try and stop ourselves; 316 | // if we've already been assigned work, stop another worker 317 | if (!assign(Work.STOP_SIGNALLED, true)) 318 | injector.schedule(Work.STOP_SIGNALLED, true); 319 | } 320 | } 321 | else if (soleSpinnerSpinTime > stopCheckInterval && injector.spinningCount.get() == 1) 322 | { 323 | // permit self-stopping 324 | assign(Work.STOP_SIGNALLED, true); 325 | } 326 | else 327 | { 328 | // if stop check has gotten too far behind present, update it so new spins can affect it 329 | while (delta > stopCheckInterval * 2 && !injector.stopCheck.compareAndSet(stopCheck, now - stopCheckInterval)) 330 | { 331 | stopCheck = injector.stopCheck.get(); 332 | delta = now - stopCheck; 333 | } 334 | } 335 | } 336 | 337 | private boolean isSpinning() 338 | { 339 | return get().isSpinning(); 340 | } 341 | 342 | private boolean stop() 343 | { 344 | return get().isStop() && compareAndSet(Work.STOP_SIGNALLED, Work.STOPPED); 345 | } 346 | 347 | private boolean isStopped() 348 | { 349 | return get().isStopped(); 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /src/main/java/bes/concurrent/WaitQueue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one 3 | * or more contributor license agreements. See the NOTICE file 4 | * distributed with this work for additional information 5 | * regarding copyright ownership. The ASF licenses this file 6 | * to you under the Apache License, Version 2.0 (the 7 | * "License"); you may not use this file except in compliance 8 | * with the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, 13 | * software distributed under the License is distributed on an 14 | * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | * KIND, either express or implied. See the License for the 16 | * specific language governing permissions and limitations 17 | * under the License. 18 | */ 19 | package bes.concurrent; 20 | 21 | import java.util.Iterator; 22 | import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; 23 | import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; 24 | import java.util.concurrent.locks.LockSupport; 25 | 26 | /** 27 | *

A relatively easy to use utility for general purpose thread signalling.

28 | *

Usage on a thread awaiting a state change using a WaitQueue q is:

29 | *
 30 |  * {@code
 31 |  *      while (!conditionMet())
 32 |  *          Signal s = q.register();
 33 |  *              if (!conditionMet())    // or, perhaps more correctly, !conditionChanged()
 34 |  *                  s.await();
 35 |  *              else
 36 |  *                  s.cancel();
 37 |  * }
 38 |  * 
39 | * A signalling thread, AFTER changing the state, then calls q.signal() to wake up one, or q.signalAll() 40 | * to wake up all, waiting threads. 41 | *

To understand intuitively how this class works, the idea is simply that a thread, once it considers itself 42 | * incapable of making progress, registers to be awoken once that changes. Since this could have changed between 43 | * checking and registering (in which case the thread that made this change would have been unable to signal it), 44 | * it checks the condition again, sleeping only if it hasn't changed/still is not met.

45 | *

This thread synchronisation scheme has some advantages over Condition objects and Object.wait/notify in that no monitor 46 | * acquisition is necessary and, in fact, besides the actual waiting on a signal, all operations are non-blocking. 47 | * As a result consumers can never block producers, nor each other, or vice versa, from making progress. 48 | * Threads that are signalled are also put into a RUNNABLE state almost simultaneously, so they can all immediately make 49 | * progress without having to serially acquire the monitor/lock, reducing scheduler delay incurred.

50 | * 51 | *

A few notes on utilisation:

52 | *

1. A thread will only exit await() when it has been signalled, but this does not guarantee the condition has not 53 | * been altered since it was signalled, and depending on your design it is likely the outer condition will need to be 54 | * checked in a loop, though this is not always the case.

55 | *

2. Each signal is single use, so must be re-registered after each await(). This is true even if it times out.

56 | *

3. If you choose not to wait on the signal (because the condition has been met before you waited on it) 57 | * you must cancel() the signal if the signalling thread uses signal() to awake waiters; otherwise signals will be 58 | * lost. If signalAll() is used but infrequent, and register() is frequent, cancel() should still be used to prevent the 59 | * queue growing unboundedly. Similarly, if you provide a TimerContext, cancel should be used to ensure it is not erroneously 60 | * counted towards wait time.

61 | *

4. Care must be taken when selecting conditionMet() to ensure we are waiting on the condition that actually 62 | * indicates progress is possible. In some complex cases it may be tempting to wait on a condition that is only indicative 63 | * of local progress, not progress on the task we are aiming to complete, and a race may leave us waiting for a condition 64 | * to be met that we no longer need. 65 | *

5. This scheme is not fair

66 | *

6. Only the thread that calls register() may call await()

67 | */ 68 | public final class WaitQueue 69 | { 70 | 71 | private static final int CANCELLED = -1; 72 | private static final int SIGNALLED = 1; 73 | private static final int NOT_SET = 0; 74 | 75 | private volatile RegisteredSignal head, tail; 76 | 77 | public WaitQueue() 78 | { 79 | head = tail = new RegisteredSignal(); 80 | head.cancel(); 81 | } 82 | 83 | /** 84 | * The calling thread MUST be the thread that uses the signal 85 | * @return x 86 | */ 87 | public Signal register() 88 | { 89 | RegisteredSignal insert = new RegisteredSignal(); 90 | RegisteredSignal tail = this.tail; 91 | while (true) 92 | { 93 | RegisteredSignal next = tail.next; 94 | if (next != null) 95 | { 96 | tail = next; 97 | } 98 | else 99 | { 100 | insert.prev = tail; 101 | if (nextUpdater.compareAndSet(tail, null, insert)) 102 | { 103 | // no need for atomicity; if updates are out of order, chain simply needs to be walked forwards 104 | this.tail = insert; 105 | return insert; 106 | } 107 | else 108 | { 109 | tail = tail.next; 110 | } 111 | } 112 | } 113 | } 114 | 115 | /** 116 | * Signal one waiting thread 117 | */ 118 | public boolean signal() 119 | { 120 | RegisteredSignal ph = head, h = ph; 121 | while (true) 122 | { 123 | RegisteredSignal n = h.next; 124 | // we set prev to null simply to help out gc 125 | if (n == null || n.signal() != null) 126 | { 127 | // no need for atomicity; if update is slow, head is simply in the past and will have to be walked forwards 128 | if (ph != h) 129 | head = h; 130 | return n != null; 131 | } 132 | prevUpdater.lazySet(n, null); 133 | h = n; 134 | } 135 | } 136 | 137 | public boolean hasWaiters() 138 | { 139 | RegisteredSignal ph = head, h = ph; 140 | while (true) 141 | { 142 | RegisteredSignal n = h.next; 143 | if (n == null || !n.isSet()) 144 | { 145 | if (ph != h) 146 | head = h; 147 | return n != null; 148 | } 149 | prevUpdater.lazySet(n, null); 150 | h = n; 151 | } 152 | } 153 | 154 | /** 155 | * Signal all waiting threads 156 | */ 157 | public void signalAll() 158 | { 159 | RegisteredSignal h = head; 160 | RegisteredSignal t = tail; 161 | while (true) 162 | { 163 | if (h == t) 164 | return; 165 | RegisteredSignal n = t.next; 166 | if (n == null) 167 | break; 168 | prevUpdater.lazySet(n, null); 169 | t = n; 170 | } 171 | head = t; 172 | while (h != t) 173 | { 174 | RegisteredSignal n = h.next; 175 | n.signal(); 176 | h = n; 177 | } 178 | } 179 | 180 | /** 181 | * A Signal is a one-time-use mechanism for a thread to wait for notification that some condition 182 | * state has transitioned that it may be interested in (and hence should check if it is). 183 | * It is potentially transient, i.e. the state can change in the meantime, it only indicates 184 | * that it should be checked, not necessarily anything about what the expected state should be. 185 | * 186 | * Signal implementations should never wake up spuriously, they are always woken up by a 187 | * signal() or signalAll(). 188 | * 189 | * This abstract definition of Signal does not need to be tied to a WaitQueue. 190 | * Whilst RegisteredSignal is the main building block of Signals, this abstract 191 | * definition allows us to compose Signals in useful ways. The Signal is 'owned' by the 192 | * thread that registered itself with WaitQueue(s) to obtain the underlying RegisteredSignal(s); 193 | * only the owning thread should use a Signal. 194 | */ 195 | public static interface Signal 196 | { 197 | 198 | /** 199 | * @return true if signalled; once true, must be discarded by the owning thread. 200 | */ 201 | public boolean isSignalled(); 202 | 203 | /** 204 | * atomically: cancels the Signal if !isSet(), or returns true if isSignalled() 205 | * 206 | * @return true if isSignalled() 207 | */ 208 | public boolean checkAndClear(); 209 | 210 | /** 211 | * Should only be called by the owning thread. Indicates the signal can be retired, 212 | * and if signalled propagates the signal to another waiting thread 213 | */ 214 | public abstract void cancel(); 215 | 216 | /** 217 | * Wait, without throwing InterruptedException, until signalled. On exit isSignalled() must be true. 218 | * If the thread is interrupted in the meantime, the interrupted flag will be set. 219 | */ 220 | public void awaitUninterruptibly(); 221 | 222 | /** 223 | * Wait until signalled, or throw an InterruptedException if interrupted before this happens. 224 | * On normal exit isSignalled() must be true; however if InterruptedException is thrown isCancelled() 225 | * will be true. 226 | * @throws InterruptedException 227 | */ 228 | public void await() throws InterruptedException; 229 | 230 | /** 231 | * Wait until signalled, or the provided time is reached, or the thread is interrupted. If signalled, 232 | * isSignalled() will be true on exit, and the method will return true; if timedout, the method will return 233 | * false and isCancelled() will be true; if interrupted an InterruptedException will be thrown and isCancelled() 234 | * will be true. 235 | * @param nanos System.nanoTime() to wait until 236 | * @return true if signalled, false if timed out 237 | * @throws InterruptedException 238 | */ 239 | public boolean awaitUntil(long nanos) throws InterruptedException; 240 | } 241 | 242 | /** 243 | * An abstract signal implementation 244 | */ 245 | public static abstract class AbstractSignal implements Signal 246 | { 247 | public void awaitUninterruptibly() 248 | { 249 | boolean interrupted = false; 250 | while (!isSignalled()) 251 | { 252 | if (Thread.interrupted()) 253 | interrupted = true; 254 | LockSupport.park(); 255 | } 256 | if (interrupted) 257 | Thread.currentThread().interrupt(); 258 | checkAndClear(); 259 | } 260 | 261 | public void await() throws InterruptedException 262 | { 263 | while (!isSignalled()) 264 | { 265 | checkInterrupted(); 266 | LockSupport.park(); 267 | } 268 | checkAndClear(); 269 | } 270 | 271 | public boolean awaitUntil(long until) throws InterruptedException 272 | { 273 | long now; 274 | while (until > (now = System.nanoTime()) && !isSignalled()) 275 | { 276 | checkInterrupted(); 277 | long delta = until - now; 278 | LockSupport.parkNanos(delta); 279 | } 280 | return checkAndClear(); 281 | } 282 | 283 | private void checkInterrupted() throws InterruptedException 284 | { 285 | if (Thread.interrupted()) 286 | { 287 | cancel(); 288 | throw new InterruptedException(); 289 | } 290 | } 291 | } 292 | 293 | /** 294 | * A signal registered with this WaitQueue 295 | */ 296 | final class RegisteredSignal extends AbstractSignal 297 | { 298 | volatile Thread thread = Thread.currentThread(); 299 | volatile int state; 300 | volatile RegisteredSignal next, prev; 301 | 302 | public boolean isSignalled() 303 | { 304 | return state == SIGNALLED; 305 | } 306 | 307 | public boolean isCancelled() 308 | { 309 | return state == CANCELLED; 310 | } 311 | 312 | public boolean isSet() 313 | { 314 | return state != NOT_SET; 315 | } 316 | 317 | private Thread signal() 318 | { 319 | if (!isSet() && signalledUpdater.compareAndSet(this, NOT_SET, SIGNALLED)) 320 | { 321 | Thread thread = this.thread; 322 | LockSupport.unpark(thread); 323 | this.thread = null; 324 | return thread; 325 | } 326 | return null; 327 | } 328 | 329 | public boolean checkAndClear() 330 | { 331 | if (!isSet() && signalledUpdater.compareAndSet(this, NOT_SET, CANCELLED)) 332 | { 333 | threadUpdater.lazySet(this, null); 334 | remove(); 335 | return false; 336 | } 337 | // must now be signalled assuming correct API usage 338 | return true; 339 | } 340 | 341 | /** 342 | * Should only be called by the registered thread. Indicates the signal can be retired, 343 | * and, if already signalled, propagates the signal to another waiting thread 344 | */ 345 | public void cancel() 346 | { 347 | if (isCancelled()) 348 | return; 349 | if (!signalledUpdater.compareAndSet(this, NOT_SET, CANCELLED)) 350 | { 351 | // must already be signalled - switch to cancelled and 352 | signalledUpdater.lazySet(this, CANCELLED); 353 | // propagate the signal 354 | WaitQueue.this.signal(); 355 | } 356 | threadUpdater.lazySet(this, null); 357 | remove(); 358 | } 359 | 360 | // attempt to edit ourselves out of the list 361 | void remove() 362 | { 363 | // in case the list has some phantom elements, we try to remove ourselves 364 | // and any contiguous adjacent range of deleted nodes 365 | 366 | // start by finding our live predecessor 367 | RegisteredSignal p = prev; 368 | while (true) 369 | { 370 | if (p == null) 371 | { 372 | // we have no live predecessor; hasWaiters() will remove us 373 | hasWaiters(); 374 | return; 375 | } 376 | if (!p.isSet()) 377 | break; 378 | p = p.prev; 379 | } 380 | 381 | // we walk forwards from our live predecessor to find the next live node, 382 | // since the forward chaining is our source of truth (prev is only a helping hand) 383 | RegisteredSignal n = p.next; 384 | // if we are the tail of the list, we cannot be removed 385 | if (n == null) 386 | return; 387 | RegisteredSignal n2; 388 | while (n.isSet() && (n2 = n.next) != null) 389 | n = n2; 390 | p.next = n; 391 | } 392 | } 393 | 394 | // for testing 395 | boolean present(Iterator signals) 396 | { 397 | RegisteredSignal n = head; 398 | while (signals.hasNext() && n != null) 399 | { 400 | Signal s = signals.next(); 401 | while (n != null && n != s) 402 | n = n.next; 403 | } 404 | return n != null; 405 | } 406 | 407 | private static final AtomicIntegerFieldUpdater signalledUpdater = AtomicIntegerFieldUpdater.newUpdater(RegisteredSignal.class, "state"); 408 | private static final AtomicReferenceFieldUpdater nextUpdater = AtomicReferenceFieldUpdater.newUpdater(RegisteredSignal.class, RegisteredSignal.class, "next"); 409 | private static final AtomicReferenceFieldUpdater prevUpdater = AtomicReferenceFieldUpdater.newUpdater(RegisteredSignal.class, RegisteredSignal.class, "prev"); 410 | private static final AtomicReferenceFieldUpdater threadUpdater = AtomicReferenceFieldUpdater.newUpdater(RegisteredSignal.class, Thread.class, "thread"); 411 | 412 | } 413 | --------------------------------------------------------------------------------