Java Concurrency Builds Efficient and Scalable Result Cache

background

Build an efficient and scalable result cache, analyze the advantages and disadvantages of multiple solutions and finally come up with a best practice

Pre-content description

public interface Computable <A,V>{
    
    

    V compute(A arg) throws InterruptedException;
}
public class ExpensiveFunction implements Computable<String, BigInteger> {
    
    

    @Override
    public BigInteger compute(String arg) throws InterruptedException {
    
    
        //经过较长时间计算后的操作

        return new BigInteger(arg);
    }
}

Purpose: A Computable wrapper will be created to help remember previous calculation results and encapsulate the caching process.

1. Use HashMap and synchronization mechanism to initialize the cache

public class Memoizer1<A,V> implements Computable<A,V> {
    
    

    private final Map<A,V> cache = new HashMap<A,V>();
    private Computable<A,V> mComputable;

    public Memoizer1(Computable<A,V> computable){
    
    
        mComputable=computable;
    }

    @Override
    public synchronized V compute(A arg) throws InterruptedException {
    
    
        V result = cache.get(arg);
        if(result==null){
    
    
            result=mComputable.compute(arg);
            cache.put(arg,result);
        }
        return result;
    }
}

Disadvantages:
hashMap is not thread-safe, so the entire method is synchronized; this method can ensure thread safety, but it will bring an obvious scalability problem, only one thread can execute compute if there are multiple threads at a time In the queue waiting for the result that has not yet been calculated, the calculation time of the compute method may be longer than the calculation time without caching operations

2. Use ConcurrentHashMap to handle concurrency

public class Memoizer2<A,V> implements Computable<A,V> {
    
    

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

    public Memoizer2(Computable<A,V> computable){
    
    
        mComputable=computable;
    }

    @Override
    public  V compute(A arg) throws InterruptedException {
    
    
        V result = cache.get(arg);
        if(result==null){
    
    
            result=mComputable.compute(arg);
            cache.put(arg,result);
        }
        return result;
    }
}

Disadvantages:
Compared with Memoizer1, Memoizer2 removes the method synchronization for compute, and uses ConcurrentHashMap instead of HashMap; the disadvantage is that there is a loophole when two threads use compute at the same time, which may lead to the calculation of the same value. In this case Especially for single-initialized object caches, the risk is greater. Secondly, when a thread starts a calculation that is expensive, and other threads do not know that the calculation is in progress, the calculation is likely to be repeated.

3. When FutureTask is expensive to process a single thread, other threads do not know that the calculation is in progress

public class Memoizer3<A, V> implements Computable<A, V> {
    
    

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

    public Memoizer3(Computable<A, V> computable) {
    
    
        mComputable = computable;
    }

    @Override
    public V compute(final A arg) throws InterruptedException {
    
    
        Future<V> result = cache.get(arg);
        if (result == null) {
    
    
            Callable<V> callable = new Callable<V>() {
    
    
                @Override
                public V call() throws Exception {
    
    

                    return mComputable.compute(arg);
                }
            };
            FutureTask<V> task = new FutureTask<>(callable);
            result = task;
            cache.put(arg, result);
            task.run();
        }
        try {
    
    
            return result.get();
        } catch (ExecutionException e) {
    
    
            e.printStackTrace();
            throw new InterruptedException(e.getMessage());
        }
    }
}

Disadvantages:
Currently, it shows very good concurrency. If the result has been calculated, it will return immediately; if other threads are calculating the result, the newly arrived thread will wait for the result to be calculated; there is still a defect, namely There is still a loophole where two threads calculate the same value; it is because the if code block in the compute method is still non-atomic first check and then execute the operation. Objects cannot be locked to ensure atomicity

4. The atomic method putIfAbsent in Future+ConcurrentMap

the best solution

public class Memoizer4<A, V> implements Computable<A, V> {
    
    

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

    public Memoizer4(Computable<A, V> computable) {
    
    
        mComputable = computable;
    }

    @Override
    public V compute(final A arg) throws InterruptedException {
    
    
        while (true) {
    
    
            Future<V> result = cache.get(arg);
            if (result == null) {
    
    
                Callable<V> callable = new Callable<V>() {
    
    
                    @Override
                    public V call() throws Exception {
    
    
                        return mComputable.compute(arg);
                    }
                };
                FutureTask<V> task = new FutureTask<>(callable);
                result = cache.putIfAbsent(arg, task);
                if (result == null) {
    
    
                    result = task;
                    task.run();
                }
            }
            try {
    
    
                return result.get();
            } catch (CancellationException e) {
    
    
                //当缓存的是Future而不是值时,将导致缓存污染问题,计算失败或者取消,需要将Future移除
                cache.remove(arg, result);
            } catch (ExecutionException exception) {
    
    
                exception.printStackTrace();
            }
        }
    }
}

Remarks: Refer to [Java Concurrent Programming Practice]

Guess you like

Origin blog.csdn.net/ixiaoma/article/details/128446806