论坛首页 Java企业应用论坛

高速缓存实现

浏览 11996 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (15) :: 隐藏帖 (0)
作者 正文
   发表时间:2010-06-08  
   各位大虾,本人实现了一个高速缓存,实现方式中依赖java的concurrent包ConcurrendHashMap,贴出代码希望各位能够讨论一下如下的addElement()方法不加锁,会不会出现线程问题(依照本人的理解应该不会,由于本人才疏学浅,还望不吝赐教,另外该方法的实现是参考<<java并发编程实践>>)。
public class Cache<K, V> {
	
	private final ConcurrentHashMap<K, FutureTask<V>> cache = 
								new ConcurrentHashMap<K, FutureTask<V>>();
	
	private final ReadWriteLock lock = new ReentrantReadWriteLock();
	private final Lock readLock = lock.readLock();
//	private final Lock writeLock = lock.writeLock();
	
	public V addElement(final K key, final V value) {
		FutureTask<V> f = cache.get(key);
		while (true) {
			if (null == f) {
				Callable<V> eval = new Callable<V>() {

					@Override
					public V call() throws Exception {
						//此处实现比较简单,但是如果创建一个V的对象需要比较消耗性能的话,
						//这种缓存实现就有明显的优势
						return value;
					}
					
				};
				FutureTask<V> future = new FutureTask<V>(eval);
				f = cache.putIfAbsent(key, future);
				if (null == f) {
					f = future;
					future.run();
				}
			}
			
			try {
				return f.get();
			} catch (Exception e) {
				e.printStackTrace();
			}
		} 
	}
	
	public V getElement(K key) {
		try {
			readLock.lock();
			return cache.get(key).get();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			readLock.unlock();
		}
		return null;
	}
}

   在具体实现中getElement()方法使用了读锁从而支持并发读,在addElement()方法中没有加任何锁依靠FutureTask和实现逻辑来实现线程安全。如上实现优势: 当线程进入addElement方法后,首先判断对应key的Futuretask是否已经存在,如果不存在表明是第一次添加,然后利用Callable和Futuretask来创建一个value对象(如果创建value对象的代价非常昂贵的话,更能体现该实现的优势).假如在第一个线程正在创建value的过程中,addElement方法进入第二个线程,此时第二个线程会首先判断key对应的Futuretask是否已经存在,如不存在则利用Callable和Futuretask来创建一个value对象。试想此时的最好实现就是第二个线程判断对象是否存在或者是否正在创建,如果存在或正在创建那么最好的办法就是等待第一个线程创建完成后直接来分享胜利的果实即可,不需要再进行创建后再依靠concurrentHashMap的putIfAbsent方法来判断是否需要加入Map中,这种实现最明显的优势就是节约了创建一个复杂对象的开销,这一点也是该实现的精华所在.
以上是本人对这个方法的理解,还望各位多多拍砖。。。。。。。。
   发表时间:2010-06-08  
1. 真的不是很清楚你这个使用环境
2. ConcurrentHashMap 本身就是线程安全的,所以cache.get(key)是不会出现线程安全问题,至于后面的.get(),这是获得异步的计算结果,再结果没有出来之前一直阻塞,所以也是线程安全的。
所以你的getElement(),锁是不必要的。
3. addElement 这个方法接口上面,final V value 应该改成 Callable<V> callable吧。此外你的f.get();这个是干嘛?既然你要做缓存,当然是把任务提交就好了,需要读数据的那就用getElement读好了
4. addElement 不加锁是不行的,你得保证你判断没有k 到 添加k是个原子操作
0 请登录后投票
   发表时间:2010-06-08  
beneo 写道
1. 真的不是很清楚你这个使用环境
2. ConcurrentHashMap 本身就是线程安全的,所以cache.get(key)是不会出现线程安全问题,至于后面的.get(),这是获得异步的计算结果,再结果没有出来之前一直阻塞,所以也是线程安全的。
所以你的getElement(),锁是不必要的。
3. addElement 这个方法接口上面,final V value 应该改成 Callable<V> callable吧。此外你的f.get();这个是干嘛?既然你要做缓存,当然是把任务提交就好了,需要读数据的那就用getElement读好了
4. addElement 不加锁是不行的,你得保证你判断没有k 到 添加k是个原子操作


谢谢拍砖,针对你提出的几点问题回答如下:
1.该实现知识模拟一个缓存
2.cache.get(key)本来是线程安全的,这一点谢谢指正
3.addElement方法中用到f.get()可以返回值,同时也可以利用它实现轮询的结束(当然也可以添加break直接结束轮询)。
4.其实我贴出这段代码的主要目的就是想和大家讨论addElement方法是否可以不加锁利用Futuretask的包装来实现线程安全。在发帖的的原文中我写了一下内容" 当线程进入addElement方法后,首先判断对应key的Futuretask是否已经存在,如果不存在表明是第一次添加,然后利用Callable和Futuretask来创建一个value对象(如果创建value对象的代价非常昂贵的话,更能体现该实现的优势).假如在第一个线程正在创建value的过程中,addElement方法进入第二个线程,此时第二个线程会首先判断key对应的Futuretask是否已经存在,如不存在则利用Callable和Futuretask来创建一个value对象。试想此时的最好实现就是第二个线程判断对象是否存在或者是否正在创建,如果存在或正在创建那么最好的办法就是等待第一个线程创建完成后直接来分享胜利的果实即可,不需要再进行创建后再依靠concurrentHashMap的putIfAbsent方法来判断是否需要加入Map中,这种实现最明显的优势就是节约了创建一个复杂对象的开销,这一点也是该实现的精华所在. "
0 请登录后投票
   发表时间:2010-06-08  
我的经验是,只要涉及到写入就需要进入临界区操作,读操作不需要加锁。

感觉上对读操作加锁没有必要,楼主可以看看google collections,
Google Collections中的MapMaker融合了Weak Reference,线程安全,高并发性能,异步超时清理,自定义构建元素等强大功能于一身。
0 请登录后投票
   发表时间:2010-06-08  
cjmcn-sh 写道
我的经验是,只要涉及到写入就需要进入临界区操作,读操作不需要加锁。

感觉上对读操作加锁没有必要,楼主可以看看google collections,
Google Collections中的MapMaker融合了Weak Reference,线程安全,高并发性能,异步超时清理,自定义构建元素等强大功能于一身。

针对读加锁是不应该,这里需要修正!而我想重点和大家探讨的是addElement方法在不加锁的前提下,使用如上源代码,是否能够实现线程安全???
0 请登录后投票
   发表时间:2010-06-08  
cjmcn-sh 写道
我的经验是,只要涉及到写入就需要进入临界区操作,读操作不需要加锁。

感觉上对读操作加锁没有必要,楼主可以看看google collections,
Google Collections中的MapMaker融合了Weak Reference,线程安全,高并发性能,异步超时清理,自定义构建元素等强大功能于一身。

不对吧? 临界区数据不管读写都得有保护,写入保护是保证在写入的时候没有其它干扰,而读取的时候保护是保证此时读取到的是最新写入的数据。

不过对于缓存级别的,不注重实时更新和数据有效性 这个可以作适当放宽。大不了缓存读取不到可以读数据库,大不了数据可以是一段时间前的。如果是要求数据实时性准确性非常高的话。读写都得加锁,保证实时读写
0 请登录后投票
   发表时间:2010-06-08  
缓存么,就是要复用.你这里就是要复用每个key对应的value值,而你在Map中使用FutureTask而不是直接存value,只是为了占位(避免两个线程同时检查到key不存在,然后同时创建两个value).呵呵.我看了半天才明白:)
问题1:读写再入锁是没用的,putIfAbsent 方法其实就已经实现了防止同时写入的可能了(第一个线程占位之后,第二个线程就不可能再写入了).
问题2: while(true)实在没看明白??
问题3:之前beneo 说的 addElement 这个方法接口上面,final V value 应该改成 Callable<V> callable吧? 你传入的value值转了一圈就被直接返回了,没用啊
0 请登录后投票
   发表时间:2010-06-08   最后修改:2010-06-08
从另外的地方抄个例子过来,

ConcurrentHashMap<String,String> map; 
String getString(String name) { 
    String x = map.get(name); 
    if (x == null) { 
        x = new String(); 
        map.put(name, x); 
    } 
    return x; 
}

如果你只调用get(),或只调用put()时,ConcurrentHashMap是线程安全的。
但是,在你调用完get后,调用put之前,
如果有另外一个线程调用了map.put(name, x),
你再去执行map.put(name,x),
就很可能把前面的操作结果覆盖掉了。
所以,即使在线程安全的情况下,
你还是有可能违反原子操作的规则。
0 请登录后投票
   发表时间:2010-06-08  
wantofly_gj 写道
缓存么,就是要复用.你这里就是要复用每个key对应的value值,而你在Map中使用FutureTask而不是直接存value,只是为了占位(避免两个线程同时检查到key不存在,然后同时创建两个value).呵呵.我看了半天才明白:)
问题1:读写再入锁是没用的,putIfAbsent 方法其实就已经实现了防止同时写入的可能了(第一个线程占位之后,第二个线程就不可能再写入了).
问题2: while(true)实在没看明白??
问题3:之前beneo 说的 addElement 这个方法接口上面,final V value 应该改成 Callable<V> callable吧? 你传入的value值转了一圈就被直接返回了,没用啊

关于你提出的问题,入下说明:
1:读写锁在此吃是应该取消,因为concurrentHashMap中已经是线程安全的了。
2:putIfAbent方法防止了同时写的问题!这里主要是想说明即便此时出现了多线程的情况,当第二个线程来请求获得对应key的value时,只要发现已经有第一个线程在创建对应value后只需要等待第一个线程的结构就行了,这也就是为什么要使用Futuretask的目的,这样就可以很好的避免在创建比较消耗性能的value对象的性能损失.
3:while(true)的目的是实现轮询,如果在Futuretak执行创建value失败时轮询做该操作,知道成功推出。
4:至于将addElement方法中的value改Callable的实现,这一点只是实现方式的问题吧。
0 请登录后投票
   发表时间:2010-06-08   最后修改:2010-06-08
getElement没必要加线程锁吧,它仅仅是取值
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics