Java Concurrency (concrete instance)--build efficient and scalable results cache

Source: Internet
Author: User

This example comes from the fifth chapter of Java concurrency Programming. This article will develop an efficient and scalable cache, starting with the simplest hashmap, then analyzing its concurrency flaws and fixing it step-by-step.

HashMap version

First we define a computable interface that contains a compute () method, which is a long-time numerical calculation method. Memoizer1 is the first version of the cache, which uses HashMap to save the results of the previous calculation, and the compute method first checks whether the desired result is already in the cache, returns the previously computed value if it exists, or recalculates and caches the results in HashMap. And then back again.

Interface Computable<a, v> {V compute (A Arg) throws interruptedexception;//time-consuming calculation}public class Memoizer1<a, v> Implements Computable<a, v> {private final map<a, v> cache = new Hashmap<a, v> ();p rivate final Computabl E<a, v> c;public Memoizer1 (Computable<a, v> c) {this.c = C;} 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;}}

HashMap is not thread-safe, so make sure that two threads do not access the hashmap,memoizer1 in a conservative way, which is to synchronize the entire method. This approach ensures thread safety, but presents a noticeable scalability problem: Only one thread can execute compute at a time.

Concurrenthashmap version

We can use Concurrenthashmap instead of hashmap to improve the bad concurrency behavior in Memoizer1, because Concurrenthashmap is thread-safe, so there is no need to synchronize when accessing the underlying map. Therefore, the serialization of the compute () method is avoided by synchronizing:

public class Memoizer2 <a, V> implements Computable<a, v> {    private final map<a, v> cache = new Conc Urrenthashmap<a, v> ();    Private final Computable<a, v> C;    Public Memoizer2 (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;}    }

But this version of the cache is still problematic, if thread a starts a computationally expensive calculation, and other threads do not know that the thread is in progress, it is likely that the calculation will be repeated.

Futuretask version 1

We can store the future object in the map instead of the final calculation, the future object is equivalent to a placeholder, it tells the user that the result is in the calculation, if you want to get the final result, call the Get () method. The Get () method of the future is a blocking method, and if the result is being calculated, it will block until the result is calculated and returned, and if the result has been calculated, it will be returned directly.

public class Memoizer3<a, v> implements Computable<a, v> {private final map<a, future<v>> cache = New Concurrenthashmap<a, future<v>> ();p rivate final computable<a, v> c;public Memoizer3 (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;cache.put (arg, FT); Ft.run (); Call to C.compute happens Here}try {return f.get ();} catch (Executionexception e) {cache.remove (arg);} return null;}}

Memoizer3 resolved the previous version of the problem, if there are other threads in the calculation results, then the new thread will wait until the result is calculated, but he is another flaw, that is, there are still two threads to calculate the same value of the vulnerability. This is a typical "check and execute" caused by the race condition error, we first check whether there is a result in the map, if it does not exist, then calculate the new value, this is not an atomic operation, so it is still possible for two threads to call compute at the same time to calculate the same value.

Futuretask Version 2

The reason for this problem is that the composite operation "if not added" does not have atomicity, we can use the Atomic method putifabsent in Concurrentmap to avoid the Memoizer3 of the Memoizer3.

public class Memoizer <a, V> implements Computable<a, v> {private final concurrentmap<a, Future<v&gt    ;> cache = new Concurrenthashmap<a, future<v>> ();    Private final Computable<a, v> C;    Public Memoizer (Computable<a, v> c) {this.c = C; Public V Compute (final A arg) throws Interruptedexception {while (true) {future<v> F = Cach            E.get (ARG); if (f = = null) {callable<v> eval = new callable<v> () {public V call () thro                    WS Interruptedexception {return c.compute (ARG);                }                };                futuretask<v> ft = new futuretask<v> (eval);                f = cache.putifabsent (arg, FT);                    if (f = = null) {f = ft;                Ft.run ();            }} try {return f.get (); } catch (CancellaTionexception e) {cache.remove (ARG, f);            } catch (Executionexception e) {throw launderthrowable.launderthrowable (E.getcause ()); }        }    }}

When a cache object is a future instead of a value, it causes a cache pollution problem, because a calculation may be canceled or failed, and when this happens, we should remove the object from the map and then recalculate it again. This caching system is very simple to use, just pass in a computable object to construct the cache, to get the results, call the Compute () method, the method will be calculated results cache.

Summarize

Compared to the first version, the final version has a great performance improvement, the use of Concurrenthashmap avoids the whole method lock; The use of Futuretask makes the computation asynchronous and tells the current thread calculation to be in progress through a future object. Avoid a lot of repetitive calculations.

Java Concurrency (concrete instance)--build efficient and scalable results cache

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.