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

某些并发环境下Double-check模型的改进

    博客分类:
  • java
阅读更多

简单场景:
        多线程环境,每个线程携带惟一的key去组装数据,相同的key会有相同的数据结果。为了提高响应速度,在线程访问的入口处设置缓存。线程根据key先从缓存中取数据,如果缓存中没有,线程就去做具体的逻辑处理。

        模型如下图:假定每个线程的key如A, B等,同时有多个携带同一key的线程进来。




        最基本的处理方式如此:


private static Map<String, Object> cache 
                      = new ConcurrentHashMap<String, Object>();
		
                //Entry
		public Object run(String key) {
			Object result = cache.get(key);
			if (result == null) {
				result = doHardWork(key);
				cache.put(key, result);
			}
			
			return result;
		}
		
		private Object doHardWork(String key) {
			Object result = null;
			//Concrete work
			return result; 
		}

        它的缺点很明显,同时会有多个相同key的线程在做事,资源浪费严重。

        先看段使用Double-check模式来完成相同功能的代码:

private static Map<String, Object> cache 
							= new ConcurrentHashMap<String, Object>();
		
		public Object run(String key) {
			Object result = cache.get(key);//First checking
			if (result == null) {
				synchronized (cache) {
					result = cache.get(key);//Second checking
					if (result == null) {
						result = doHardWork(key);
						cache.put(key, result);
					}
				}
			}
			
			return result;
		}
		
		private Object doHardWork(String key) {
			Object result = null;
			
			//Concrete work
			
			return result; 
		}

        假定某个线程T1的参数是A,如果它能从Cache中取到之前A的执行结果,就立马返回。否则在同步块外等待,期望此时在同步块中有另外一个参数也是A的线程T2正在运行,然后将运行结果放入缓存中,在T2执行完成退出同步块后,T1可以从Cache读取T2的执行结果,退出请求。Double-check模型有两次对Cache内容的check,一次在同步块外,一次在同步块里面。它的执行流程如图:


        系统初始时,假定有30个参数,每个参数有10个请求线程,那么同时会有300个线程从Cache中读数据,在没有读到任何数据时,只会有一个线程进入同步块,其它299个线程在外面等着。Double-check的好处在于,每个参数第一个进入同步块的线程才会去执行正式逻辑,其它拥有同样参数的线程只要从Cache中取数据即可,效率很高。如果参数A的某个线程之前执行过,其它参数A的线程在进入同步块后,能从Cache中取到数据,立马退出同步块。但同时它的缺点就是因为有同步块的存在,每个参数的第一个线程不能并行进入具体逻辑执行过程,得一个一个的来。如此30个参数,每个参数的第一个线程得依次串行进入具体逻辑。

        对于这样的应用场景,最好的流程是:相同参数的线程只有一个进入具体逻辑,其它线程等待这个参数的执行结果,在得到结果后,直接返回;不同参数的线程在具体逻辑阶段可以并发执行。期望的执行流程如下图:




        这篇帖子的目的是改进Double-check模型的这种缺点,但不是修改Double-check来满足需求。实现可以很简单,一是多个线程的数据共享,二是对于同样参数多个线程的通知。具体模型如下图:


        从代码来看:
   /**
	 * 用来标识当前参数有线程正在做具体逻辑
	 */
	public static Object lock = new Object();
   /**
	 * 假定参数为'A',系统初始时检查lockMap中‘A’的value是否为null,如果为null,那当前线程就得做具体逻辑,把'A'的value设置为固定的lock,其它线程看到有这个lock就什么事也不做,然后suspend。当有返回数据时,将value由lock替换为正式返回数据,以在多个线程间共享
	 */
private Map<String, Object> lockMap 
                         = new ConcurrentHashMap<String, Object>();
	
   /**
	 * 所有suspend的线程都要在这里注册,以便随后得到通知
	 */
	private Map<String, List<Thread>> caller = new ConcurrentHashMap<String, List<Thread>>();

	


        它的方法有:
/*
*返回值是lock时,做具体逻辑,返回值不为lock时,是真正的返回数据,线程得到这个数据,直接返回
*/
public Object runOrWait(String key);

/*
*做具体逻辑的那个线程在做完事后,需要把result写入共享空间,让其它线程看到。然后通知所有注册这个参数的线程知道
*/
public void releaseLock(String key, Object result)


        具体程序见附件,里面有一个测试类,用来模拟测试Case。然后列举了以上出现的几种cache Demo。这个程序只是用来验证这个处理策略,对于细节问题,值得商榷,欢迎提出意见,十分感谢!
  • 大小: 17 KB
  • 大小: 18.7 KB
  • 大小: 20.4 KB
  • 大小: 51.4 KB
1
0
分享到:
评论
4 楼 langyu 2010-11-02  
tg008007x3 写道
将cache中的key构造称谓一个枚举集合,然后将下面的同步块中的参数
synchronized (cache) {  
                    result = cache.get(key);//Second checking  
                    if (result == null) {  
                        result = doHardWork(key);  
                        cache.put(key, result);  
                    }  
                }
cache改成相关的参数枚举参数,这样就能按照不同的参数key进行同步控制,从而使不同的key在第一次访问中也能够坐到异步控制。对同步的控制只与自己的key相关即可。
不知道这样做有没有错误,呵呵,随便想的。

呵呵,我这个砖头没白扔呀,果然砸出玉来。如果有枚举来控制不同参数的话,是可以实现我的要求:对相同参数,只有一个线程做具体逻辑;不同线程执行具体逻辑的线程可以并发。修改了下代码,看看是不是类似:
public static enum CacheKey {
		A, B, C, D
}

public static class EnumDemo extends Thread {
		private static Map<CacheKey, Object> cache 
					= new ConcurrentHashMap<CacheKey, Object>();
		
		public Object run(String key) {
			CacheKey cKey = CacheKey.valueOf(key);
			Object result = cache.get(cKey);//First checking
			if (result == null) {
				synchronized (cKey) {
					result = cache.get(cKey);//Second checking
					if (result == null) {
						result = doHardWork(key);
						cache.put(cKey, result);
					} 
				}
			}
			return result;
		}
		
		public void run() {
			run(this.key);
		}
	}
	
	

但这种方式存在的问题,如果前面请求的参数是后端不可预料的,那么怎么做?

顺着你的思路,我想到一种更好的方法: 既然要为相同的key加锁,那么相同的key的相同点在哪?string.intern()方法可以取出相同key的那个字符常量,做为加锁的条件:
public Object run(String key) {
			String cKey = key.intern();//literal key
			Object result = cache.get(cKey);//First checking
			if (result == null) {
				synchronized (cKey) {
					result = cache.get(cKey);//Second checking
					if (result == null) {
						result = doHardWork(key);
						cache.put(cKey, result);
					} 
				}
			}
			return result;
		}

3 楼 tg008007x3 2010-11-02  
将cache中的key构造称谓一个枚举集合,然后将下面的同步块中的参数
synchronized (cache) {  
                    result = cache.get(key);//Second checking  
                    if (result == null) {  
                        result = doHardWork(key);  
                        cache.put(key, result);  
                    }  
                }
cache改成相关的参数枚举参数,这样就能按照不同的参数key进行同步控制,从而使不同的key在第一次访问中也能够坐到异步控制。对同步的控制只与自己的key相关即可。
不知道这样做有没有错误,呵呵,随便想的。
2 楼 langyu 2010-11-01  
liugm1 写道
调用线程的suspend()和resume()不太安全
还有在EntityLock类中的runOrWait()方法,有对lockMap赋值
if (obj == null) lockMap.put(key, lock);
可能查询同一个key如'A'有多个线程执行到了这个地方,重复设置了lock,做了同样的hardWork是否又要双重检查,又回到了问题的原点。因为不排除put()方法还没执行完就跳到了下一个线程来获取值。
不知道提的是不是个问题,望楼主解答,谢谢!

首先感谢你提出的问题。
1. suspend()和resume()被认为不安全有特定的条件,在这段程序里不符合,暂时没有会出现死锁的机会。但可以对程序修改,以获得更高的安全性。

2. 会出现你所说的多个线程都可能去做具体逻辑。对于同一个参数,每个线程的执行结果是一致的,就是它们并行运行,也不会对执行结果有什么影响。这段程序的主要目的是想对于不同的参数至少并发运行一个线程。刚才修改了releaseLock()中的一行代码,对于每一个通知过的Thread,remove它,避免重复resume。

References:为什么Thread类中有这么多的Deprecated方法?

1 楼 liugm1 2010-11-01  
调用线程的suspend()和resume()不太安全
还有在EntityLock类中的runOrWait()方法,有对lockMap赋值
if (obj == null) lockMap.put(key, lock);
可能查询同一个key如'A'有多个线程执行到了这个地方,重复设置了lock,做了同样的hardWork是否又要双重检查,又回到了问题的原点。因为不排除put()方法还没执行完就跳到了下一个线程来获取值。
不知道提的是不是个问题,望楼主解答,谢谢!

相关推荐

    计算机后端-Java-Java高并发从入门到面试教程-发课程资料.zip

    - **Double-Check Locking**:理解双重检查锁定模式,以及其在单例模式中的应用。 - ** volatile与synchronized的区别**:深入探讨两者的异同。 通过本课程的学习,开发者不仅能掌握Java并发编程的基础知识,还能...

    一本经典的多线程书籍 Java并发编程 设计原则与模式 第二版 (英文原版)

    - 并发性:并发编程是系统能够同时执行多个任务的能力,是现代多核处理器环境下提高系统效率的关键。 - 线程:线程是程序执行的最小单元,一个进程可以包含多个线程。 - 并发控制:包括互斥(互斥锁)、同步...

    【面试资料】-(机构内训资料)Java并发编程面试题.zip

    - `volatile`关键字:确保变量在多线程环境下的可见性,但不保证原子性。 - `java.util.concurrent`包:提供了高级并发工具类,如`Semaphore`信号量、`CyclicBarrier`屏障、`CountDownLatch`倒计时器等。 3. **...

    Java并发编程设计原则和模式

    4. 双重检查锁定模式(Double-Check Locking):用于安全地创建单例,防止多线程环境下的多次实例化。 5. 原子操作模式:使用java.util.concurrent.atomic包中的原子类,如AtomicInteger,提供无锁的并发操作。 四...

    深入Java内存模型-JMM

    Java内存模型,简称JMM(Java Memory Model),是Java虚拟机规范中定义的一个抽象概念,它描述了在多线程环境下,如何保证各个线程对共享数据的一致性视图。JMM的主要目标是定义程序中各个变量的访问规则,以及在...

    java面试题_高并发、高可用、分布式(9题)

    - **单例模式**:在多线程环境下,双重检查锁定(Double-Check Locking)是实现线程安全的单例模式。 5. **JVM优化** 面对高并发场景,优化JVM的性能至关重要。这包括调整内存分配(堆、栈、元空间等),设置垃圾...

    java并发编程学习思维导图

    - **线程安全初始化**:使用Double-Check Locking或Initialization-on-Demand Holder Class模式初始化单例。 - **避免长时间阻塞操作**:尽量减少I/O、网络等可能导致长时间阻塞的操作在主线程中执行。 以上只是...

    Java并发编程(18)第五篇中volatile意外问题的

    在某些复杂的并发场景下,如双重检查锁定(Double-Check Locking)模式,单纯依赖volatile可能导致“幻象实例”问题,这时需要结合synchronized来保证正确初始化单例。 文档可能还分析了volatile变量在循环中的使用...

    Java Concurrency In Practice Learning Note

    - **双检锁/双重校验锁(DCL,Double-Check Locking)**:用于单例模式,保证多线程环境下的正确初始化。 8. **并发性能调优** - **线程池大小的设置**:根据系统资源和应用需求合理配置线程池的大小。 - **并发...

    并发编程面试专题_并发编程_面试_源码

    10. **线程安全问题**:了解常见的线程不安全场景,如Double-Check Locking、ThreadLocal等解决方案,并能够识别和解决这些问题。 11. **Java内存模型(JMM)**:理解Java内存模型对于线程间通信的规定,以及...

    Java并发体系.pdf

    在实际应用中,如DCL(Double Check Locking)单例模式,我们需要关注重排序可能导致的问题。为了解决这个问题,我们可以使用volatile关键字来禁止重排序,或者利用类加载机制,通过类初始化阶段的锁来确保线程安全...

    04 并发编程专题05.zip

    在并发编程领域,Java语言提供了一系列机制来保证多线程环境下的正确性和效率。"04 并发编程专题05.zip"中的内容主要聚焦于Java内存模型(JMM)和volatile关键字,这两个概念是理解Java并发编程核心的基石。 Java...

    java内存模型和一些多线程的资料

    Java内存模型(JVM Memory Model,简称JMM)是Java平台中的一个重要概念,它定义了在多线程环境下,如何在共享内存中读写变量的行为。JMM的主要目标是确保多线程环境下的可见性、有序性和原子性,从而避免数据不一致...

    深入理解 Java 内存模型

    10. **双重检查锁定(Double-Check Locking)**:这是一种常见的单例模式实现方式,通过 JMM 来保证线程安全的单例创建。 深入学习 JMM 不仅能帮助开发者理解 Java 并发的基础原理,还能提高程序的并发性能,避免因...

    【Java面试资料】-(机构内训资料)上海-拼多多-Java高级

    - 双重检查锁定(Double-Check Locking)与初始化-on-demand holder类设计模式。 5. **IO流与NIO** - 流的分类:字节流与字符流,输入流与输出流,缓冲流与转换流。 - 文件操作:File类的常用方法,文件复制与...

    Java并发编程:设计原则与模式(Concurrent.Programming.in.Java)(中英版)

    `synchronized`关键字或`Double-Check Locking`策略可用于实现线程安全的单例。 5. **守护线程模式**:用于后台服务,当所有非守护线程结束时,程序退出。Java中的`Thread.setDaemon(true)`方法可以将线程设置为...

    JSR133中文版.pdf

    4. **线程安全的初始化(Double-Check Locking)**:JSR133修正了经典的双重检查锁定模式,使其在多线程环境中能够正确工作。这涉及到对 volatile 的理解和正确使用。 5. ** Happens-Before 关系**:这是JMM中的一...

    尚硅谷大厂面试题第二季周阳主讲整理笔记

    在Java中,DCL(Double Check Locking)是一种常见的实现方式,但在多线程环境下,如果不使用volatile,可能存在指令重排导致线程安全问题。通过将instance声明为volatile,可以避免指令重排,确保线程安全。 总结...

    深入探讨Java多线程中的volatile变量共6页.pd

    在实际应用中,volatile常用于简单的状态标记,如标志位(例如:一个线程是否应该继续运行),或者是单例模式中的Double-Check Locking(双重检查锁定)中用来确保实例化过程的正确性。不过,需要注意的是,过度依赖...

Global site tag (gtag.js) - Google Analytics