通常生产者-消费者的经典实现方式是,启动一个消费者线程从阻塞队列里获取消息进行消费,(多个)生产者往队列里插入待消费的数据然后立即返回。如果生产者生产的速率远大于消费者消费的速率,那么队列的待处理数据就会累积得越来越多。
顾名思义,“多消费者”就是开启多个消费者线程,这里借用Java线程池来管理线程的生命周期:
首先,定义一个接口表示异步消费:
import java.util.concurrent.RejectedExecutionException;
/**
* An object that accepts elements for future consuming.
*
* @param The type of element to be consumed
*/
public interface AsynchronousConsumer {
/**
* Accept an element for future consuming.
*
* @param e the element to be consumed
* @return true if accepted, false otherwise
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException if some property of this element
* prevents it from being accepted for future consuming
* @throws RejectedExecutionException if the element
* cannot be accepted for consuming
*/
public boolean accept(E e);
}
再定义一个消费者线程(由于线程托管给线程池管理了,这里是定义一个Runnable),多个Runnable通过共用BlockingQueue来实现多个消费者消费。这里 通过指定“batchSize”来表示批量处理(如果值大于1的话),TimeUnit指明了线程(Runnable)的最大空闲时间,超过该时间将会自动退出(如果值为0则表示永远等待)并提供beforeConsume、afterConsume、terminated等方法以便扩展:
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* An abstract {@code Runnable} of consumer for batches consuming Asynchronously
*
* @param The type of element to be consumed
*/
public abstract class ConsumerRunnable implements Runnable, AsynchronousConsumer{
/**
* for recording any exception
*/
private static final Log log = LogFactory.getLog(ConsumerRunnable.class);
/**the elements queue**/
private final BlockingQueue queue;
/**
* the batchSize for {@link #consume} used in every loop
*/
private volatile int batchSize;
/**
* Timeout in nanoseconds for polling an element from the
* working queue(BlockingQueue).Thread uses this timeout to
* wait up if the working queue is empty.
* zero means wait forever until an element become available.
*/
private volatile long waitTime;
/**
* If true then will cause run() quit
* when waiting for element if interrupted
*/
private volatile boolean quitIfInterrupted = true;
/**
* Allocates a new {@code ConsumerRunnable} object
* with the given initial parameters and default waitTime(0)
* which will cause the thread to wait forever until an element
* become available when the element queue is empty.
*
* @param queue the BlockingQueue for working
* @param batchSize the batch size used in {@link #consume} for every loop
*
* @throws NullPointerException if the queue is null
* @throws IllegalArgumentException if the batchSize is less then or equal to zero
*/
public ConsumerRunnable(int batchSize, BlockingQueue queue){
this.batchSize = PracticalUtils.requirePositive(batchSize);
this.queue = Objects.requireNonNull(queue);
}
/**
* Allocates a new {@code ConsumerRunnable} object
* with the given initial parameters.
* A time value of zero will cause the thread to wait forever
* until an element become available when the element queue is empty.
*
* @param queue the BlockingQueue for working
* @param batchSize the batch size used in {@link #consume} for every loop
* @param time the time to wait. A time value of zero will cause
* the thread to wait forever until an element become available
* when the element queue is empty.
* @param unit the time unit of the time argument
*
* @throws NullPointerException if any specified parameter is null
* @throws IllegalArgumentException if the batchSize is less then or equal to zero,
* or the time is less than zero
*/
public ConsumerRunnable(int batchSize, long time, TimeUnit unit,
BlockingQueue queue){
this.batchSize = PracticalUtils.requirePositive(batchSize);
this.queue = Objects.requireNonNull(queue);
setWaitTime(time, unit);
}
/**
* Accept an element for future consuming.
*
* Default implementation is equally invoke
* {@link java.util.Queue#offer queue.offer}
*
* @param e the element to be consumed
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException if some property of this element
* prevents it from being accepted for future consuming
* @throws RejectedExecutionException if the element
* cannot be accepted for future consuming (optional)
* @return true if accepted, false otherwise
*/
public boolean accept(E e){
Objects.requireNonNull(e);
return queue.offer(e);
}
/**
* Main worker run loop. Repeatedly gets elements from working queue
* and consumes them, while coping with a number of issues:
*
*
1. Each loop run is preceded by a call to beforeConsume, which
* might throw an exception, in which case the {@link #consume}
* of the current loop will not be invoked.
*
*
2. Assuming beforeConsume completes normally, we consume the elements,
* gathering any of its thrown exceptions to send to afterConsume. Any thrown
* exception in the afterConsume conservatively causes the runnable to quit.
*
*
3. After {@link #consume} of the current loop completes, we call afterExecute,
* which may also throw an exception, which will cause the runnable to quit.
*
*/
@Override
public final void run() {
starting();
final List list = new ArrayList<>();
try{
while(true){
try {
E info;
int bSize = batchSize;
for(int i=0;i list){
Throwable t = null;
try {
consume(list);
} catch (Throwable t1) {
t = t1;
throw t1;
} finally{
try {
afterConsume(list,t);
} finally{
list.clear();//clear to be reused for next loop
}
}
}
/**
* Returns the batchSize for {@link #consume} used in every loop
*
* @return the batchSize
*
* @see #setBatchSize
*/
public int getBatchSize(){
return batchSize;
}
/**
* Sets the batchSize for next loop used in {@link #consume}.
* This overrides any value set in the constructor.
*
* @param batchSize the new batchSize
* @throws IllegalArgumentException if the new batchSize is
* less than or equal to zero
* @see #getBatchSize
*/
public void setBatchSize(int batchSize){
this.batchSize = PracticalUtils.requirePositive(batchSize);
}
/**
* Returns the element queue used by this thread. Access to the
* element queue is intended primarily for debugging and monitoring.
* This queue may be in active use. Retrieving the element queue
* does not prevent queued element from being saved.
*
* @return the element queue
*/
public BlockingQueue getQueue() {
return queue;
}
/**
* Sets the time limit for the thread when waiting for
* an element to become available. This overrides any value
* set in the constructor. zero means wait forever.
* Timeout will cause the runnable to quit.
*
* @param time the time to wait. A time value of zero will cause
* the thread to wait forever until an element become available
* when the element queue is empty.
* @param unit the time unit of the {@code time} argument
* @throws IllegalArgumentException if {@code time} less than zero
* @see #getWaitTime(TimeUnit)
*/
public void setWaitTime(long time, TimeUnit unit) {
long t = unit.toNanos(PracticalUtils.requireNonNegative(time));
this.waitTime = t;
}
/**
* Returns the thread waiting time, which is the time to wait up
* when the working queue is empty. zero means wait forever.
*
* @param unit the desired time unit of the result
* @return the time limit
* @see #setWaitTime(long, TimeUnit)
*/
public long getWaitTime(TimeUnit unit) {
return unit.convert(waitTime, TimeUnit.NANOSECONDS);
}
/**
* Returns true if this runnable will quit
* if interrupted when waiting for element.
* @return true if this runnable will quit
* if interrupted when waiting for element
*/
public boolean isQuitIfInterrupted(){return quitIfInterrupted;}
/**
* Set quit or not if interrupted when waiting for element.
* @param quitIfInterrupted true will cause run() quit
* if interrupted when waiting for element
*/
public void setQuitIfInterrupted(boolean quitIfInterrupted){
this.quitIfInterrupted = quitIfInterrupted;
}
/* Extension hooks */
/**
* Method invoked at the beginning of the run() method.
* Note:if this method throw any exception then
* any other method will not be invoked and cause the runnable to quit.
*/
protected void starting() { }
/**
* Method invoked prior to current thread waiting for
* an element to become available.
* Any exception thrown by this method will be ignored.
*
*
This implementation does nothing, but may be customized in
* subclasses. Note: To properly nest multiple overridings, subclasses
* should generally invoke {@code super.beforeWaiting} at the end of
* this method.
*
*/
protected void beforeWait() { }
/**
* Method invoked prior to invoking {@link #consume} in the
* current runnable for every loop.
*
*
Note:if this method throw any exception then the
* {@link #consume} of current loop will not be invoked
*
*
This implementation does nothing, but may be customized in
* subclasses. Note: To properly nest multiple overridings, subclasses
* should generally invoke {@code super.beforeConsume} at the end of
* this method.
*
* @param list the elements that will be consumed
*/
protected void beforeConsume(List list) { }
/**
* For subclass to implement the consume actions.
*
* @param list the elements that will be consumed
*/
protected abstract void consume(List list);
/**
* Method invoked upon one loop completion of {@link #consume} invoked.
* This method is invoked by the current thread. If
* non-null, the Throwable is the uncaught {@code Error}
* that caused execution to terminate abruptly.
*
* This implementation does nothing, but may be customized in
* subclasses. Note: To properly nest multiple overridings, subclasses
* should generally invoke {@code super.afterConsume} at the
* beginning of this method.
*
* @param list the elements that has consumed(or to be consumed if the exception caused termination)
* @param t the exception that caused termination, or null if
* the current loop execution completed normally
*/
protected void afterConsume(List list, Throwable t) { };
/**
* Method invoked when the runnable is about to quit. Default
* implementation does nothing. Note: To properly nest multiple
* overridings, subclasses should generally invoke
* {@code super.terminated} within this method.
*/
protected void terminated() { };
}
最后需要定义一个线程(这里是Runnable)管理器用于管理线程(Runnable)的创建及消亡:
maximumPoolSize 表示最大的线程(Runnable)数
batchSizeMultiple 用于控制线程的增长,增长条件是“(getPoolSize()
”。值为0表示每来一条消息就创建一条线程(Runnable)去处理。
ConsumerRunnableFactory 用于在增长线程(Runnable)时需要创建用来表示线程的ConsumerRunnable实例
ExecutorService 是实际的线程池
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Manage the instance creation of class
* {@code ConsumerRunnable} for pooling reuse
*
* @param The type of element to be consumed
*/
public class ThreadPoolConsumer implements AsynchronousConsumer{
/**
* The maximum pool size for common use of every business type.
*/
public static final int MAX_POOL_SIZE = 20;
/**
* the common pool used for every business type and
* can not be shut down!
*/
private static final ExecutorService SERVICE;
static{
ThreadPoolExecutor executor = new ThreadPoolExecutor(
MAX_POOL_SIZE, MAX_POOL_SIZE, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue(),ThreadFactorys.newFactory("ConsumerRunnable")){
@Override
public void execute(Runnable command) {
BlockingQueue queue = getQueue();
if(queue.offer(command)){
int poolSize;
if(queue.size()>0&&((poolSize=getPoolSize())==0||
(poolSize<=getActiveCount()&&queue.size()>0))){
prestartCoreThread();
}
return;
}
super.execute(command);
}
};
executor.allowCoreThreadTimeOut(true);
SERVICE = new DelegateExecutorService(executor);;
}
/**
* Returns the ExecutorService used for common in this class,
* which can not be shut down and it will throws
* {@link UnsupportedOperationException}
* if being invoked
* @return the common ExecutorService
*/
public static ExecutorService getCommonExecutorService(){
return SERVICE;
}
/**
* the pool size, that is ,the instance number of PooledConsumerRunnable
* in the pool (ExecutorService {@code esv})
*/
private final AtomicInteger counter = new AtomicInteger();
/**
* the ExecutorService used for pooling
*/
private volatile ExecutorService esv;
/**
* the BlockingQueue of elements
*/
private final BlockingQueue queue;
/**
* the time to wait in nanoseconds. A time value of zero will cause
* the thread to wait forever when the working queue is empty.
*/
private final long time;
/**
* the maximum number of runnables to allow in the
* pool for current business type
*/
private volatile int maxPoolSize;
/**
* the batch size used in {@code consume} for every loop
*/
private final int batchSize;
/**
* the product of batchSize * batchSizeMultiple
*/
private volatile int multipleSize;
/**
* The multiple used to adjust pool size if needed.
*/
private volatile float batchSizeMultiple;
/**
* Factory for new ConsumerRunnable. All ConsumerRunnable are created
* using this factory (via constructor DelegateConsumerRunnable).
*/
private volatile ConsumerRunnableFactory factory;
/**
* Allocates a new {@code ThreadPoolConsumer} object
* with the given initial parameters and default LinkedBlockingQueue and ExecutorService.
*
* @param maximumPoolSize the maximum number of runnables to allow in the
* pool for current business type
* @param batchSize the batch size used in {@code consume} for every loop
* @param batchSizeMultiple the multiple of batchSize, if the condition
* {@code (getPoolSize()getPoolSize()*batchSize*batchSizeMultiple)}
* is true then we create a new runnable and put it into the ExecutorService
* {@code esv}
* @param time the time to wait. A time value of zero will cause
* the thread to wait forever until an element become available
* when the working queue is empty.
* @param unit the time unit of the time argument
* @throws NullPointerException if specified factory is null
* @throws IllegalArgumentException if specified maximumPoolSize or batchSize
* is negative
*/
public ThreadPoolConsumer(int maximumPoolSize, int batchSize,float batchSizeMultiple,
long time, TimeUnit unit, ConsumerRunnableFactory factory){
this(maximumPoolSize, batchSize, batchSizeMultiple, time, unit,
new LinkedBlockingQueue(), SERVICE, factory);
}
/**
* Allocates a new {@code ThreadPoolConsumer} object
* with the given initial parameters.
*
* @param maximumPoolSize the maximum number of runnables to allow in the
* pool for current business type
* @param batchSize the batch size used in {@code consume} for every loop
* @param batchSizeMultiple the multiple of batchSize, if the condition
* {@code (getPoolSize()getPoolSize()*batchSize*batchSizeMultiple)}
* is true then we create a new runnable and put it into the ExecutorService
* {@code esv}
* @param time the time to wait. A time value of zero will cause
* the thread to wait forever until an element become available
* when the working queue is empty.
* @param unit the time unit of the time argument
* @param queue the BlockingQueue for working
* @param esv the ExecutorService used for pooling
* @throws IllegalArgumentException if specified batchSize is not positive or
* the product of (batchSize * batchSizeMultiple), or maximumPoolSize,
* or time value is negative
* @throws NullPointerException if any specified parameter is null
*/
public ThreadPoolConsumer(int maximumPoolSize,int batchSize,float batchSizeMultiple,
long time, TimeUnit unit,BlockingQueue queue,
ExecutorService esv, ConsumerRunnableFactory factory) {
this.esv = Objects.requireNonNull(esv);
this.queue = Objects.requireNonNull(queue);
this.batchSize = PracticalUtils.requirePositive(batchSize);
if(batchSizeMultiple<0){
throw new IllegalArgumentException("Negative batchSizeMultiple!");
}
this.batchSizeMultiple = batchSizeMultiple;
int mSize = (int) (batchSize * batchSizeMultiple);
this.multipleSize = PracticalUtils.requireNonNegative(mSize);
int psize = PracticalUtils.requireNonNegative(maximumPoolSize);
if(psize>MAX_POOL_SIZE){
psize = MAX_POOL_SIZE;
}
this.maxPoolSize = psize;
this.time = unit.toNanos(PracticalUtils.requireNonNegative(time));
this.factory = Objects.requireNonNull(factory);
}
/**
* Returns the current number of runnable in the pool.
*
* @return the number of runnables
*/
public int getPoolSize() {
return counter.get();
}
/**
* get the ExecutorService instance passed to
* the ThreadPoolConsumer constructor.
* if the returned ExecutorService instance is
* the common static instance defined in
* the ThreadPoolConsumer, it dose'nt
* support shutdown() and shutdownNow()
* operation which will cause
* {@link UnsupportedOperationException}
* if being invoked
*
* @return the ExecutorService instance used by
* this ThreadPoolConsumer instance
*/
public ExecutorService getExecutorService(){
return esv;
}
/**
* Sets the ExecutorService used to execute the ConsumerRunnable.
* @param newExecutorService the new ExecutorService
* @throws NullPointerException if newExecutorService is null
* @throws IllegalArgumentException if the newExecutorService has
* already been shut down
*/
public void setExecutorService(ExecutorService newExecutorService){
if(newExecutorService.isShutdown()){
throw new IllegalArgumentException();
}
this.esv = newExecutorService;
}
/**
* To keep alive when the pool size is zero but the queue is not empty.
* @throws IllegalStateException if the executorService has been shut down
*/
public void keepAlive(){
int c;
if(!queue.isEmpty()&&(c = counter.get())==0){
c = counter.incrementAndGet();
if(c==1&&!queue.isEmpty()){
try {
DelegateConsumerRunnable r = new DelegateConsumerRunnable();
esv.execute(r);
} catch (Throwable t) {
counter.decrementAndGet();
t.printStackTrace();
if(esv.isShutdown()){
throw new IllegalStateException(
"The executorService has been shut down!");
}
throw t;
}
}else{
counter.decrementAndGet();
}
}
}
/**
* Method invoked when fails to keep the current pool alive.
* This implementation does nothing, but may be customized
* in subclasses.
* @param t the failure reason thrown by {@link #keepAlive()}
*/
protected void failToKeepAlive(Throwable t){
}
private void tryKeepAlive(){
try {
keepAlive();
} catch (Throwable t) {
failToKeepAlive(t);
}
}
/**
* Returns the element queue used by current pool. Access to the
* element queue is intended primarily for debugging and monitoring.
* This queue may be in active use. Retrieving the element queue
* does not prevent queued element from being consumed.
*
* @return the element queue
*/
public BlockingQueue getQueue() {
return queue;
}
/**
* Accept an element for future consuming.
*
* @param e the element to be consumed
* @return true if accepted, false otherwise
* @throws NullPointerException if the specified element is null
* @throws IllegalArgumentException if some property of this element
* prevents it from being accepted for future consuming
* @throws RejectedExecutionException if the element
* cannot be accepted for consuming
*/
public boolean accept(E e){
Objects.requireNonNull(e);
int c;
boolean added = queue.offer(e);
if(added&&((c = counter.get())==0||
(c=c*multipleSize))){
c = counter.incrementAndGet();
if(c==1||(c<=maxPoolSize&&queue.size()>=(c-1)*multipleSize)){
try {
DelegateConsumerRunnable r = new DelegateConsumerRunnable();
//If current pool size is larger than one
//then except for the first one, the others
//don't have to wait too long.
if(c>1){
int n = maxPoolSize - c;
if(n<=0){
n = 1;
}
r.setWaitTime(n*100, TimeUnit.MILLISECONDS);
}
esv.execute(r);
} catch (Throwable t) {
counter.decrementAndGet();
if(queue.remove(e)){
if(t instanceof RejectedExecutionException){
throw (RejectedExecutionException)t;
}
throw new RejectedExecutionException(t);
}else{
tryKeepAlive();
}
}
}else{
counter.decrementAndGet();
}
}
return added;
}
/**
* Adjusts the pool size as needed if the maximumPoolSize has changed
* by {@link #setMaximumPoolSize(int) setMaximumPoolSize} or the
* {@code batchSizeMultiple} value has changed by
* {@link #setBatchSizeMultiple(float) setBatchSizeMultiple}.
* @return true if adjusted successfully, false otherwise
*/
public boolean adjustPoolSize(){
if(queue.isEmpty()){
return false;
}
int c = counter.get();
if(c==0||(c=c*multipleSize)){
c = counter.incrementAndGet();
if((c==1&&!queue.isEmpty())||(c<=maxPoolSize&&queue.size()>=(c-1)*multipleSize)){
try {
DelegateConsumerRunnable r = new DelegateConsumerRunnable();
//If current pool size is larger than one
//then except for the first one, the others
//don't have to wait too long.
if(c>1){
int n = maxPoolSize - c;
if(n<=0){
n = 1;
}
r.setWaitTime(n*100, TimeUnit.MILLISECONDS);
}
esv.execute(r);
return true;
} catch (Throwable t) {
counter.decrementAndGet();
tryKeepAlive();
}
}else{
counter.decrementAndGet();
}
}
return false;
}
/**
* Sets the {@code ConsumerRunnable} factory used to create new ConsumerRunnables.
*
* @param factory the new ConsumerRunnable factory
* @throws NullPointerException if factory is null
* @see #getConsumerRunnableFactory
*/
public void setConsumerRunnableFactory(ConsumerRunnableFactory factory) {
this.factory = Objects.requireNonNull(factory);
}
/**
* Returns the {@code ConsumerRunnable} factory used to create new ConsumerRunnables.
*
* @return the current ConsumerRunnable factory
* @see #setConsumerRunnableFactory(factory)
*/
public ConsumerRunnableFactory getConsumerRunnableFactory() {
return factory;
}
/**
* Sets the maximum allowed number of ConsumerRunnables (threads).
* This overrides any value set in the constructor.
*
* @param maximumPoolSize the new maximum
* @throws IllegalArgumentException if the new maximum is
* less than or equal to zero
* @see #getMaximumPoolSize
*/
public void setMaximumPoolSize(int maximumPoolSize){
PracticalUtils.requirePositive(maximumPoolSize);
if(maxPoolSize != maximumPoolSize){
maxPoolSize = maximumPoolSize;
adjustPoolSize();
}
}
/**
* Returns the maximum allowed number of ConsumerRunnables (threads).
*
* @return the maximum allowed number of ConsumerRunnables (threads)
* @see #setMaximumPoolSize
*/
public int getMaximumPoolSize() {
return maxPoolSize;
}
/**
* Returns the multiple of batchSize used when the pool size need to grow up.
*
* @return the multiple
*/
public float getBatchSizeMultiple(){
return batchSizeMultiple;
}
/**
* Sets the multiple of batchSize used when the pool size need to grow up.
* This overrides any value set in the constructor.
*
* @param batchSizeMultiple the multiple
* @throws IllegalArgumentException if the batchSizeMultiple passed in is negative
*/
public void setBatchSizeMultiple(float batchSizeMultiple){
if(batchSizeMultiple<0){
throw new IllegalArgumentException("Negative batchSizeMultiple!");
}
int mSize = (int) (batchSize * batchSizeMultiple);
this.multipleSize = PracticalUtils.requireNonNegative(mSize);
this.batchSizeMultiple = batchSizeMultiple;
adjustPoolSize();
}
/**
* A delegate class that dosn'nt support shutdown() and shutdownNow()
*
*/
private static class DelegateExecutorService implements ExecutorService{
private final ExecutorService delegate;
private DelegateExecutorService(ExecutorService esv){
delegate = esv;
}
@Override
public void execute(Runnable arg0) {
delegate.execute(arg0);
}
@Override
public void shutdown() {
throw new UnsupportedOperationException("can not shutdown the common pool!");
}
@Override
public List shutdownNow() {
throw new UnsupportedOperationException("can not shutdown the common pool!");
}
@Override
public boolean isShutdown() {
return false;
}
@Override
public boolean isTerminated() {
return false;
}
@Override
public boolean awaitTermination(long arg0, TimeUnit arg2)
throws InterruptedException {
return false;
}
@Override
public Future submit(Callable arg0) {
return delegate.submit(arg0);
}
@Override
public Future submit(Runnable arg0, T arg1) {
return delegate.submit(arg0, arg1);
}
@Override
public Future> submit(Runnable arg0) {
return delegate.submit(arg0);
}
@Override
public List> invokeAll(
Collection extends Callable> arg0)
throws InterruptedException {
return delegate.invokeAll(arg0);
}
@Override
public List> invokeAll(
Collection extends Callable> arg0, long arg1, TimeUnit arg3)
throws InterruptedException {
return delegate.invokeAll(arg0, arg1, arg3);
}
@Override
public T invokeAny(Collection extends Callable> arg0)
throws InterruptedException, ExecutionException {
return delegate.invokeAny(arg0);
}
@Override
public T invokeAny(Collection extends Callable> arg0,
long arg1, TimeUnit arg3) throws InterruptedException,
ExecutionException, TimeoutException {
return delegate.invokeAny(arg0, arg1, arg3);
}
}
/**
*
* Delegate the ConsumerRunnable for pooling manage
*
*/
private final class DelegateConsumerRunnable implements Runnable{
/**
* Delegated ConsumerRunnable
*/
private final ConsumerRunnable r;
/**
* Allocates a new {@code ConsumerRunnable} object
* with the given initial parameter in the constructor
* of the outer class and wrap it to {@code DelegateConsumerRunnable}.
*/
public DelegateConsumerRunnable(){
r = Objects.requireNonNull(
factory.newConsumerRunnable(
batchSize, time, TimeUnit.NANOSECONDS, queue
)
);
}
/**
* Delegate the method ConsumerRunnable.run()
*/
@Override
public void run() {
try {
r.run();
} finally{
counter.decrementAndGet();
tryKeepAlive();
}
};
/**
* Delegate the method ConsumerRunnable.setWaitTime(long time, TimeUnit unit)
* @param time the time to wait. A time value of zero will cause the
* thread to wait forever until an element become available when the element
* queue is empty.
* @param unit the time unit of the time argument
*/
public void setWaitTime(long time, TimeUnit unit){
r.setWaitTime(time, unit);
}
}
}
ConsumerRunnableFactory.java:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
*
* An object that creates new ConsumerRunnable on demand.
*
* @param The type of element to be consumed
*/
public interface ConsumerRunnableFactory {
/**
* Constructs a new {@code ConsumerRunnable} with the given initial parameters.
* @param batchSize the batch size used in {@code consume} for every loop
* @param time the time to wait. A time value of zero will cause
* the thread to wait forever until an element become available
* when the working queue is empty.
* @param unit the time unit of the time argument
* @param queue the BlockingQueue for working
* @return constructed ConsumerRunnable
*/
ConsumerRunnable newConsumerRunnable(int batchSize, long time, TimeUnit unit,
BlockingQueue queue);
}
上面定义的ConsumerRunnable.java提供的consume(List
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Session;
import org.hibernate.Transaction;
/**
* An abstract {@code Runnable} of consumer for batches saving the elements to DB Asynchronously
*
* @param The type of element to be saved
*/
public abstract class BatchSaveRunnable extends ConsumerRunnable{
/**
* for recording any exception
*/
private static final Log log = LogFactory.getLog(BatchSaveRunnable.class);
/**
* When true (default), this runnable will begin transaction automatically
* by the program before the method {@code batchSave} and after
* invoked, the method commit/rollback will also be invoked automatically
* which means user code donsen't have to call them explicitly.
*/
private volatile boolean autoBeginTransaction = true;
/**
* The session stored in thread local variable
*/
private static final ThreadLocal localSession = new ThreadLocal<>();
private static void clearThreadlocalSession(){
localSession.remove();
}
private static void setThreadlocalSession(Session session){
localSession.set(session);
}
/**
* Get the DB Session from the thread local if any.
* @return org.hibernate.Session If it exists in the
* thread local then return it, otherwise return null.
* The return Session dose'nt have any Transactions,
* if you need the transactions you should invoke
* {@code session.beginTransaction()} explicitly
* and then commit or rollback it by yourself.
*/
public static Session getSessionFromThreadlocal(){
Session session = localSession.get();
if(session==null){
//If called in ended() then we don't have any chance to clear thread-local session,
//so just clear it when return null to avoid memory leak.
clearThreadlocalSession();
}
return session;
}
/**
* Allocates a new {@code ConsumerRunnable} object
* with the given initial parameters and default waitTime(0)
* which will cause the thread to wait forever until an element
* become available when the element queue is empty.
*
* @param queue the BlockingQueue for working
* @param batchSize the batch size used in {@link #batchSave} for every loop
*
* @throws NullPointerException if the queue is null
* @throws IllegalArgumentException if the batchSize is less then or equal to zero,
* or the time is less than zero
*/
public BatchSaveRunnable(int batchSize, BlockingQueue queue){
super(batchSize, queue);
}
/**
* Allocates a new {@code ConsumerRunnable} object
* with the given initial parameters.
* A time value of zero will cause the thread to wait forever
* until an element become available when the element queue is empty.
*
* @param queue the BlockingQueue for working
* @param batchSize the batch size used in {@link #batchSave} for every loop
* @param time the time to wait. A time value of zero will cause
* the thread to wait forever when the element queue is empty.
* @param unit the time unit of the time argument
*
* @throws NullPointerException if the queue is null
* @throws IllegalArgumentException if the batchSize is less then or equal to zero,
* or the time is less than zero
*/
public BatchSaveRunnable(int batchSize, long time, TimeUnit unit,
BlockingQueue queue){
super(batchSize, time, unit, queue);
}
/**
* If the {@code Session} currently used is present in the thread local
* then return it otherwise return null.
* The return Session dose'nt have any Transactions, if you need the
* transactions you should invoke session.beginTransaction() explicitly
* and then commit or rollback it by yourself.
* @return current session if any or null otherwise
* @see #getSessionFromThreadlocal()
*/
protected final Session getCurrentSessionIfPresent(){
return getSessionFromThreadlocal();
}
/**
* Returns true (the default) if this runnable will begin transaction
* automatically by the program before the method {@code batchSave} and
* after invoked, the method commit/rollback will also be invoked automatically
* which means you don't have to call them explicitly.
*
* @return {@code true} if this runnable will manage the transactions
* automatically, else {@code false}
*/
public boolean autoBeginTransaction(){
return autoBeginTransaction;
}
/**
* Sets the policy governing whether this runnable should manage
* the transactions automatically.
* If true (the default), this runnable will begin transaction automatically
* by the program before the method {@code batchSave} and after
* invoked, the method commit/rollback will also be invoked automatically
* which means you don't have to call them explicitly.
*
* @param auto {@code true} if should manage the transactions
* automatically, else {@code false}
*
*/
public void autoBeginTransaction(boolean auto){
autoBeginTransaction = auto;
}
/**
* Saves elements and ensure that the method
* {@code afterSave} must be invoked in the end.
* @param session the current session
* @param list the elements that will be saved to DB
*/
private void doBatchSave(Session session, List list){
Throwable t = null;
Transaction transaction = null;
boolean needTransaction = autoBeginTransaction;
try {
if(needTransaction){
transaction = DBUtil.beginTransaction(session);
}
batchSave(session, list);
if(needTransaction){
DBUtil.commit(transaction);
}
} catch (Throwable t1) {
t = t1;
try {
if(needTransaction){
DBUtil.rollback(transaction);
}
if(!session.isOpen()){
clearThreadlocalSession();
}
} catch (Throwable e) {
clearThreadlocalSession();
DBUtil.closeSession(session);
log.error("rollbacking batchSave occurred the exception:", e);
}
log.error("The runnable["+getClass().getName()+"] which invoked batchSave occurred the exception:"
+t1.toString(), t1);
throw t1;
} finally{
afterSave(list,t);
}
}
private void cleaningUp(){
Session session = getSessionFromThreadlocal();
clearThreadlocalSession();
if(session!=null){
DBUtil.closeSession(session);
}
}
/* overridings */
/**
* cleaning up before waiting.
* Note: To properly nest multiple overridings, subclasses
* should generally invoke {@code super.beforeWaiting} at the end of
* this method.
*/
@Override
protected void beforeWait() {
cleaningUp();
}
/**
* ensure have a session
*/
@Override
protected final void beforeConsume(List list) {
ensureHasThreadlocalSession();
}
private static Session ensureHasThreadlocalSession(){
Session session = getSessionFromThreadlocal();
if(session==null||!session.isOpen()){
session = DBUtil.newSession();
assert session!=null&&session.isOpen();
setThreadlocalSession(session);
}
return session;
}
/**
* perform batchSave
*/
@Override
protected final void consume(List list) {
beforeSave(list);
Session session = ensureHasThreadlocalSession();
doBatchSave(session, list);
}
/**
* Subclass should use {@link #afterSave} instead
* for extension which is more readable for current class.
*/
@Override
protected final void afterConsume(List list, Throwable t) {
//afterSave(list, t);
//We can't do this because this method will also be invoked
//if beforeSave throws any exception
}
/**
* Cleaning up resources before runnable quit.
* Subclass should use {@link #ended}
* instead for extension.
*/
@Override
protected final void terminated() {
try {
cleaningUp();
} finally{
ended();
}
};
/* Extension hooks */
/**
* Method invoked prior to invoking {@link #batchSave} in the
* current runnable for every loop.
*
* Note:if this method throw any exception then the
* {@link #batchSave} of current loop will not be invoked
*
*
This implementation does nothing, but may be customized in
* subclasses. Note: To properly nest multiple overridings, subclasses
* should generally invoke {@code super.beforeSave} at the end of
* this method.
*
* @param list the elements that will be saved
*/
protected void beforeSave(List list) { }
/**
* for subclass to implement.
*
* Note:subclass doesn't required to invoke commit/rollback explicitly
* by default, unless the default policy changed by invoked
* {@link #autoBeginTransaction(boolean) autoBeginTransaction(boolean)}
* @param session the DB session currently used
* @param list the elements that to be saved
*/
protected abstract void batchSave(Session session, List list);
/**
* Method invoked upon one loop completion of {@link #batchSave} invoked.
* This method is invoked by the current thread. If
* non-null, the Throwable is the uncaught {@code Error}
* that caused execution to terminate abruptly.
*
* This implementation does nothing, but may be customized in
* subclasses. Note: To properly nest multiple overridings, subclasses
* should generally invoke {@code super.afterSave} at the
* beginning of this method.
*
* @param list the elements that has saved(or to be saved if the exception caused termination)
* @param t the exception that caused termination, or null if
* the current loop execution completed normally
*/
protected void afterSave(List list, Throwable t) { };
/**
* Method invoked when the runnable is about to quit. Default
* implementation does nothing. Note: To properly nest multiple
* overridings, subclasses should generally invoke
* {@code super.ended} within this method.
*/
protected void ended(){};
}
通过重写父类的beforeWait()方法在等待之前释放掉数据库连接,并提供了beforeSave、afterSave等语义更明确的可扩展的方法。
其他涉及到的类:
DefaultBatchSaveRunnableFactory.java:
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* An implementation of ConsumerRunnableFactory for
* {@code BatchSaveRunnable} which will simply implement
* {@code BatchSaveRunnable} by save elements directly :
* {@code @Override
* protected void batchSave(Session session, List{@literal } list) {
* for(E e:list){
* session.save(e);
* }
* }
*
*
* @param The type of element to be saved
*/
public class DefaultBatchSaveRunnableFactory implements ConsumerRunnableFactory{
@Override
public ConsumerRunnable newConsumerRunnable(int batchSize, long time,
TimeUnit unit, BlockingQueue queue) {
return new SimpleBatchSaveRunnable(batchSize, time, unit, queue);
}
}
SimpleBatchSaveRunnable.java:
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import org.hibernate.Session;
/**
* A simple implementation of {@code BatchSaveRunnable} :
* {@code @Override
* protected void batchSave(Session session, List{@literal } list) {
* for(E e:list){
* session.save(e);
* }
* }
*
*
* @param The type of element to be saved
*/
public class SimpleBatchSaveRunnable extends BatchSaveRunnable{
public SimpleBatchSaveRunnable(int batchSize, long time, TimeUnit unit,
BlockingQueue queue) {
super(batchSize, time, unit, queue);
}
@Override
protected void batchSave(Session session, List list) {
DBUtil.batchSave(session, list);
}
}
DBUtil.java:
import java.util.Collection;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.hibernate.Session;
import org.hibernate.Transaction;
public final class DBUtil {
private static final Log log = LogFactory.getLog(DBUtil.class);
private DBUtil(){}
/**
* Force the underlying transaction to roll back.
* @param transaction
*/
public static void rollback(Transaction transaction) {
if ((transaction != null) && (transaction.isActive())) {
log.warn(" rollbacking the transaction...");
transaction.rollback();
}
}
public static void commit(Transaction transaction){
if(transaction!=null&&transaction.isActive()){
log.warn(" committing the transaction...");
transaction.commit();
}
}
public static void commitAndClose(Session session){
if (session != null && session.isOpen()){
commit(session.getTransaction());
closeSession(session);
}
}
public static Transaction beginTransaction(Session session){
log.warn(" begining the transaction...");
return session.beginTransaction();
}
public static void closeSession(Session session){
if(session!=null){
try {
if(session.isOpen()){
log.warn(" closing the session...");
session.close();
}
} catch (Throwable t) {
log.error("An error has occurred when closing the db session!", t);
}
}
}
public static Session newSession(){
log.warn(" opening a new session...");
//Opens a new session
//TODO just return null as example
return null;
}
/**
* roll back transaction and then close the session
* @param session
*/
public static void rollbackAndClose(Session session) {
try {
if (session != null && session.isOpen()) {
Transaction transaction = session.getTransaction();
rollback(transaction);
closeSession(session);
}
} catch (Throwable rbEx) {
log.error(" An error has occurred when rollbacking the transaction!", rbEx);
}
}
/**
* Batch save elements:
* {@code @Override
* protected void batchSave(Session session, Collection{@literal } col) {
* for(E e:col){
* session.save(e);
* }
* }
*
* @param session DB Session
* @param col the elements to be saved
* @param E The type of element to be saved
*/
public static void batchSave(Session session, Collection col){
if(col==null||col.isEmpty()){
return;
}
for(E e:col){
session.save(e);
}
}
}
PracticalUtils.java:
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public final class PracticalUtils {
private PracticalUtils(){}
/**
* Checks if a String is empty ("") or null.
*
* StringUtils.isEmpty(null) = true
* StringUtils.isEmpty("") = true
* StringUtils.isEmpty(" ") = false
* StringUtils.isEmpty("bob") = false
* StringUtils.isEmpty(" bob ") = false
*
* @param str the String to check, may be null
* @return true
if the String is empty or null
*/
public static boolean isEmpty(String str) {
return str == null || str.length() == 0;
}
/**
* Checks if there is empty string
* @param ss the strings to be checked
* @return true if there is any empty string in specified parameters, false otherwise
*/
public static boolean hasEmpty(String... ss){
if(ss==null){
return true;
}
for(String s:ss){
if(s==null||s.isEmpty()){
return true;
}
}
return false;
}
/**
* 判断是否存在null对象
* @param objs 那些需要检查的对象
* @return true 存在,false 不存在
*/
@SafeVarargs
public static boolean hasNull(T... objs){
if(objs==null)
return true;
for(Object obj:objs){
if(obj==null)
return true;
}
return false;
}
/**
* 如果参数obj为null则返回空字符串"",否则返回obj.toString
* @param obj
* @return
*/
public static String nullToEmpty(Object obj){
if(obj==null)
return "";
return obj.toString();
}
/**
* 1. 获取参数obj的longValue,如果obj是Number的实例的话。
* 2. 如果obj是String实例的话 尝试调用Long.parseLong,失败则返回0。
* @param obj
* @return obj.longValue or 0 if parse failed
*/
public static long getLongVal(Object obj){
long val = 0;
if(obj==null){
return val;
}
if(obj instanceof Number){
val = ((Number)obj).longValue();
}else if(obj instanceof String){
String s = (String)obj;
if(s.isEmpty()){
return val;
}
try {
val = Long.parseLong(s);
} catch (NumberFormatException ignore) {}
}
return val;
}
/**
* 1. 获取参数obj的intValue,如果obj是Number的实例的话。
* 2. 如果obj是String实例的话 尝试调用Integer.parseInt,失败则返回0。
* @param obj
* @return obj.intValue or 0 if parse failed
*/
public static int getIntVal(Object obj){
int val = 0;
if(obj==null){
return val;
}
if(obj instanceof Number){
val = ((Number)obj).intValue();
}else if(obj instanceof String){
String s = (String)obj;
if(s.isEmpty()){
return val;
}
try {
val = Integer.parseInt(s);
} catch (NumberFormatException ignore) {}
}
return val;
}
/**
* 以逗号分隔的long字符串转为List
* @param strDigits
* @return
* @throws IllegalArgumentException 如果其中的字符无法转换为long类型
*/
public static List getLongsBySplitComma(String strDigits) {
if(isEmpty(strDigits))
return Collections.emptyList();
String[] digis = strDigits.split(",");
List list = new ArrayList<>(digis.length);
for(String digi:digis){
try {
list.add(Long.valueOf(digi));
} catch (Exception e) {
throw new IllegalArgumentException("["+digi+"]无法转换成long类型");
}
}
return list;
}
/**
* 以逗号分隔的int字符串转为List
* @param strDigits
* @return
* @throws IllegalArgumentException 如果其中的字符无法转换为long类型
*/
public static List getIntsBySplitComma(String strDigits) {
if(isEmpty(strDigits))
return Collections.emptyList();
String[] digis = strDigits.split(",");
List list = new ArrayList<>(digis.length);
for(String digi:digis){
try {
list.add(Integer.valueOf(digi));
} catch (Exception e) {
throw new IllegalArgumentException("["+digi+"]无法转换成int类型");
}
}
return list;
}
/**
* 获取参数obj对应的boolean值
*
1. 如果obj是Number的实例的话,只有Number.intValue==1才返回true
*
2. 如果obj是Boolean的实例的话,直接返回该Boolean
*
3. 如果obj是String的实例的话,只有是"true"(不区分大小写)或"1"才返回true
*
3. 如果obj是Character的实例的话,只有Character.charValue=='1'才返回true
*
否则返回false
* @param obj
* @return
*/
public static boolean getBooleanVal(Object obj){
if(obj instanceof Boolean){
return ((Boolean)obj);
}
if(obj instanceof String){
String s = ((String)obj);
return s.equals("1")||s.equalsIgnoreCase("true");
}
if(obj instanceof Number){
return ((Number)obj).intValue()==1;
}
if(obj instanceof Character){
return ((Character)obj).charValue()=='1';
}
return false;
}
/**
* 将JavaBean对象的属性-值转为Map形式,包括继承的protected、public属性。
* 优先调用属性的getXXX方法获取值
* @param javaBean 需要将属性-值转为Map的JavaBean实例
* @return 包含属性-值的Map对象,key为属性名,value为属性值
* @throws Exception
*/
public static Map beanToMap(Object javaBean) throws Exception{
final Collection excludeField = null;
return beanToMap(javaBean, excludeField);
}
/**
* 将JavaBean对象的属性-值转为Map形式,包括继承的protected、public属性。
* 优先调用属性的getXXX方法获取值
* @param javaBean javaBean 需要将属性-值转为Map的JavaBean实例
* @param excludeField 需要忽略的属性名
* @return 包含属性-值的Map对象,key为属性名,value为属性值
* @throws Exception
*/
public static Map beanToMap(Object javaBean,Collection excludeField) throws Exception{
Map map = new HashMap<>();
if(javaBean==null)
return map;
Set excludeFields = null;
if(excludeField!=null&&!excludeField.isEmpty()){
if(excludeField instanceof Set){
excludeFields = (Set) excludeField;
}else{
int size = excludeField.size()<<1;
if(size<0)//overflow
size = excludeField.size();
excludeFields = new HashSet<>(size);
excludeFields.addAll(excludeField);
}
}
Class> cls = javaBean.getClass();
superClassToMap(javaBean, cls.getSuperclass(), map, excludeFields);
beanToMap(javaBean, cls, map, false, excludeFields);
return map;
}
/**
* 将JavaBean对象的属性-值转为Map形式,包括继承的protected、public属性。
* 优先调用属性的getXXX方法获取值
* @param javaBeans 需要将属性-值转为Map的JavaBean实例集合
* @param excludeField 需要忽略的属性名
* @return 包含属性-值的Map对象集合,key为属性名,value为属性值
* @throws Exception
*/
public static List
ThreadFactorys.java:
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
*
* A utility class to offer an implementation of {@code ThreadFactory}
* conveniently which is the same as
* {@code java.util.concurrent.Executors$DefaultThreadFactory}
* but will offer a more specific thread name by the specified
* parameter {@code threadNamePrefix}
*
*/
public final class ThreadFactorys {
private ThreadFactorys(){}
/**
* Creates a new {@code ThreadFactory} with specified thread name prefix.
* The thread name will be "{@code threadNamePrefix-PoolThread-n}",
* where {@code threadNamePrefix} is the parameter passed in, and the
* {@code n} is the number of threads that have been created via this ThreadFactory.
* @param threadNamePrefix the thread name prefix
* @return a new {@code ThreadFactory}
*/
public static ThreadFactory newFactory(String threadNamePrefix){
//Copy from java.util.concurrent.Executors$DefaultThreadFactory
//to offer a more specific thread name
return new ThreadFactory(){
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix = threadNamePrefix+"-PoolThread-";
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r,
namePrefix + threadNumber.getAndIncrement());
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
};
}
}
以上只是“多生产者-多消费者”的一个简单实现,并没有什么容灾机制(比如系统意外退出时,并没有将队列里尚未消费的数据进行持久化导致消息丢失)、回调机制等,可用于异步处理一些无关紧要的事情(比如上面提到的用于异步记录用户登录登出日志,用于即使稍微有记录丢失也无关紧要的业务场景)。