Java多线程设计模式之双重检查加锁实战(Java concurrency patterns:double-checked locking)...
一、场景
最近需要在页面上展现一个通过http请求微信服务接口而生成的带参二维码,用户扫描后可以体验到关注公众号、显示一些动态消息、注册会员等功能。然而在测试的中发现通过微信接口生成二维码这个过程偶尔会发生超时或者其他异常,这时候需要把图片替换为一张静态的二维码图片;如果这种情况在一段期间内反复发生(譬如微信接口服务突然挂了),将会影响用户使用我们系统的体验,因此需要有个fall back的策略。
二、静默模式策略
设置请求接口生成动态二维码的http超时和线程超时时间,如果请求超时或其他连接异常连续发生次数超过预设的阀值(譬如5次),就进入静默模式for 30分钟(一般说来,宕机恢复、故障排查要求在30分钟内完成),这段期间内收到的新用户请求一律直接返回静态图片,直至30分钟后再清零计数器和计时器,重新尝试生成动态图片。
三、分析设计
1.处理流程图
2.代码
变量声明:
/**
* atomic wx error counter, ranging from 0 to {@link #wxErrorThreshHold}
* (might be more in concurrenct situation, but never mind, not a very big deal)
*/
private AtomicInteger wxErrorCount = new AtomicInteger(0);
/**
* silent mode indicator<br/>
* 1.{@link #wxErrorThreshHold} successive wx barcode errors or timeouts will trigger this variable to be true;<br/>
* 2.then turn back to false after {@link #wxErrorSilentMinutes} minutes.
*/
private volatile boolean isSilentModeActivated = false;
private ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);// one is enough;
private int wxErrorThreshHold = 5;
private int wxErrorSilentMinutes = 30;
处理流程:
/**
* load barcode image: <br/>
* 1.from wx server if error counter does not exceeds {@link #wxErrorThreshHold}; or else,<br/>
* 2.from local storage;<br/>
* and everytime the first thread that exceeds the threshhold, it shall: <br/>
* 1.turn on the silent mode;<br/>
* 2.schedule exit silent mode task.
*/
public BufferedImage loadImage(WxBarcodeParamDto param) {
BufferedImage image = null;
long begin = System.currentTimeMillis();
LOGGER.info("counter: " + wxErrorCount.get() + ", thresh hold: "
+ wxErrorThreshHold);
if (wxErrorCount.get() < wxErrorThreshHold) {
image = loadImageFromWx(param);
if (image == null) {
wxErrorCount.incrementAndGet();
LOGGER.info("result image is null, using default...");
image = fallback2DefaultBarcodeImage();
}
} else {
LOGGER.info("Fuck ya! Wx server disappoints me again! Fall back on the static image directly for some time.");
image = fallback2DefaultBarcodeImage();
enterExitSilentMode();
}
long end = System.currentTimeMillis();
LOGGER.info("consumed: " + (end - begin) + " ms.");
return image;
}
private void enterExitSilentMode() {
if (!isSilentModeActivated) {
// enter silent mode
isSilentModeActivated = true;
Runnable task = new ExitSilentModeTask();
// remain silent for some time
scheduler.schedule(task, wxErrorSilentMinutes,
TimeUnit.MINUTES);
LOGGER.info("exit silent mode task scheduled...");
} else {
LOGGER.info("nothing to do...");
}
}
class ExitSilentModeTask implements Runnable {
@Override
public void run() {
LOGGER.info("time's up, got to exit the silent mode...");
// clear the counter so that the next invoke will retry to fetch image from wx server
wxErrorCount.set(0);
// exit silent mode to make it possible to re-enter silent mode if wx server fails again
isSilentModeActivated = false;
}
}
3.并发安全性分析
为保证原子性的递增操作,实例变量errorCount被声明为AtomicInteger(顺便说下AtomicInteger的内部实现:包装了一个volatile int确保线程可见性,其get、set等无锁方法是通过sun.misc.Unsafe的jni方法实现原子性,底层是基于硬件提供原子性操作的CAS算法)。一般来说,在原子性操作的保护下,这个计数器变量取值应该位于[0, 5),但是在并发环境下,仍然有可能会大于5.譬如,在某个时刻有10个用户并发调用loadImage方法,这时候如果刚好微信的服务器卡壳了,那么5秒超时后就会对其进行10次自增操作;而且这5秒内新到达的用户请求,由于计数器仍未来得及更新,也会继续尝试去请求微信服务器获取图片,致使它们后面同样地遭受失败。不过这也是没有办法的事,从尽可能保证并发执行效率以及控制代码复杂度的角度出发,我们总得做出点权衡和牺牲,不是么?
isSilentMode声明为volatile boolean,以确保并发可见性。其作为一个开关变量,是进入和离开静默模式的依据。状态翻转的操作被封装在enterExitSilentMode方法和内部类ExitSilentModeTask中,但目前的代码实现在并发条件下存在着安全隐患。请看下面示意图分析:
在n个并发请求的情况下,有可能会有m[1,n]条线程看见当前isSilentModeActivated变量为false而将该变量翻转了m次,并预埋了m条离开静默模式的任务!(有兴趣的观众可以刨一下java api文档,看看ThreadPoolExecutor和ScheduledThreadPoolExecutor的相关说明,设想并验证一下设置了core size为1以后再向scheduler提交多个延时任务会出现什么后果)
既然有此隐患,那么我们就采用同步的策略如何?把方法enterExitSilentMode声明为synchronized就能解决问题了吧?请看下面示意图:
采取方法同步后,可以看到,并发的隐患被消除了。同步方法其实就是对当前对象this进行加锁,我们以上的处理代码是放在一个singleton的spring bean中,多个线程调用这个对象上的同步方法由于对象锁的缘故必须被串行化执行。这样问题就圆满解决了?且慢,客官请仔细想一下:这样处理的后果会导致当执行流程进入到[wxErrorCount.get() >= wxErrorThreshHold]的处理分支的时候,暴露对外的loadImage方法也间接性地变成同步方法了。大家都知道,同步会使系统处理的效率急剧降低,而且,只有当每次产生竞争进入到silent mode的时候(即!isSilentModeActivated为true)才需要同步,其他的时候并不需要同步。这样的处理方式会导致大量不必要的同步!
那么,有没有更好的解决方法呢?答案是有的。当当当!接下来,有请我们今日要介绍的主角登场——
4.双重检查加锁模式(double-checked locking)
回顾一下我们引入同步前的那个执行示意图,引起问题的是见到了isSilentModeActivated变量为false而同时尝试进入enter silent mode处理分支的那m条线程,如果我们能想办法对它们的行为做出约束,只放一条线程进入该处理分支,那么问题不就解决了?
回想一下设计模式范例,以lazy init的方式实现单例模式时,可以采用双重检查加锁的策略,防止并发导致意外创建多个对象。那么是否可以把这个策略应用到我们的设计上呢?嗯,事不宜迟,我们马上着手进行改造:
代码:
/**
* use double checked lock to enter and exit silent mode
*/
private void doubleCheckLockEnterExitSilentMode() {
if (!isSilentModeActivated) {
// use double checked lock strategy to avoid collision or duplicate execution
LOGGER.info("first check passed..");
synchronized (this) {
LOGGER.info("lock acquired..");
if (!isSilentModeActivated) {
LOGGER.info("second check passed, entering silent mode..");
// enter silent mode
isSilentModeActivated = true;
Runnable task = new ExitSilentModeTask();
// remain silent for some time
scheduler.schedule(task, wxErrorSilentMinutes,
TimeUnit.MINUTES);
LOGGER.info("exit silent mode task scheduled...");
} else {
// nothing to do, cos some other thread has done the dirty stuff
LOGGER.info("lock acquired but failed the second check, nothing to do...");
}
}
} else {
LOGGER.info("failed the first check, nothing to do...");
}
}
示意图:
至此,问题终于得到比较满意的解决。另外,值得一提的是,采取此策略实现单例模式,还有一些其他的细节需要细心处理。详情请参看其他相关的参考资料。
四、测试验证
源代码中已经添加了大量的日志覆盖执行路径,我们直接跑一下看看执行效果如何:
2014-11-13 15:38:23 ERROR WxBarcodeLoader .loadImage - counter: 1, thresh hold: 5 。。。。。。 2014-11-13 15:43:10 ERROR WxBarcodeLoader .loadImage - counter: 5, thresh hold: 5 2014-11-13 15:43:10 ERROR WxBarcodeLoader .loadImage - Fuck ya! Wx server disappoints me again! Fall back on the static image directly for some time. 2014-11-13 15:43:10 ERROR WxBarcodeLoader .loadImage - first check passed.. 2014-11-13 15:43:10 ERROR WxBarcodeLoader .loadImage - lock acquired.. 2014-11-13 15:43:10 ERROR WxBarcodeLoader .loadImage - second check passed, entering silent mode.. 。。。。。。 2014-11-13 15:43:10 ERROR WxBarcodeLoader .loadImage - time's up, got to exit the silent mode...
暂时未捕捉到second check failed的日志,日后写好测试stub,开线程压出来这种情况后补充上来。暂时请各位客官自行脑补一下。
五、参考资料
jdk-7u45-apidocs
http://en.wikipedia.org/wiki/Double-checked_locking
http://www.cnblogs.com/wenjiang/p/3276433.html
http://cantellow.iteye.com/blog/838473
《Head First Design Patterns》,ISBN: 0596007124
下一篇: 按顺序读取选中的复选框