一种高效可伸缩的缓存设计方法

几乎所有的服务器应用中都要使用缓存,重用之前的计算结果能降低延迟,提高吞吐量,但是要消耗更多的内存。

Memorizer1简单地使用HashMap来缓存之前的计算结果:

public interface Computable<A,V> {
	V compute(A arg) throws InterruptedException;
}

public class Memorizer1<A,V> implements Computable<A,V> {

	private final Map<A,V> cache=new HashMap<A,V>();
	private final Computable<A,V> c;
	
	public Memorizer1(Computable<A,V> c){
		this.c=c;
	}
	
	@Override
	public synchronized V compute(A arg) throws InterruptedException {
		V result=cache.get(arg);
		if(result==null){
			result=c.compute(arg);
			cache.put(arg, result);
		}
		return result;
	}
	
}

由于HashMap不是线程安全的,所以Memorizer1采用了一种保守的策略:对整个compute方法进行同步。并行性很低。

用ConcurrentHashMap来代替HashMap就不需要对compute方法进行同步了。这便有了Memorizer2:

public class Memorizer2<A,V> implements Computable<A,V> {
	private final Map<A,V> cache=new ConcurrentHashMap<A,V>();
	private final Computable<A,V> c;
	
	public Memorizer1(Computable<A,V> c){this.c=c;}
	
	@Override
	public V compute(A arg) throws InterruptedException {
		V result=cache.get(arg);
		if(result==null){
			result=c.compute(arg);
			cache.put(arg, result);
		}
		return result;
	}
}
Memorizer2存在的问题是:当某个线程启动了一个开销很大的计算,而其他线程并不知道这个计算正在进行,那么很可能会重复这个计算。

Memorizer4首先检查某个相应的计算是否已经启动(与Memorizer2不同,它是首先检查某个计算是否已经完成)。如果还没有启动,就创建一个FutureTask,并注册到Map中,然后启动计算;如果已经启动,那就等待现有计算的结果。

public class Memorizer3<A, V> implements Computable<A, V> {
	private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
	private final Computable<A, V> c;

	public Memorizer1(Computable<A, V> c) {
		this.c = c;
	}

	@Override
	public V compute(final A arg) throws InterruptedException {
		Future<V> f = cache.get(arg);
		if (f == null) {
			Callable<V> eval = new Callable<V>() {
				@Override
				public V call() throws Exception {
					return c.compute(arg);
				}
			};
			FutureTask<V> ft = new FutureTask<V>(eval);
			f = ft;
			cache.put(arg, ft); // 把FutureTask放入Map
			ft.run(); // 启动计算
		}
		try {
			return f.get(); // 等待计算完成
		} catch (ExecutionException e) {
			throw launderThrowable(e.getCause());
		}
	}

	// 对各类异常分别进行处理
	private RuntimeException launderThrowable(Throwable cause) {
		if (cause instanceof RuntimeException)
			return (RuntimeException) cause;
		else if (cause instanceof Error)
			throw (Error) cause;
		else
			throw new IllegalStateException("Not unckecked", cause);
	}
}

同Memorizer2一样,Memorizer3仍然有可能导致同样的计算重复进行,当然这个可能性比Memorizer2小很多。由于在compute方法采用了“先检查再执行”操作,有可能两个线程在检查时都发现相应的FutureTask不在Map中,导致重复的计算。

Memorizer4使用了ConcurrentMap的原子操作putIfAbsent,避免了Memorizer3的漏洞。

public class Memorizer4<A, V> implements Computable<A, V> {
	private final ConcurrentMap<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
	private final Computable<A, V> c;

	public Memorizer1(Computable<A, V> c) {
		this.c = c;
	}

	@Override
	public V compute(final A arg) throws InterruptedException {
		while (true) {
			Future<V> f = cache.get(arg);
			if (f == null) {
				Callable<V> eval = new Callable<V>() {
					@Override
					public V call() throws Exception {
						return c.compute(arg);
					}
				};
				FutureTask<V> ft = new FutureTask<V>(eval);
				f = cache.putIfAbsent(arg, ft);
				if (f == null) {
					f = ft;
					ft.run();
				}
			}
			try {
				return f.get(); // 等待计算完成
			} catch (CancellationException e) {
				cache.remove(arg, f);   //如果计算没有完成Task就取消了,那它应该从Map中移除。
			} catch (ExecutionException e) {
				throw launderThrowable(e.getCause());
			}
		}
	}

	// 对各类异常分别进行处理
	private RuntimeException launderThrowable(Throwable cause) {
		if (cause instanceof RuntimeException)
			return (RuntimeException) cause;
		else if (cause instanceof Error)
			throw (Error) cause;
		else
			throw new IllegalStateException("Not unckecked", cause);
	}
}
注意putIfAbsent是在ConcurrentMap类中定义的方法,所以这次的cache声明时是ConcurrentMap,而非Map。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值