Preface
The use of caching has become a commonplace in development , Commonly used tools that deal with caching, such as Redis、MemCache etc. , But sometimes it may require some simple caching , There’s no need to use this special caching tool , Then write a cache class is the most appropriate .
One 、 analysis
First, analyze how to design cache class , Here I implement a cache class in a very simple way , This is the design I’ve been using all along .
In order to clarify the function , An interface first defines a class CacheInt, Then there is the tool class for caching implementation CacheUtil. And then look at the functions , For easy access , The cache should be accessed in the form of key value pairs , In order to adapt to more scenes , So you can add a cache expiration time when accessing , Then add other common additions 、 obtain 、 Delete 、 Cache size 、 Whether there is key、 Clean up the expired cache and so on , This is almost the way the cache tool works .
Cache class need to pay attention to the problem :
- Cache objects should be unique , That is, the singleton ;
- The operation method of cache should be synchronized , To prevent errors in multithreading concurrency ;
- The cache container should have high concurrency performance ,ConcurrentHashMap It’s a good choice .
Two 、 Concrete realization
1. CacheInt Definition of interface
CacheInt The interface is defined as follows :
public interface CacheInt {
/**
* Deposited in the cache , This process always clears the expired cache
* @param key key
* @param value value
* @param expire Expiration time , Unit second , If it is less than 1 It means long-term preservation
*/
void put(String key, Object value, int expire);
/**
* Access to the cache ,1/3 The probability of clearing the expired cache
* @param key key
* @return Object object
*/
Object get(String key);
/**
* Get cache size
* @return int
*/
int size();
/**
* Is there a cache
* @param key key
* @return boolean
*/
boolean isContains(String key);
/**
* Get all cached keys
* @return Set aggregate
*/
Set<String> getAllKeys();
/**
* Delete a cache
* @param key key
*/
void remove(String key);
}
2. CacheUtil The concrete realization of
The core of cache implementation is CacheUtil, The following is illustrated with the notes , In order to avoid the redundancy of the article , The following screenshot is the complete source screenshot , And keep it in order .
The first is the definition of a class and its properties , The instance object of this class uses volatile Embellish to improve visibility , Initialization cache capacity is used to initialize ConcurrentHashMap The size of the cache container , This size is optimized according to the actual application scenario .
import java.util.Iterator;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author beifengtz
* <a href='http://www.beifengtz.com'>www.beifengtz.com</a>
* Created in 13:24 2019/7/13
*/
public class CacheUtil implements CacheInt{
// The instance object of this class , Single case
private static volatile CacheUtil instance;
// Initialize cache capacity
private static final int CACHE_INITIAL_SIZE = 8;
// Cache container definition
private static ConcurrentHashMap<String, Entry> cacheMap;
// Then there's the inner class Entry The definition of , This class is used to store actual data , In order to facilitate the processing of expiration time , Add initialization timestamp 、 Expiration time and other attributes .
// The definition of the structure of stored content
static class Entry {
long initTime;// Storage time
int expire; // Company : second
Object data;// Specific data
Entry(long initTime, int expire, Object data) {
this.initTime = initTime;
this.expire = expire;
this.data = data;
}
}
// Then we use the double lock singleton to obtain the instance object of this class , Because singleton can only have unique characteristics , So notice that the constructor needs to be set to private
private CacheUtil() {
cacheMap = new ConcurrentHashMap<>(CACHE_INITIAL_SIZE);
}
public static CacheUtil getInstance() {
if (instance == null) {
synchronized (CacheUtil.class) {
if (instance == null) {
instance = new CacheUtil();
}
return instance;
}
}
return instance;
}
// The next step is to store cache data `put()` Method ,
// there `clearExpiredCache()` Is to clean up the expired cache ,
// You'll see the method body later , Because there are fewer caches in my project ,
// So here I fixed the expiration time cache to be cleaned before each save ,
// Here you can optimize according to the actual situation of your project .
public synchronized void put(String key, Object value, int expire) {
clearExpiredCache();
Entry entry = new Entry(System.currentTimeMillis(), expire, value);
cacheMap.put(key, entry);
}
// And then it's getting the cache `get()` Method , Because it takes a lot of time to get data ,
// So here I set a one-third probability to clean up the expired cache , Release heap memory appropriately ,
// At the same time, it detects whether it is expired , If it's expired and you get it , Delete and return empty .
public synchronized Object get(String key) {
// Construct a one-third chance to clear the expired cache
if(new Random().nextInt(12) > 8){
clearExpiredCache();
}
if (cacheMap.containsKey(key)) {
Entry entry = cacheMap.get(key);
if (entry.expire > 0 && System.currentTimeMillis() > entry.expire * 1000 + entry.initTime) {
cacheMap.remove(key);
return null;
} else {
return entry.data;
}
} else {
return null;
}
}
// Then there are some more conventional methods , See the code for details
public int size() {
return cacheMap.size();
}
@Override
public boolean isContains(String key) {
return cacheMap.containsKey(key);
}
@Override
public Set<String> getAllKeys() {
return cacheMap.keySet();
}
@Override
public void remove(String key) {
cacheMap.remove(key);
}
// The last way is to clean up the expired cache , Here you can choose to start a monitor
// Threads clean up the cache in real time , You can also choose to do a clean-up at the right time ,
// For example, I exist here put and get Fixed or probabilistic cleaning of the cache during operation .
private synchronized void clearExpiredCache() {
Iterator<Map.Entry<String, Entry>> iterator = cacheMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, Entry> entry = iterator.next();
if (entry.getValue().expire > 0 &&
System.currentTimeMillis() > entry.getValue().expire * 1000 + entry.getValue().initTime) {
iterator.remove();
}
}
}
}
3、 … and 、 Concurrent test
Common implementation tests are not shown here , There must be no problem , Readers can simply write some test samples , This is mainly about concurrent testing , Because in the actual situation, there are concurrent processing cache conditions , To make sure it’s right , So concurrent testing is a must , Here’s a sample of my test .
@Test
public void concurrentCacheTest() {
final int LOOP_TIMES = 1000;// cycles
// Thread pool , start-up 10 Child thread to process
ExecutorService es = Executors.newFixedThreadPool(10);
// Store randomly generated key
ConcurrentLinkedQueue<String> clq = new ConcurrentLinkedQueue<>();
// Define two counters , Used to measure two concurrent processes
CountDownLatch count1 = new CountDownLatch(LOOP_TIMES);
CountDownLatch count2 = new CountDownLatch(LOOP_TIMES);
// The count of cache operations
AtomicInteger atomicInteger = new AtomicInteger(0);
// Test concurrency put performance
for (int i = 0; i < LOOP_TIMES; i++) {
es.execute(new Runnable() {
@Override
public void run() {
String key = String.valueOf(new Random().nextInt(1000));
Object value = new Random().nextInt(1000);
int expire = new Random().nextInt(100);
clq.add(key);
cacheUtil.put(key, value, expire);
System.out.println(atomicInteger.incrementAndGet() +
". Cache successful ,key=" + key +
",value=" + value +
",expire=" + expire);
count1.countDown();
}
});
}
try {
count1.await();// Wait for all put After execution
} catch (InterruptedException e) {
e.printStackTrace();
}
// Test concurrency get performance
atomicInteger.set(0);
for (int i = 0; i < LOOP_TIMES; i++) {
es.execute(new Runnable() {
@Override
public void run() {
try {
// Random waiting time
Thread.sleep(new Random().nextInt(100));
} catch (InterruptedException e) {
e.printStackTrace();
}
String key = clq.poll();
System.out.println(atomicInteger.incrementAndGet() +
". From the cache key=" + key +
" Value :" + cacheUtil.get(key));
count2.countDown();
}
});
}
try {
count2.await();// Wait for all get After execution
} catch (InterruptedException e) {
e.printStackTrace();
}
es.shutdown();
while (true){
if (es.isTerminated()){
System.out.println(" All tasks have been carried out ");
System.out.println(" Cache size :" + cacheUtil.size());
return;
}
}
}
The final test was very good , There was no mistake , Some of the test results are shown below :
Four 、 expand
This class simply implements the caching process , But it doesn’t always work well in practice , First of all, its capacity must be limited , Can’t save too much cache , Because it uses JVM Memory in the heap , If optimized, direct memory can be used for storage , Secondly, its function is relatively simple , For example, it doesn’t support LRU Elimination, etc , This can be done with a double linked list +Map Or is it LinkedHashMap To achieve , More functions can be expanded .
My official account of WeChat north wind IT The way Better browsing experience , Here are more excellent articles for you , Pay attention to it !