十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
这篇文章主要讲解了Java怎么构建高效结果缓存,内容清晰明了,对此有兴趣的小伙伴可以学习一下,相信大家阅读完之后会有帮助。
北碚网站制作公司哪家好,找创新互联!从网页设计、网站建设、微信开发、APP开发、响应式网站等网站项目制作,到程序开发,运营维护。创新互联自2013年起到现在10年的时间,我们拥有了丰富的建站经验和运维经验,来保证我们的工作的顺利进行。专注于网站建设就选创新互联。
缓存是现代应用服务器中非常常用的组件。除了第三方缓存以外,我们通常也需要在java中构建内部使用的缓存。那么怎么才能构建一个高效的缓存呢? 本文将会一步步的进行揭秘。
使用HashMap
缓存通常的用法就是构建一个内存中使用的Map,在做一个长时间的操作比如计算之前,先在Map中查询一下计算的结果是否存在,如果不存在的话再执行计算操作。
我们定义了一个代表计算的接口:
public interface Calculator { V calculate(A arg) throws InterruptedException; }
该接口定义了一个calculate方法,接收一个参数,并且返回计算的结果。
我们要定义的缓存就是这个Calculator具体实现的一个封装。
我们看下用HashMap怎么实现:
public class MemoizedCalculator1 implements Calculator { private final Map cache= new HashMap(); private final Calculator calculator; public MemoizedCalculator1(Calculator calculator){ this.calculator=calculator; } @Override public synchronized V calculate(A arg) throws InterruptedException { V result= cache.get(arg); if( result ==null ){ result= calculator.calculate(arg); cache.put(arg, result); } return result; } }
MemoizedCalculator1封装了Calculator,在调用calculate方法中,实际上调用了封装的Calculator的calculate方法。
因为HashMap不是线程安全的,所以这里我们使用了synchronized关键字,从而保证一次只有一个线程能够访问calculate方法。
虽然这样的设计能够保证程序的正确执行,但是每次只允许一个线程执行calculate操作,其他调用calculate方法的线程将会被阻塞,在多线程的执行环境中这会严重影响速度。从而导致使用缓存可能比不使用缓存需要的时间更长。
使用ConcurrentHashMap
因为HashMap不是线程安全的,那么我们可以尝试使用线程安全的ConcurrentHashMap来替代HashMap。如下所示:
public class MemoizedCalculator2 implements Calculator { private final Map cache= new ConcurrentHashMap<>(); private final Calculator calculator; public MemoizedCalculator2(Calculator calculator){ this.calculator=calculator; } @Override public V calculate(A arg) throws InterruptedException { V result= cache.get(arg); if( result ==null ){ result= calculator.calculate(arg); cache.put(arg, result); } return result; } }
上面的例子中虽然解决了之前的线程等待的问题,但是当有两个线程同时在进行同一个计算的时候,仍然不能保证缓存重用,这时候两个线程都会分别调用计算方法,从而导致重复计算。
我们希望的是如果一个线程正在做计算,其他的线程只需要等待这个线程的执行结果即可。很自然的,我们想到了之前讲到的FutureTask。FutureTask表示一个计算过程,我们可以通过调用FutureTask的get方法来获取执行的结果,如果该执行正在进行中,则会等待。
下面我们使用FutureTask来进行改写。
FutureTask
@Slf4j public class MemoizedCalculator3 implements Calculator { private final Map> cache= new ConcurrentHashMap<>(); private final Calculator calculator; public MemoizedCalculator3(Calculator calculator){ this.calculator=calculator; } @Override public V calculate(A arg) throws InterruptedException { Futurefuture= cache.get(arg); V result=null; if( future ==null ){ Callable callable= new Callable () { @Override public V call() throws Exception { return calculator.calculate(arg); } }; FutureTask futureTask= new FutureTask<>(callable); future= futureTask; cache.put(arg, futureTask); futureTask.run(); } try { result= future.get(); } catch (ExecutionException e) { log.error(e.getMessage(),e); } return result; } }
上面的例子,我们用FutureTask来封装计算,并且将FutureTask作为Map的value。
上面的例子已经体现了很好的并发性能。但是因为if语句是非原子性的,所以对这一种先检查后执行的操作,仍然可能存在同一时间调用的情况。
这个时候,我们可以借助于ConcurrentHashMap的原子性操作putIfAbsent来重写上面的类:
@Slf4j public class MemoizedCalculator4 implements Calculator { private final Map> cache= new ConcurrentHashMap<>(); private final Calculator calculator; public MemoizedCalculator4(Calculator calculator){ this.calculator=calculator; } @Override public V calculate(A arg) throws InterruptedException { while (true) { Futurefuture = cache.get(arg); V result = null; if (future == null) { Callable callable = new Callable () { @Override public V call() throws Exception { return calculator.calculate(arg); } }; FutureTask futureTask = new FutureTask<>(callable); future = cache.putIfAbsent(arg, futureTask); if (future == null) { future = futureTask; futureTask.run(); } try { result = future.get(); } catch (CancellationException e) { log.error(e.getMessage(), e); cache.remove(arg, future); } catch (ExecutionException e) { log.error(e.getMessage(), e); } return result; } } } }
上面使用了一个while循环,来判断从cache中获取的值是否存在,如果不存在则调用计算方法。
上面我们还要考虑一个缓存污染的问题,因为我们修改了缓存的结果,如果在计算的时候,计算被取消或者失败,我们需要从缓存中将FutureTask移除。
看完上述内容,是不是对Java怎么构建高效结果缓存有进一步的了解,如果还想学习更多内容,欢迎关注创新互联行业资讯频道。