1.当时的场景
开一个线程,定时check hbase,避免hbase异常时,阻塞大量请求。
2.程序伪代码
伪代码如下:
定时用的是jdk自带的工具类 Timer
具体逻辑:
private ExecutorService executorService = Executors.newSingleThreadExecutor();
public void run(){
Future<Boolean> future = null;
List<CheckResult> checkList = new Arraylist();
while(){//避免由于网络问题导致check不准确,所以失败时会重试三次
try{
// CheckThread里面是具体的check逻辑,会将check的结果放到checkList中
// 为了防止check超时,所以采用线程池的submit方法,返回一个future
future = executorService.submit(new CheckThread(checkList));
Boolean result = future.get(30,TimeUnit.SECONDS);
}catch (Exception e){
if(future != null){
// 有异常时 中断CheckThread执行
future.cancel(true);
}
}
}
// 增强for循环 遍历 CheckList,得到最终的check结果缓存
setCheckResult(checkList);
}
到这,不知道你们能不能看出来并发问题?
3.导致的问题
将程序上线到prd的问题:Timer停止运行,hbase某个RegionServer恢复正常后,由于check停止了,没有恢复RegionServer的状态,导致了部分请求到该RegionServer的请求异常。查日志,并发问题出在checkList上。
4.并发问题
CheckThread是由线程池中的线程执行,在执行异常时虽然调用了future.cancel(true);但是这个方法只是给打上了中断标记,并不能保证一定能中断线程执行。也就是说当此线程在往checkList增加内容的同时;
setCheckResult(checkList)是由主线程执行,这个方法正在遍历checkList:
for(CheckResult result : checkList){
}
增强for循环是采用的迭代器遍历,也就是说在遍历过程中如果list中的元素个数有变化就会报错(fast-fail)。
一个线程在添加内容,一个线程在用增强for循环遍历,最终也就导致了并发问题。
5.解决方案
知道原因,也就很好找解决方案了。
以上主要有两个问题,一个是Timer停止问题,一个是并发问题,当然是因为并发问题导致的Timer停止。
其实在我们这个场景下,只要Timer不停止,就算出现并发问题,影响也不大,因为check定时10秒就会执行一次,后面check结果就正常了,毕竟并发问题出现的概率不高。
解决办法:
1.将run()方法的所有方法体给try catch,不要往外抛异常(Timer执行过程中有异常就会停止执行)
2.最简单的方式,也是我采用的方式:重新new一个List对象:
//这个方法最终采用的是native方法:System.arraycopy();
List<CheckResult> list = new ArrayList(checkList);
到这就完了,这也更加警示了自己,只要涉及到多线程,就一定要小心谨慎,多多思考。因为这种问题一般测试时测不出来的。