`
lixuanbin
  • 浏览: 137696 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

Java多线程设计模式之双重检查加锁实战(Java concurrency patterns:double-checked locking)

    博客分类:
  • Java
阅读更多

一、场景
   最近需要在页面上展现一个通过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

 

  • 大小: 47 KB
  • 大小: 349.4 KB
  • 大小: 423.4 KB
  • 大小: 382.3 KB
分享到:
评论

相关推荐

    Java_9_Concurrency_Cookbook_-_Second_Edition.pdf

    - **设计模式**:讨论适用于并发场景的设计模式,如生产者消费者模式、观察者模式等。 - **测试和调试**:介绍并发程序的测试和调试方法,确保程序的稳定性和可靠性。 - **性能优化**:提供关于如何优化并发程序...

    fucking-java-concurrency::carp_streamer:简单地展示Java并发问题的案例,看看:see-no-evil_monkey:相信:monkey_face:

    Java语言标准库支持线程,语言本身(如GC )以及应用(服务器端The Server side )中会重度使用多线程。 如果不系统理解和充分分析并发逻辑,随意写代码,这样的程序用『碰巧』能运行出正确结果来形容一点都不为过...

    PHP: Coroutine-based concurrency library for PHP.zip

    PHP: Coroutine-based concurrency library for PHP.zip

    SQL Server并发性:锁定,阻止和行版本控制SQL Server Concurrency: Locking, Blocking and Row Versioning

    一本面向想要了解SQL Server并发性以及如何解决过多的阻塞或死锁问题的DBA和开发人员的书。

    java并行编程(Java Concurrency in Practice) 英文版chm

    &lt;&lt;java并行编程&gt;&gt;英文版chm格式,英文名称&lt;Java Concurrency in Practice&gt;,一直想买这本书,但总是缺货,找到了电子版,分享给大家。 Java Concurrency in Practice By Brian Goetz, Tim Peierls, Joshua Bloch,...

    Java并发编程实践(java concurrency in practice)pdf (java多线程总结.ppt)

    《Java并发编程实践》是Java开发者必读的经典之作,由Brian Goetz等多位专家共同撰写。这本书深入浅出地探讨了Java平台上的并发问题,帮助读者理解和掌握如何编写高效、可靠且可维护的多线程应用程序。以下是该书...

    RH-Tree-Concurrency2:RH-Tree-Concurrency的实现

    RH-Tree-Concurrency2项目显然关注的是如何在C++环境中实现这种数据结构,并且着重于并发控制,确保多线程环境下的正确性和性能。 RH树的核心在于它的旋转哈希策略,这使得它能够在保持平衡的同时,提供快速的查找...

    用C++ concurrencytask实现异步编程(附代码实例).rar

    C++11引入了并发编程的支持,包括`std::future`、`std::promise`以及`std::async`等工具,而`concurrency::task`是微软的PPL(Parallel Patterns Library,并行模式库)的一部分,为C++提供了更高级别的异步编程模型...

    Android代码-java-concurrency-patterns

    Java Concurrency Patterns and Features Concurrency Patterns and features found in Java, through multithreaded programming. Features: Threads and Runnables Locks Intrinsic Explicit Reentrant Read...

    JAVA线程高级-线程按序交替执行

    在Java编程中,多线程是并发编程的重要组成部分,它允许程序同时执行多个任务,从而提高了系统的效率和响应性。然而,在某些场景下,我们可能需要控制线程的执行顺序,确保它们按照特定的顺序交替运行,这在并发编程...

    Java-concurrency-master.zip

    `Java-concurrency-master.zip`这个压缩包很可能包含了关于Java并发编程的各种资料和示例,对于学习和理解Java并发机制非常有帮助。 Java并发主要包括以下几个核心知识点: 1. **线程**:Java通过`Thread`类来创建...

    java多线程下载器

    Java多线程下载器是一种利用Java编程语言实现的高效文件下载工具,它通过将大文件分割成多个小部分,然后创建多个线程同时下载这些部分,以提高下载速度。这种技术在处理大文件或者网络带宽有限的情况下尤其有用,...

    Core Java Volume II 最新第8版 part1全两卷 (附随书源码)

    Fully updated for the new Java SE 6 platform, this no-nonsense tutorial and reliable reference illuminates the most important language and library features with thoroughly tested real-world examples....

    Java并发实践英文版(Java Concurrency in Practice)

    - **书名**:《Java并发实践》(Java Concurrency in Practice) - **作者**:Brian Goetz, Tim Peierls, Joshua Bloch, Joseph Bowbeer, David Holmes, Doug Lea - **出版社**:Addison Wesley Professional - **...

    CPP-Concurrency-In-Action-2ed-2019-master.zip

    《C++并发实战第二版》是一本深入探讨C++多线程编程的权威书籍,其电子版资源以Markdown格式提供。这本书由资深C++专家Anthony Williams撰写,详细讲解了C++11及后续版本中的并发编程特性,为开发者提供了丰富的实践...

    大唐JAVA笔试题

    ### 大唐JAVA笔试题知识点解析 #### 1. 关键字public、protected、private、final的用法 ...以上知识点涵盖了Java基础、面向对象、多线程、异常处理、Web开发等多个方面,对理解和掌握Java技术栈具有重要意义。

    java_concurrency_in_practice

    《Java并发实践》是一本由Brian Göetz、Tim Peierls、Joshua Bloch、Joseph Bowbeer、David Holmes和Doug Lea等多位作者共同编著的经典之作,旨在帮助Java开发者深入理解并掌握多线程编程技术。本书由Addison-...

Global site tag (gtag.js) - Google Analytics