`
85977328
  • 浏览: 1904191 次
  • 性别: 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
分享到:
评论

相关推荐

    构建高效可伸缩的缓存demo

    在构建高效可伸缩的缓存系统中,我们需要考虑的关键因素包括性能、容错性、扩展性和资源管理。本demo提供了实现这些目标的实例代码和测试案例,让我们深入探讨其中涉及的技术点。 首先,缓存的主要目的是提高数据...

    Java并发编程实战

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

    java并发编程实战(英文版)

    书中通过实例解释了诸如`ExecutorService`、`Future`、`Callable`、`BlockingQueue`等高级并发工具类的使用方法,并详细阐述了如何利用这些工具来构建可伸缩、高性能的应用系统。 #### 二、并发基础知识 本书不仅...

    《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并发编程实践-构建执行程序块-学习笔记

    为计算结果建⽴⾼效、可伸缩的缓存是指在多线程环境下,使用缓存来实现高效、可靠的计算结果。例如,在桌⾯搜索时,使用缓存来实现高效、可靠的计算结果,以避免出现死锁、竞态条件等问题。 JAVA并发编程实践-构建...

    Java并发编程与高并发解决方案-学习笔记-www.itmuch.com.pdf

    在探讨Java并发编程与高并发解决方案的过程...从CPU多级缓存、缓存一致性、Java内存模型,到具体的高并发系统设计策略,都是构建高性能、可伸缩应用的基石。掌握这些知识对于开发大型的、高性能的互联网应用尤为重要。

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

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

    JAVA并发编程实践 高清 中文版 PDF

    Java SE 5和Java SE 6在并发方面做出了重要的改进,提供了改进的虚拟机性能、高可伸缩性的并发类和一系列新的并发构建块。《JAVA并发编程实践》的作者们,他们大多来自Java标准化组织JSR166专家组,为Java并发工具的...

    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 ...

    《大型分布式网站架构设计与实践》 Java 并发编程实战

    《大型分布式网站架构设计与实践》是一本深入探讨如何构建高效、可扩展的分布式系统的技术专著,结合了Java并发编程的实际应用。本书旨在帮助读者理解在高流量、大规模应用场景下,如何通过精心设计的架构和Java并发...

    多线程,高并发.zip

    另外,Java并发API(java.util.concurrent包)提供了一系列高级并发工具,如`ConcurrentHashMap`(线程安全的哈希映射)、`BlockingQueue`(阻塞队列)和`Future`(异步计算结果)。这些工具可以帮助开发者更好地...

    Java并发程序设计教程

    Java并发程序设计教程主要围绕Java语言如何实现高效并发编程展开。在WEB开发领域,多线程编程是提高服务器处理能力的核心技术之一。并发编程的知识点包括但不限于以下几个方面: 1. **线程的使用经验**:包括如何...

    利用Java开发高性能、高并发Web应用

    在Java开发领域,构建高性能、高并发的Web应用是一项核心任务。这涉及到多个技术层面的综合运用,包括但不限于系统架构设计、线程管理、数据访问优化、缓存策略、负载均衡以及性能监控等。以下是一些关键的知识点,...

    高并发场景杂谈.zip

    采用缓存策略,预先计算和存储常用结果;使用数据库的分区和并行查询功能,提升查询性能;或者利用搜索引擎如Elasticsearch进行全文搜索和数据分析。 总结来说,这些文档涵盖了从数据库优化、分布式系统设计到网络...

    Java高并发高性能分布式框架从无到有微服务架构设计.doc

    总结来说,Java高并发高性能分布式框架的构建涉及到微服务架构设计、服务拆分、独立部署、通信机制的选择,以及各种缓存策略和池化技术的运用。通过这些手段,开发者可以构建出既能处理大规模并发请求,又能保持高效...

    分布式Java应用 完整版 PDF

    此外,还会讨论Java并发库的使用,如ExecutorService和并发集合,以及如何设计低延迟的系统架构。 第五部分:构建高可用、可伸缩的系统 最后,本书将引导读者设计和实施高可用和可伸缩的Java系统。内容包括负载均衡...

Global site tag (gtag.js) - Google Analytics