• ConcurrentHashMap+FutureTask实现高效缓存耗时较长的业务结果获取


    假设服务器有一个computer复杂运算,耗时较久,如果服务器要实现缓存,一种思路就是通过HashMap或者redis来做缓存

    下面是使用HashMap的简单示例,源码取自《并发编程实战》

     1 package net.jcip.examples;
     2 
     3 import java.math.BigInteger;
     4 import java.util.*;
     5 
     6 import net.jcip.annotations.*;
     7 
     8 /**
     9  * Memoizer1
    10  *
    11  * Initial cache attempt using HashMap and synchronization
    12  *
    13  * @author Brian Goetz and Tim Peierls
    14  */
    15 public class Memoizer1 <A, V> implements Computable<A, V> {
    16     @GuardedBy("this") private final Map<A, V> cache = new HashMap<A, V>();
    17     private final Computable<A, V> c;
    18 
    19     public Memoizer1(Computable<A, V> c) {
    20         this.c = c;
    21     }
    22 
    23     public synchronized V compute(A arg) throws InterruptedException {
    24         V result = cache.get(arg);
    25         if (result == null) {
    26             result = c.compute(arg);
    27             cache.put(arg, result);
    28         }
    29         return result;
    30     }
    31 }
    32 
    33 
    34 interface Computable <A, V> {
    35     V compute(A arg) throws InterruptedException;
    36 }
    37 
    38 class ExpensiveFunction
    39         implements Computable<String, BigInteger> {
    40     public BigInteger compute(String arg) {
    41         // after deep thought...
    42         return new BigInteger(arg);
    43     }
    44 }

    我们看compute方法的逻辑,先从hashMap中取,取不到再计算后存入hashMap。这是最容易想到的逻辑,但由于hashMap不是线程安全的,所以做一下优化,用ConcurrentHashMap

     1 package net.jcip.examples;
     2 
     3 import java.util.*;
     4 import java.util.concurrent.*;
     5 
     6 /**
     7  * Memoizer2
     8  * <p/>
     9  * Replacing HashMap with ConcurrentHashMap
    10  *
    11  * @author Brian Goetz and Tim Peierls
    12  */
    13 public class Memoizer2 <A, V> implements Computable<A, V> {
    14     private final Map<A, V> cache = new ConcurrentHashMap<A, V>();
    15     private final Computable<A, V> c;
    16 
    17     public Memoizer2(Computable<A, V> c) {
    18         this.c = c;
    19     }
    20 
    21     public V compute(A arg) throws InterruptedException {
    22         V result = cache.get(arg);
    23         if (result == null) {
    24             result = c.compute(arg);
    25             cache.put(arg, result);
    26         }
    27         return result;
    28     }
    29 }

    使用ConcurrentHashMap仍然存在一个问题,前面说过,compute的计算耗时较久,可能存在线程A已经快计算完了,但还没来得及放到ConcurrentHashMap中,线程B获取不到就自己开始计算了

    我们理想的情况是,线程B发现ConcurrentHashMap中没有的时候,先去瞅一眼有没有其他线程已经开始计算了,有的话就等,没有的话再自己算,算完放到缓存中。

    这时候可以想到用FutureTask, 将Future作为ConcurrentHashMap的Value,如果有结果可用,Future.get将立即返回结果,否则线程就会阻塞直到计算完成,下面用ConcurrentHashMap+FutureTask进行优化。

     1 package net.jcip.examples;
     2 
     3 import java.util.*;
     4 import java.util.concurrent.*;
     5 
     6 /**
     7  * Memoizer3
     8  * <p/>
     9  * Memoizing wrapper using FutureTask
    10  *
    11  * @author Brian Goetz and Tim Peierls
    12  */
    13 public class Memoizer3 <A, V> implements Computable<A, V> {
    14     private final Map<A, Future<V>> cache
    15             = new ConcurrentHashMap<A, Future<V>>();
    16     private final Computable<A, V> c;
    17 
    18     public Memoizer3(Computable<A, V> c) {
    19         this.c = c;
    20     }
    21 
    22     public V compute(final A arg) throws InterruptedException {
    23         Future<V> f = cache.get(arg);
    24         if (f == null) {
    25             Callable<V> eval = new Callable<V>() {
    26                 public V call() throws InterruptedException {
    27                     return c.compute(arg);
    28                 }
    29             };
    30             FutureTask<V> ft = new FutureTask<V>(eval);
    31             f = ft;
    32             cache.put(arg, ft);
    33             ft.run(); // call to c.compute happens here
    34         }
    35         try {
    36             return f.get();
    37         } catch (ExecutionException e) {
    38             throw LaunderThrowable.launderThrowable(e.getCause());
    39         }
    40     }
    41 }

    上面的代码看似完美了,但还是存在一个明显的问题,就是if条件的“”先检查再执行”,两个线程仍有可能在同一时间进行了if判断,然后分别计算,分别放到缓存。所以并不能解决上面的问题

    继续优化。使用ConcurrentHashMap.putIfAbsent来避免这个漏洞

     1 package net.jcip.examples;
     2 
     3 import java.util.concurrent.*;
     4 
     5 /**
     6  * Memoizer
     7  * <p/>
     8  * Final implementation of Memoizer
     9  *
    10  * @author Brian Goetz and Tim Peierls
    11  */
    12 public class Memoizer <A, V> implements Computable<A, V> {
    13     private final ConcurrentMap<A, Future<V>> cache
    14             = new ConcurrentHashMap<A, Future<V>>();
    15     private final Computable<A, V> c;
    16 
    17     public Memoizer(Computable<A, V> c) {
    18         this.c = c;
    19     }
    20 
    21     public V compute(final A arg) throws InterruptedException {
    22         while (true) {
    23             Future<V> f = cache.get(arg);
    24             if (f == null) {
    25                 Callable<V> eval = new Callable<V>() {
    26                     public V call() throws InterruptedException {
    27                         return c.compute(arg);
    28                     }
    29                 };
    30                 FutureTask<V> ft = new FutureTask<V>(eval);
    31                 f = cache.putIfAbsent(arg, ft);
    32                 if (f == null) {
    33                     f = ft;
    34                     ft.run();
    35                 }
    36             }
    37             try {
    38                 return f.get();
    39             } catch (CancellationException e) {
    40                 cache.remove(arg, f);
    41             } catch (ExecutionException e) {
    42                 throw LaunderThrowable.launderThrowable(e.getCause());
    43             }
    44         }
    45     }
    46 }

    完美实现。

    但这里需要注意如果f.get()抛出异常时,需要将Future从缓存中移除,即

     cache.remove(arg, f);

    不然可能存在缓存污染问题。

  • 相关阅读:
    Finding Lines
    2020-3-3 牛客试题复盘
    2020-3-2 牛客试题复盘
    2020-02-29(观看视频笔记)
    2020-02-29(观看视频笔记)
    2020-02-29(观看视频笔记)
    2020-02-28(观看视频笔记)
    2020-02-27(观看视频笔记)
    2020-2-27 牛客试题复盘
    2020-02-26(观看视频笔记)
  • 原文地址:https://www.cnblogs.com/sulishihupan/p/14352734.html
Copyright © 2020-2023  润新知