设计高效的线程安全的缓存--JCIP5.6读书笔记

标签: 设计 线程安全 缓存 | 发表时间:2012-04-01 22:49 | 作者:
出处:http://www.iteye.com

[本文是我对Java Concurrency In Practice 5.6的归纳和总结.  转载请注明作者和出处,  如有谬误, 欢迎在评论中指正. ] 

几乎每一个应用都会使用到缓存, 但是设计高效的线程安全的缓存并不简单. 如:

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

public class ExpensiveFunction 
        implements Computable<String, BigInteger> { 
    // 模拟一个耗时操作
    public BigInteger compute(String arg) { 
	// ...
        return new BigInteger(arg); 
    } 
} 

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; 
    } 
    // 使用synchronized同步整个方法解决线程安全
    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; 
    } 
}

Memorizer1使用HashMap缓存计算结果. 如果能在缓存中取出参数对应的结果, 就直接返回缓存的数据, 避免了重复进行代价昂贵的计算. 由于HashMap不是线程安全的, Memorizer1同步整个compute方法, 避免重复计算的同时, 牺牲了并发执行compute方法的机会, 此种设计甚至可能导致性能比没有缓存更差.

使用ConcurrentHashMap代替HashMap, 同时取消对compute方法的同步可以极大的改善性能:

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; } 

    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; 
    } 
} 

ConcurrentHashMap是线程安全的, 并且具有极好的并发性能. 但是该设计仍存在问题: 无法避免所有的重复的计算. 有时这是可以的, 但对于一些要求苛刻的系统, 重复计算可能会引发严重的问题. Memorizer2的问题在于一个线程在执行compute方法的过程中, 其他线程以相同的参数调用compute方法时, 无法从缓存中获知已有线程正在进行该参数的计算的信息, 因此造成了重复计算的发生. 针对这一点, 可以改进缓存的设计:

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

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

    public V compute(final A arg) throws InterruptedException { 
        Future<V> f = cache.get(arg); 
        if (f == null) { 
            Callable<V> eval = new Callable<V>() { 
                public V call() throws InterruptedException { 
                    return c.compute(arg); 
                } 
            }; 
            FutureTask<V> ft = new FutureTask<V>(eval); 
            f = ft; 
	    // 在计算开始前就将Future对象存入缓存中.
            cache.put(arg, ft); 
            ft.run(); // call to c.compute happens here 
        } 
        try { 
	    // 如果缓存中存在arg对应的Future对象, 就直接调用该Future对象的get方法.
	    // 如果实际的计算还在进行当中, get方法将被阻塞, 直到计算完成
            return f.get(); 
        } catch (ExecutionException e) { 
            throw launderThrowable(e.getCause()); 
        } 
    } 
} 

Memorizer3中的缓存系统看起来已经相当完美: 具有极好的并发性能, 也不会存在重复计算的问题. 真的吗? 不幸的是Memorizer3仍然存在重复计算的问题, 只是相对于Memorizer2, 重复计算的概率降低了一些. cache.get(arg)的结果为null, 不代表cache.put(arg, ft)时cache中依旧没有arg对应的Future, 因此直接调用cache.put(arg, ft)是不合理的:

public class Memorizer<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 Memorizer(Computable<A, V> c) { this.c = c; } 

    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>() { 
                    public V call() throws InterruptedException { 
                        return c.compute(arg); 
                    } 
                }; 
                FutureTask<V> ft = new FutureTask<V>(eval); 
		// 使用putIfAbsent测试是否真的将ft存入了缓存, 如果存入失败, 说明cache中已经存在arg对应的future对象
		// 否则才进行计算.
                f = cache.putIfAbsent(arg, ft); 
                if (f == null) { f = ft; ft.run(); } 
            } 
            try { 
                return f.get(); 
            } catch (CancellationException e) { 
		// 当计算被取消时, 从缓存中移除arg-f键值对
                cache.remove(arg, f); 
            } catch (ExecutionException e) { 
                throw launderThrowable(e.getCause()); 
            } 
        } 
    } 
} 

至此才真正实现了高效且线程安全的缓存.

 

PS: 终于看完了JCIP的第五章, 这一章真是又臭又长...

 



已有 0 人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



相关 [设计 线程安全 缓存] 推荐:

设计高效的线程安全的缓存--JCIP5.6读书笔记

- - ITeye博客
[本文是我对Java Concurrency In Practice 5.6的归纳和总结.  转载请注明作者和出处,  如有谬误, 欢迎在评论中指正. 几乎每一个应用都会使用到缓存, 但是设计高效的线程安全的缓存并不简单. // 使用synchronized同步整个方法解决线程安全. Memorizer1使用HashMap缓存计算结果.

APP 缓存数据线程安全问题探讨

- - bang’s blog
一般一个 iOS APP 做的事就是:请求数据->保存数据->展示数据,一般用 Sqlite 作为持久存储层,保存从网络拉取的数据,下次读取可以直接从 Sqlite DB 读取. 我们先忽略从网络请求数据这一环节,假设数据已经保存在 DB 里,那我们要做的事就是,ViewController 从 DB 取数据,再传给 view 渲染:.

缓存设计的一些思考

- Sepher - NOSQL Notes
互联网架构中缓存无处不在,某厂牛人曾经说过:”缓存就像清凉油,哪里不舒服,抹一下就好了”. 高品质的存储容量小,价格高;低品质存储容量大,价格低,缓存的目的就在于”扩充”高品质存储的容量. 缓存的技术点包括内存管理和替换算法. LRU是使用最多的替换算法,每次淘汰最久没有使用的元素. LRU缓存实现分为两个部分:Hash表和LRU链表,Hash表用于查找缓存中的元素,LRU链表用于淘汰.

Web应用的缓存设计模式

- - robbin的自言自语
从10年前的2003年开始,在Web应用领域,ORM(对象-关系映射)框架就开始逐渐普及,并且流行开来,其中最广为人知的就是Java的开源ORM框架Hibernate,后来Hibernate也成为了EJB3的实现框架;2005年以后,ORM开始普及到其他编程语言领域,其中最有名气的是Ruby on rails框架的ORM - ActiveRecord.

“本地缓存”架构设计

- - ITeye博客
最近在做的项目其实是对老系统的一个深度改造,在老系统里缓存使用这块感觉有些瑕疵. 在老系统里不管是“配置数据”还是“业务数据”都统一使用redis作为缓存. “业务数据”使用redis作为缓存无可厚非,但“配置数据”使用使用redis就感觉不是很妥. 首先:过渡依赖redis,一些开关配置都依赖redis,如果redis服务挂掉整个服务瘫痪;.

什么是线程安全

- - CSDN博客编程语言推荐文章
线程安全是多线程领域的问题,线程安全可以简单理解为一个方法或者一个实例可以在多线程环境中使用而不会出现问题. 在同一程序中运行多个线程本身不会导致问题,问题在于多个线程访问了相同的资源. 如,同一内存区(变量,数组,或对象)、系统(数据库,web services等)或文件. 实际上,这些问题只有在一或多个线程向这些资源做了写操作时才有可能发生,只要资源没有发生变化,多个线程读取相同的资源就是安全的.

Servlet是否线程安全

- - 研发管理 - ITeye博客
Servlet是线程安全吗. 要解决这个问题,首先要知道什么是线程安全:.   如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码. 如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的. 或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题.

web开发中的线程安全

- - 编程 - 编程语言 - ITeye博客
在web开发中,要关注由于并发访问所导致的对某一同一个值的修改,否则信息会造成泄漏servlet是在多线程环境下的. 即可能有多个请求发给一个servelt实例,每个请求是一个线程. struts下的action也类似,同样在多线程环境下. 译:为多线程环境编写代码. 我们的controller servlet指挥创建你的Action 类的一个实例,用此实例来服务所有的请求.

《深入理解mybatis原理》 MyBatis的二级缓存的设计原理

- - CSDN博客架构设计推荐文章
       MyBatis的二级缓存是Application级别的缓存,它可以提高对数据库查询的效率,以提高应用的性能. 本文将全面分析MyBatis的二级缓存的设计原理. 1.MyBatis的缓存机制整体设计以及二级缓存的工作模式.      如上图所示,当开一个会话时,一个 SqlSession对象会使用一个 Executor对象来完成会话操作, MyBatis的二级缓存机制的关键就是对这个 Executor对象做文章.

日访问量百亿级的应用如何做缓存架构设计

- - IT瘾-dev
中生代技术链接技术大咖,分享技术干货. 链接3000+技术总监/CTO, 每天早上推送技术干货文章. 微博日活跃用户1.6亿+,每日访问量达百亿级,面对庞大用户群的海量访问,良好架构且不断改进的缓存体系具有非常重要的支撑作用. 4月21日,中生代技术走进盒子科技的现场技术交流活动上,新浪微博技术专家陈波为大家讲解了微博Cache架构的设计实践过程.