La siguiente clase envuelve un ThreadPoolExecutor y usa un semáforo para bloquear, luego la cola de trabajo está llena:
public final class BlockingExecutor {
private final Executor executor;
private final Semaphore semaphore;
public BlockingExecutor(int queueSize, int corePoolSize, int maxPoolSize, int keepAliveTime, TimeUnit unit, ThreadFactory factory) {
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>();
this.executor = new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, unit, queue, factory);
this.semaphore = new Semaphore(queueSize + maxPoolSize);
}
private void execImpl (final Runnable command) throws InterruptedException {
semaphore.acquire();
try {
executor.execute(new Runnable() {
@Override
public void run() {
try {
command.run();
} finally {
semaphore.release();
}
}
});
} catch (RejectedExecutionException e) {
// will never be thrown with an unbounded buffer (LinkedBlockingQueue)
semaphore.release();
throw e;
}
}
public void execute (Runnable command) throws InterruptedException {
execImpl(command);
}
}
Esta clase contenedora se basa en una solución dada en el libro Java Concurrency in Practice de Brian Goetz. La solución en el libro solo toma dos parámetros de constructor: un Executor
y un límite usado para el semáforo. Esto se muestra en la respuesta dada por Fixpoint. Hay un problema con ese enfoque: puede llegar a un estado en el que los subprocesos del grupo están ocupados, la cola está llena, pero el semáforo acaba de liberar un permiso. ( semaphore.release()
en el bloque finalmente). En este estado, una nueva tarea puede obtener el permiso recién publicado, pero se rechaza porque la cola de tareas está llena. Por supuesto, esto no es algo que desee; desea bloquear en este caso.
Para solucionar esto, debemos utilizar una cola ilimitada , como claramente menciona JCiP. El semáforo actúa como un protector, dando el efecto de un tamaño de cola virtual. Esto tiene el efecto secundario de que es posible que la unidad pueda contener maxPoolSize + virtualQueueSize + maxPoolSize
tareas. ¿Porqué es eso? Debido al
semaphore.release()
bloque en el finalmente. Si todos los subprocesos del grupo llaman a esta declaración al mismo tiempo, maxPoolSize
se liberan los permisos, lo que permite que ingresen a la unidad el mismo número de tareas. Si estuviéramos usando una cola limitada, aún estaría llena, lo que resultaría en una tarea rechazada. Ahora, como sabemos que esto solo ocurre cuando un subproceso de grupo está casi terminado, esto no es un problema. Sabemos que el subproceso del grupo no se bloqueará, por lo que pronto se tomará una tarea de la cola.
Sin embargo, puedes usar una cola limitada. Solo asegúrate de que su tamaño sea igual virtualQueueSize + maxPoolSize
. Los tamaños más grandes son inútiles, el semáforo evitará que entren más elementos. Los tamaños más pequeños darán como resultado tareas rechazadas. La posibilidad de que las tareas sean rechazadas aumenta a medida que disminuye el tamaño. Por ejemplo, digamos que desea un ejecutor acotado con maxPoolSize = 2 y virtualQueueSize = 5. Luego, tome un semáforo con 5 + 2 = 7 permisos y un tamaño de cola real de 5 + 2 = 7. El número real de tareas que pueden estar en la unidad es entonces 2 + 5 + 2 = 9. Cuando el ejecutor está lleno (5 tareas en cola, 2 en el grupo de subprocesos, por lo que hay 0 permisos disponibles) y TODOS los subprocesos del grupo liberan sus permisos, las tareas que ingresan pueden tomar exactamente 2 permisos.
Ahora, la solución de JCiP es algo engorrosa de usar ya que no hace cumplir todas estas restricciones (cola ilimitada o limitada con esas restricciones matemáticas, etc.). Creo que esto solo sirve como un buen ejemplo para demostrar cómo se pueden construir nuevas clases seguras para subprocesos basadas en las partes que ya están disponibles, pero no como una clase completamente desarrollada y reutilizable. No creo que esto último fuera la intención del autor.