最近在项目中使用了多线程生产者消费者模型来模拟消息队列处理问题,但是发现在要求线程退出时,由于没能处理好退出线程的操作造成了Tomcat进程无法停止的问题。经过一番折腾后想总结一下这方面的经验。
线程中断的方式常用的有两种,一种是使用标记位,在代码执行中通过对标记位的判断来决定是否应该退出线程的执行;另一种是使用Thread类的interrupt方法,来终止线程。
因此,刚开始时,我便采用第二种方式来中断执行的线程。在我的代码中,我使用了LinkedBlockingQueue来做为生产者和消费者任务传递的队列,这个队列的put方法和take方法会响应中断,因此只要当线程的中断方法被调用,这两个方法就会抛出InterruptedException,因此我只要在代码的逻辑中catch到这个异常,然后做一些处理就可以完成线程的退出了。
然而事实没有我想的那么好,按照上述的写法,我在线程池结束时调用Executor的shutdownnow方法,查看这个方法的源代码就知道,它会对所有的任务调用interrupt方法。因为我的代码中有很多地方调用了HBase的API,而HBase里有许多API,它们会创建新的线程执行一些如RPC这样的I/O调用,此时,由于线程的interrupt方法被调用,这些I/O调用会抛出未受检异常如
InterruptedIOException,由于这些异常是在新的线程中抛出的,我的代码就无法catch到,因此它会导致线程异常停止。从而导致tomcat进程无法正常退出。使用jdk下的jstack工具可以看到虚拟机一直在等待线程终止。因此必须要使用其它机制来使线程能正常停止。
发现问题后,我便采用设置标志位的方法来处理线程的退出行为。
在要结束线程时,设置线程的标志位,代码在判断标志位被修改后,便可执行退出。但这样会有个问题,即BlockingQueue中的代码使用的take或put方法可能会一直阻塞着,导致线程无法判断它的标志位。查阅LinkedBlockingQueue的源代码,发现好在有办法解决这个问题,那就是使用poll方法和offer方法,这两个方法可以设置一个timeout标志,当阻塞超时后,它会停止阻塞,从而使我们可以进行其它的一些操作。
下面,就是我使用一个很粗糙的方法来进行这些线程的管理。
在生产者和消费者的代码中都设置了一个私有标志位来判断线程是否应该停止,并设置了私有方法修改标志位。
首先是生产者Runnable的代码,它的大概逻辑是遍历HBase表中的每一行,然后把每行数据放入BlockingQueue中。若offer造成了阻塞,那么它会阻塞直到超时,然后遍历下一行或退出,没被遍历到的行,在下一次遍历中还会被遍历的,因此也不必担心。
package com.cyber_space.Queue;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.channels.ClosedByInterruptException;
import java.util.Iterator;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import org.apache.hadoop.hbase.client.Result;
import org.apache.hadoop.hbase.client.ResultScanner;
import org.apache.hadoop.hbase.util.Bytes;
import com.cyber_space.HBaseManager.QueueHTable;
import com.cyber_space.Queue.Operation.QueueOperation;
import com.cyber_space.util.Log;
public class Producer implements Runnable {
private QueueHTable qh;
private ResultScanner rs;
private final BlockingQueue<QueueOperation> bq;
private int sleepTime;
private volatile boolean isInterrupted = false;
public Producer(BlockingQueue<QueueOperation> bq, int sleepTime) {
this.bq = bq;
this.sleepTime = sleepTime;
}
@Override
public void run() {
Log.logger.info("生产者线程开始工作");
while (!isInterrupted) {
try {
while (bq.size() != 0) { // 队列不为空时,不向队列放入任务
TimeUnit.SECONDS.sleep(sleepTime);
Thread.yield();
}
qh = new QueueHTable();
rs = qh.getAll();
Iterator<Result> iterator = rs.iterator();
while (iterator.hasNext() && !isInterrupted) {
Result r = iterator.next();
String rowkey = Bytes.toString(r.getRow());
String tag = qh.getTag(rowkey);
String userRowKey = qh.getUserRowKey(rowkey);
String operationInfo = qh.getOperationInfo(rowkey);
QueueOperation df = new QueueOperation(operationInfo, userRowKey, rowkey, tag);
Log.logger.info("将操作" + tag + "放入队列");
bq.offer(df, 2, TimeUnit.SECONDS);
}
} catch (InterruptedException | InterruptedIOException | ClosedByInterruptException e) {
// 收到中断请求后立即退出线程
Log.logger.info("生产者收到中断请求,退出线程");
break;
} catch (Exception e) {
if (Thread.currentThread().isInterrupted())
break;
Log.logException(e);
} finally {
if (qh != null)
try {
qh.close();
} catch (IOException e) {
if (Thread.currentThread().isInterrupted())
break;
Log.logException(e);
}
}
}
Log.logger.info("生产者线程退出:" + Thread.currentThread().getId());
}
public void stopWork() {
this.isInterrupted = true;
}
}
下面的代码是消费者线程,消费者线程不断从BlockingQueue中读取数据,然后执行相应的操作,它使用的是poll方法,也设置了超时时间,超时后,代码需要进行一些逻辑判断,决定是否退出线程。
package com.cyber_space.Queue;
import java.io.IOException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import com.cyber_space.Exception.FileException;
import com.cyber_space.Queue.Operation.QueueOperation;
import com.cyber_space.util.Log;
public class Consumer implements Runnable {
BlockingQueue<QueueOperation> bq;
private volatile boolean isInterrupted = false;
public Consumer(BlockingQueue<QueueOperation> df) {
bq = df;
}
@Override
public void run() {
try {
Log.logger.info("消费者线程开始工作");
while (!isInterrupted) { // 队列中有任务时,不能因为中断而直接退出
QueueOperation df = null;
try {
df = bq.poll(2, TimeUnit.SECONDS);
if (df == null) {
continue;
}
df.doOperation();
} catch (InterruptedException e) { // 队列中的数据就算没处理也不会丢失,因为队列数据是处理后才删除hbase中的表数据的
Log.logger.info("收到中断请求,退出线程");
break;
} catch (FileException e) {
Log.logException(e);
Log.storeSystemLog("删除队列删除文件发生错误", e, Log.LogSign.DELETE_FAILED, "删除队列处理线程", "删除队列处理线程");
bq.add(df);
}
}
Log.logger.info("消费者线程退出" + Thread.currentThread().getId());
} catch (IOException e) {
Log.logException(e);
}
}
public void stopWork() {
this.isInterrupted = true;
}
}
package com.cyber_space.Queue;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import com.cyber_space.util.Log;
public class QueueExcutor {
ExecutorService exec = Executors.newFixedThreadPool(10);
ArrayList<Runnable> runnables = new ArrayList<>();
public void execute(Runnable r) {
if (r instanceof Producer || r instanceof Consumer) {
runnables.add(r);
}
exec.execute(r);
}
public void shutdown() {
for (Runnable r : runnables) {
if (r instanceof Consumer) {
Consumer c = (Consumer) r;
c.stopWork();
} else if (r instanceof Producer) {
Producer p = (Producer) r;
p.stopWork();
}
}
try {
TimeUnit.SECONDS.sleep(Queue.SLEEP_TIME);
} catch (InterruptedException e) {
Log.logException(e);
}
exec.shutdown();
}
public void awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
exec.awaitTermination(timeout, unit);
}
}
然后针对线程池的启动和停止,只要在Tomcat的Listener框架下写好,就行了。在contextInitialized方法中启动线程池,在contextDestroyed线程池停止线程。
package com.cyber_space.Listener;
import java.util.concurrent.TimeUnit;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import com.cyber_space.Queue.Consumer;
import com.cyber_space.Queue.Queue;
import com.cyber_space.Queue.QueueExcutor;
import com.cyber_space.Queue.Producer;
import com.cyber_space.util.Log;
@WebListener()
public class QueueListener implements ServletContextListener {
Queue dq;
QueueExcutor exec = new QueueExcutor();
public void contextDestroyed(ServletContextEvent arg0) {
Log.logger.info("删除队列准备停止");
if (exec != null) {
exec.shutdown();
try {
exec.awaitTermination(3, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Log.logException(e);
}
}
}
public void contextInitialized(ServletContextEvent arg0) {
Log.logger.info("删除队列开始工作");
try {
dq = new Queue();
exec.execute(new Producer(dq.getQueue(), Queue.SLEEP_TIME));
for (int i = 0; i < Queue.CONSUMER_NUM; ++i) {
exec.execute(new Consumer(dq.getQueue()));
}
} catch (Exception e) {
Log.logException(e);
}
}
}
以上的代码便能保证在Tomcat中正常的启动和终止这个线程池。这个代码其实太粗糙,其实可以创建ThreadPoolExecutor实例时定制自己的ThreadFactory,这个ThreadFactory创建我们自定义的线程,重写其中的interrupt方法,在停止操作前,首先调用stopWork方法。还是有时间再做吧,放假了。。