`
85977328
  • 浏览: 1871504 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

java并发(二十九)构建高效且可伸缩的结果缓存

 
阅读更多
概述
    几乎所有应用程序,都会使用某种形式的缓存。重用之前的计算结果,能降低延时,提高吞吐量,但却要消耗更多的内存。用内存“换”CPU。缓存看上去非常简单,然而简单的缓存可能会将性能瓶颈装变为可伸缩性瓶颈,即使缓存是用于提升单线程的性能。笔者会循序渐进的介绍缓存的使用方法演进。

模拟定义接口和功能
声明一个计算函数,使用泛型,输入是A,输出是V。然后我们实现这个接口,再开发一个包装器,可以缓存计算的结果。
接口:
package com.chinaso.phl;

/**
 * @author piaohailin
 * @date 2014-4-23
 */
public interface Computable<A, V> {
    V compute(A arg) throws InterruptedException;
}

实现:
package com.chinaso.phl;

import java.math.BigInteger;
/**
 * @author piaohailin
 * @date 2014-4-23
 */
public class ExpensiveFunction implements Computable<String, BigInteger> {

    @Override
    public BigInteger compute(String arg) throws InterruptedException {
        return new BigInteger(arg);
    }

}


使用HashMap和同步机制来初始化缓存
package com.chinaso.phl;

import java.util.HashMap;
import java.util.Map;

import net.jcip.annotations.GuardedBy;

/**
 * @author piaohailin
 * @date 2014-4-23
 */
public class Memorizer1<A, V> implements Computable<A, V> {
    @GuardedBy("this")
    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;
    }
}

    这种方法是最基本的缓存用法,是安全的。但是有一个明显的可伸缩性问题:每次只有一个线程能够执行compute。如果另一个线程正在计算结果,那么其他调用coumpute的线程可能被阻塞很长时间。如果有多个线程在排队等待还未计算出的结果,那么compute方法的计算时间可能比没有“记忆”操作的计算时间更长。


用ConcurrentHashMap替换HashMap
package com.chinaso.phl;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author piaohailin
 * @date 2014-4-23
 */
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 Memorizer2(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比Memorizer1有着更好的并发行为,ConcurrentHashMap是线程安全的,所以不需要同步compute方法。但是作为缓存仍然有问题----2个线程同时调用的compute的时候,可能会导致计算得到相同的值。因为缓存是用来避免相同的数据被计算多次,但对于更通用的缓存机制来说,这种情况是更糟糕的,对于提供单词初始化对象缓存来说,这个漏洞会存在安全风险。

    Memorizer2问题在于,如果某个线程启动了一个开销很大的计算,而其他线程并不知道这个计算正在进行,那么很可能会重复这个计算。我们希望通过某种方法来表达“线程X正在计算f(1226)”这种情况,这样当另一个线程查找f(1226)时,他能够知道最高效的方法是等待线程X计算结束,然后去查询缓存“f(1226)的结果是多少”。


基于FutureTask的Memorizer封装器
    FutureTask表示一个计算过程,这个过程可能已经计算完成,也可能正在进行。如果有结果可用,那么FutureTask.get将立即返回结果,否则它会一直阻塞,直到结果计算出来再将其返回。
package com.chinaso.phl;

import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author piaohailin
 * @date 2014-4-23
 */
public class Memorizer3<A, V> implements Computable<A, V> {

    private final Map<A, FutureTask<V>> cache = new ConcurrentHashMap<A, FutureTask<V>>();
    private final Computable<A, V>      c;

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

    @Override
    public V compute(final A arg) throws InterruptedException {
        FutureTask<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);
            ft.run(); // 这里调用的是c.compute(arg);
        }
        try {
            return f.get();
        } catch (ExecutionException e) {
            throw new InterruptedException(e.getMessage());
        }
    }
}

    Memorizer3进一步改进了代码。基于ConcurrentHashMap表现出了更好的并发性。但是他仍然有一个漏洞,就是2个线程计算相同值的漏洞。这个漏洞的概率远远小于Memorizer2的情况。但是compute方法中的if代码块仍然是非原子的“先检查再执行”操作。


基于原子操作putIfAbsent的改进
package com.chinaso.phl;

import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
 * @author piaohailin
 * @date 2014-4-23
 */
public class Memorizer4<A, V> implements Computable<A, V> {

    private final ConcurrentMap<A, FutureTask<V>> cache = new ConcurrentHashMap<A, FutureTask<V>>();
    private final Computable<A, V>                c;

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

    @Override
    public V compute(final A arg) throws InterruptedException {
        FutureTask<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);
            // 只有第一个线程添加的时候才会为空,第二个线程此处会获取之前的FutureTask
            f = cache.putIfAbsent(arg, ft);
            if (f == null) {
                f = ft;
                ft.run(); // 这里调用的是c.compute(arg);
            }
        }
        try {
            return f.get();
        } catch (CancellationException e) {
            cache.remove(arg, f);
            return null;
        } catch (ExecutionException e) {
            throw new InterruptedException(e.getMessage());
        }
    }
}

    Memorizer4使用了putIfAbsent的原子方法,从而有效避免了Memorizer3的漏洞。但是这个缓存仍然存在问题。
    缓存污染
    缓存逾期
    缓存清理
  • 描述: 缓存1
  • 大小: 6.3 KB
  • 描述: 缓存2
  • 大小: 13.3 KB
  • 描述: 缓存3
  • 大小: 18 KB
分享到:
评论

相关推荐

    Java并发编程实战

    5.6 构建高效且可伸缩的结果缓存 第二部分 结构化并发应用程序 第6章 任务执行 6.1 在线程中执行任务 6.1.1 串行地执行任务 6.1.2 显式地为任务创建线程 6.1.3 无限制创建线程的不足 6.2 Executor框架 6.2.1...

    《java并发编程实战》读书笔记-第5章-基础构建模块

    《java并发编程实战》读书笔记-第3章-对象的共享,脑图形式,使用xmind8制作 包括同步容器类、并发容器类、阻塞队列和生产者消费者模式、阻塞和中断方法、同步工具类。最后是构建高效且可伸缩的结果缓存

    Java并发编程实践 PDF 高清版

    Java 5以及6在开发并发程序取得了显著的进步,提高了Java虚拟机的性能,提高了并发类的可伸缩性,并加入了丰富的新并发构建块。在本书中,这些便利工具的创造者不仅解释了它们究竟如何工作、如何使用,同时,还阐释...

    Java 并发编程实战

    5.6 构建高效且可伸缩的结果缓存 第二部分 结构化并发应用程序 第6章 任务执行 6.1 在线程中执行任务 6.1.1 串行地执行任务 6.1.2 显式地为任务创建线程 6.1.3 无限制创建线程的不足 6.2 Executor框架 6.2.1...

    JAVA并发编程实践_中文版(1-16章全)_1/4

    5.6 为计算结果建立高效、可伸缩的高速缓存 第2部分 构建并发应用程序 第6章 任务执行 6.1 在线程中执行任务 6.2 executor 框架 6.3 寻找可强化的并行性 第7章 取消和关闭 7.1 任务取消 7.2 停止基于线程的服务 7.3 ...

    Java并发编程part2

    5.6 为计算结果建立高效、可伸缩的高速缓存 第2部分 构建并发应用程序 第6章 任务执行 6.1 在线程中执行任务 6.2 executor 框架 6.3 寻找可强化的并行性 第7章 取消和关闭 7.1 任务取消 7.2 停止基于线程的服务 7.3 ...

    Java并发编程实践part1

    5.6 为计算结果建立高效、可伸缩的高速缓存 第2部分 构建并发应用程序 第6章 任务执行 6.1 在线程中执行任务 6.2 executor 框架 6.3 寻找可强化的并行性 第7章 取消和关闭 7.1 任务取消 7.2 停止基于线程的服务 7.3 ...

    基于jsp的JdonFramework开源框架

    JdonFramework是一个基于Java技术栈的高性能、可扩展的Web应用框架。它是基于事件驱动架构设计的,使得应用程序的设计更加灵活和可扩展。该框架广泛应用于企业级Web应用程序的开发中。 JdonFramework提供了一些优秀...

    solr 企业搜索引擎教程

     可配置的查询结果,过滤器,和文档缓存实例  可插拔的缓存实现  后台缓存热启:当一个新的搜索器被打开时,可配置的搜索将它热启,避免第一个结果慢 下来,当热启时,当前搜索器处理目前的请求(???)。  后台自动热...

    基于J2EE框架的个人博客系统项目毕业设计论文(源码和论文)

    具有很好的伸缩性,可跨越从运行Windows 95/98的膝上型电脑到运行Windows 2000的大型多处理器等多种平台使用。  6.对Web技术的支持,使用户能够很容易地将数据库中的数据发布到Web页面上。  7.SQL Server提供...

Global site tag (gtag.js) - Google Analytics