/** * Copyright (c) 2014-2017 by the respective copyright holders. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html */ package org.eclipse.smarthome.core.common; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedTransferQueue; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This is a thread pool executor service, which works as a developer would expect it to work. * The default {@link ThreadPoolExecutor} does the following (see * <a href="http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html">the official * JavaDoc)</a>: * <ul> * <li>If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than * queuing.</li> * <li>If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a * new thread.</li> * <li>If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case, * the task will be rejected.</li> * </ul> * * This class in contrast implements the following logic: * <ul> * <li>corePoolSize is 1, so threads are only created on demand</li> * <li>If the number of busy threads is smaller than the threadPoolSize, the Executor always prefers adding (or reusing) * a thread rather than queuing it.</li> * <li>If threadPoolSize threads are busy, new requests will be put in a FIFO queue and processed as soon as a thread * becomes idle.</li> * <li>The queue size is unbound, i.e. requests will never be rejected. * <li>Threads are terminated after being idle for at least 10 seconds. * </ul> * Please note that this implementation (with its partially hard-coded settings) is specifically targeted for use * on embedded devices without a high throughput. If you intend to use it for mass data processing on a server, you * should definitely tweak those settings. * * @author Kai Kreuzer - Initial contribution and API * */ public class QueueingThreadPoolExecutor extends ThreadPoolExecutor { private Logger logger = LoggerFactory.getLogger(QueueingThreadPoolExecutor.class); /** we will use a core pool size of 1 since we allow to timeout core threads. */ final static int CORE_THREAD_POOL_SIZE = 1; /** Our queue for queueing tasks that wait for a thread to become available */ private LinkedTransferQueue<Runnable> taskQueue = new LinkedTransferQueue<>(); /** The thread for processing the queued tasks */ private Thread queueThread; final private Object semaphore = new Object(); final private String threadPoolName; /** * Allows to subclass QueueingThreadPoolExecutor. */ protected QueueingThreadPoolExecutor(String name, int threadPoolSize) { this(name, new CommonThreadFactory(name), threadPoolSize, new QueueingThreadPoolExecutor.QueueingRejectionHandler()); } private QueueingThreadPoolExecutor(String threadPoolName, ThreadFactory threadFactory, int threadPoolSize, RejectedExecutionHandler rejectionHandler) { super(CORE_THREAD_POOL_SIZE, threadPoolSize, 10L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), threadFactory, rejectionHandler); this.threadPoolName = threadPoolName; allowCoreThreadTimeOut(true); } /** * Creates a new instance of {@link QueueingThreadPoolExecutor} * * @param name the name of the thread pool, will be used as a prefix for the name of the threads * @param threadPoolSize the maximum size of the pool * @return the {@link QueueingThreadPoolExecutor} instance */ public static QueueingThreadPoolExecutor createInstance(String name, int threadPoolSize) { if (name == null || name.trim().isEmpty()) { throw new IllegalArgumentException("A thread pool name must be provided!"); } return new QueueingThreadPoolExecutor(name, new CommonThreadFactory(name), threadPoolSize, new QueueingThreadPoolExecutor.QueueingRejectionHandler()); } /** * Adds a new task to the queue * * @param runnable the task to add */ protected void addToQueue(Runnable runnable) { taskQueue.add(runnable); if (queueThread == null || !queueThread.isAlive()) { synchronized (this) { // check again to make sure it has not been created by another thread if (queueThread == null || !queueThread.isAlive()) { logger.trace("Thread pool '{}' exhausted, queueing tasks now.", threadPoolName); queueThread = createNewQueueThread(); queueThread.start(); } } } } @Override protected void afterExecute(Runnable r, Throwable t) { super.afterExecute(r, t); synchronized (semaphore) { semaphore.notify(); } } /** * This implementation does not allow setting a custom handler. * * @throws UnsupportedOperationException if called. */ @Override public void setRejectedExecutionHandler(RejectedExecutionHandler handler) { throw new UnsupportedOperationException(); } @Override public BlockingQueue<Runnable> getQueue() { return taskQueue; } @Override public void execute(Runnable command) { // make sure that rejected tasks are executed before any new concurrently incoming tasks if (taskQueue.isEmpty()) { super.execute(command); } else { if (command == null) { throw new IllegalArgumentException("Command can not be null."); } // ignore incoming tasks when the executor is shutdown if (!isShutdown()) { addToQueue(command); } } } private Thread createNewQueueThread() { Thread thread = getThreadFactory().newThread(new Runnable() { @Override public void run() { while (true) { // check if some thread from the pool is idle if (QueueingThreadPoolExecutor.this.getActiveCount() < QueueingThreadPoolExecutor.this .getMaximumPoolSize()) { try { // keep waiting for max 2 seconds if further tasks are pushed to the queue Runnable runnable = taskQueue.poll(2, TimeUnit.SECONDS); if (runnable != null) { logger.trace("Executing queued task of thread pool '{}'.", threadPoolName); QueueingThreadPoolExecutor.super.execute(runnable); } else { break; } } catch (InterruptedException e) { } } else { // let's wait for a thread to become available, but max. 1 second try { synchronized (semaphore) { semaphore.wait(1000); } } catch (InterruptedException e) { } } } logger.trace("Queue for thread pool '{}' fully processed - terminating queue thread.", threadPoolName); } }); thread.setName(threadPoolName + "-queue"); return thread; } /** * This is the internally used thread factory, which creates non-daemon threads and assigns them a sequentially * indexed name. */ private static class CommonThreadFactory implements ThreadFactory { protected final ThreadGroup group; protected final AtomicInteger threadNumber = new AtomicInteger(1); protected final String name; public CommonThreadFactory(String name) { this.name = name; SecurityManager s = System.getSecurityManager(); group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); } @Override public Thread newThread(Runnable r) { Thread t = new Thread(group, r, name + "-" + threadNumber.getAndIncrement(), 0); if (t.isDaemon()) { t.setDaemon(false); } if (t.getPriority() != Thread.NORM_PRIORITY) { t.setPriority(Thread.NORM_PRIORITY); } return t; } } /** * This is the internally used rejection handler, which - instead of rejecting a task - puts it to the queue of the * pool. */ private static class QueueingRejectionHandler extends ThreadPoolExecutor.DiscardPolicy { @Override public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) { if (!threadPoolExecutor.isShutdown()) { QueueingThreadPoolExecutor queueingThreadPoolExecutor = (QueueingThreadPoolExecutor) threadPoolExecutor; queueingThreadPoolExecutor.addToQueue(runnable); } } } }