The source code address is: gitee.com/lidishan/gu…

Involving rely on

< the dependency > < groupId > com. Google. Guava < / groupId > < artifactId > guava < / artifactId > < version > 29.0 jre < / version > </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>failureaccess</artifactId> The < version > < / version 1.0.1 > < / dependency >Copy the code

Data structure diagram

Initialize the Guava Cache object

There are two ways to initialize Guava Cache. The first way is loader. After initialization, only the load() method is permanently called to load data. Examples are as follows:

// The first method is loader
LoadingCache<Object, Object> cache = CacheBuilder.newBuilder()
        .build(new CacheLoader<Object, Object>() {
            @Override
            public Object load(Object name) {
                // Get data from load() if it is not found in cache
                return String.format("Reload (%s) : %s", System.currentTimeMillis(), name); }}); cache.get("a");
Copy the code

Second: Callable mode. After initialization, it can be loaded with fewer restrictions by calling callable arguments in the get and PUT () method arguments. Examples are as follows:

Cache<Object, Object> cache = CacheBuilder.newBuilder().build();
String key = "a";
// The biggest difference with loader is that Callable can directly customize its own return method in different GET and PUT, with fewer restrictions.
cache.get(key, new Callable<String>() {
            @Override
            public String call(a) throws Exception {
                // Get the data from the get() method in callable
                return "aaa:"+ key; }});Copy the code

Call parameter Example

LoadingCache<Object, Object> userCache = CacheBuilder.newBuilder()
        // Based on capacity reclamation. Maximum number of caches. If the value exceeds MAXIMUM_CAPACITY = 1 << 30. The LRU queue recencyQueue is used for capacity flushing
        .maximumSize(1000)
        // Based on capacity reclamation. MaximumWeight and maximumSize cannot be used at the same time. Set the maximum total weight
        .maximumWeight(1000)
        // Set the weight (which can be taken as the size of each cache)
        .weigher((o, o2) -> 5)
        // Weak reference (reference strength order: strong weak weak weak weak)
        // -- weak reference key
        .weakKeys()
        // -- weak reference value
        .weakValues()
        // -- soft reference value
        .softValues()
        // Recycle after expiration
        // -- No read/write access, will be invalid for more than 5 seconds (not automatic, need any getPUT method to scan expired data)
        .expireAfterAccess(5L, TimeUnit.SECONDS)
        // -- No write access, will be invalid after 5 seconds (not automatic, need any putget method to scan expired data)
        .expireAfterWrite(5L, TimeUnit.SECONDS)
        // If there is no write access, the data will be invalid after 5 seconds (non-automatic failure, only any putGET method will scan the expired data. The difference is that an asynchronous thread is opened to refresh, and the old data is returned.)
        .refreshAfterWrite(5L, TimeUnit.SECONDS)
        // Remove the listening event
        .removalListener(removal -> {
            // You can perform some post-deletion actions, such as reporting deleted data for statistics
            System.out.printf(Trigger delete action, delete key=%s%n, removal);
        })
        // Parallelism level. The parameter that determines the number of segments, concurrencyLevel and maxWeight
        .concurrencyLevel(16)
        // Enable cache statistics. Such as hit times, miss times and so on
        .recordStats()
        // The initial total capacity of all segments
        .initialCapacity(512)
        // For testing, you can change the current time arbitrarily. Reference: https://www.geek-share.com/detail/2689756248.html
        .ticker(new Ticker() {
            @Override
            public long read(a) {
                    return 0;
                    }
        })
        .build(new CacheLoader<Object, Object>() {
            @Override
            public Object load(Object name) {
                    // Fetch data if not found in cache
                    return String.format("Reload (%s) : %s", System.currentTimeMillis(), name); }});// Easy to use
userCache.put("a"."aaa");
System.out.println(userCache.get("a"));
Copy the code

Obsolete way

There are three ways based on capacity. They are capacity-based reclamation, scheduled reclamation, and reference-based reclamation.

Periodic reclamation is divided into two types: according to the write time, the earliest write is recycled first; The earliest access is reclaimed according to the access time

Elimination method – based on capacity recovery

There are two ways:

The first is based on the number of caches. Xxx.maximumsize (1000), if the number of caches exceeds 1000, it will be recycled. The second way is based on cache size. Xxx.maximumweight (1000).weigher((o, O2) -> 5).

Since it is difficult to determine the size of memory occupied by a value in the second way, it is generally used to decide reclamation based on the number of caches. Xxx. MaximumSize (1000). The following source code parsing:

  • Demo scenario for triggering volume-based reclamation:
Cache<Object, Object> cache = CacheBuilder.newBuilder()
        // Maximum number of caches, which is equal to 2. If the value exceeds MAXIMUM_CAPACITY = 1 << 30. The LRU queue recencyQueue is used for capacity flushing
        .maximumSize(2).build();
cache.put("a"."aa");
cache.put("b"."bb");
cache.put("c"."cc");// When the third put is executed, the "A" is reclaimed according to the LRU.
Copy the code
  • How does the above code do the recycling?

One of the call links is: LocalCache#put() -> LocalCache#evictEntries() -> LocalCache#removeEntry() Put, get, replace, etc.

V put(K key, int hash, V value, boolean onlyIfAbsent) {... evictEntries(newEntry); . }void evictEntries(ReferenceEntry<K, V> newest) {
    if(! map.evictsBySize()) {// If there is no element, it is not recycled
        return;
    }
    // Put the recencyQueue element in Access (if it exists in Access)
    drainRecencyQueue();

    // If the newest entry by itself is too heavy for the segment, don't bother evicting
    // anything else, just that
    // If the new entry is greater than the maximum weight, it will be removed
    if (newest.getValueReference().getWeight() > maxSegmentWeight) {
        if(! removeEntry(newest, newest.getHash(), RemovalCause.SIZE)) {throw newAssertionError(); }}// key !!!! If the total weight is greater than the maximum weight (since we are using maximumSize(1000), maxSegmentWeight=1000)
    // key !!!! If the total weight is greater than the maximum weight (since we are using maximumSize(1000), maxSegmentWeight=1000)
    while (totalWeight > maxSegmentWeight) {
        // Get the first data in the accessQueue whose weight is greater than zero (weight=1 for each element based on volume)
        ReferenceEntry<K, V> e = getNextEvictable();
        if(! removeEntry(e, e.getHash(), RemovalCause.SIZE)) {throw newAssertionError(); }}}Copy the code

Elimination method – periodic recycling

Timed collection is not automatically timed (that is, no extra thread is used to perform timed collection). All collections need external operations such as GET, PUT, and REPLACE to determine whether they are expired and trigger the collection. <br>

  • ExpireAfterXxxx: Expiration reloads data with a lock and blocks all other thread access.

  • RefreshAfterWrite: Expired data is loaded asynchronously with a lock. The original thread may return the old value, but access by other threads is not blocked.

  • Procedure check whether expireAfterXxxx needs to be recycled, and then check whether refreshAfterWrite needs to be recycled

  • RefreshAfterWrite Asynchronous refresh with good performance. It is suitable for scenarios where data consistency is not required.

Periodic reclamation has the following parameters:

CacheBuilder.newBuilder()
    // -- If there is no read/write access, it will be invalid for more than 5 seconds (not automatic, need any getPUT method to scan expired data)
    .expireAfterAccess(5L, TimeUnit.SECONDS)
    // -- No write access, will be invalid after 5 seconds (not automatic, need any put, replace methods to scan expired data)
    .expireAfterWrite(5L, TimeUnit.SECONDS)
    // If there is no write access, it will be invalid for more than 5 seconds. (It is not automatic. Any put or replace methods are required to scan expired data. The difference is that an asynchronous thread is opened to refresh, and the old data is returned.)
    .refreshAfterWrite(5L, TimeUnit.SECONDS)
    .build();
Copy the code

The reclaimed logic code is as follows:

/** ** get: * - if the value of expireAfterXxxx is not null, return * - if the value is not null If there is no timeout, the value is not null. RefreshAfterWrite parameters are checked to see whether they need to be refreshed asynchronously. If loaded, the old value is returned. * - Step 3: Call lockedGetOrLoad(), which will be called if step 1 times out. It locks and determines whether to block waiting or fetch data directly. * /
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
  checkNotNull(key);
  checkNotNull(loader);
  try {
    if(count ! =0) { // read-if volatile count is not null, there are still elements that can be read
      // don't call getLiveEntry, which would ignore loading values
      // Get the first node of the list with the corresponding index. It then iterates through the linked list to get the corresponding key value
      ReferenceEntry<K, V> e = getEntry(key, hash);
      if(e ! =null) {
        long now = map.ticker.read();// Get the current time, nanoseconds
        ExpireAfterAccess and expireAfterWrite determine whether the value is expired. If expireAfterXxxx is not set or not expired, null is not returned
        // Step 1:!!!!!!! First determine expireAfterXxxx
        // Step 1:!!!!!!! First determine expireAfterXxxx
        V value = getLiveValue(e, now);// Get the data without expiration
        if(value ! =null) {// Finally find a normal value
          recordRead(e, now);// Record the value of the method time accessTime, and the latest use of the queue recencyQueue
          statsCounter.recordHits(1);// Add the number of hits to calculate the hit ratio
          // Step 2:!!!!!!! ExpireAfterXxxx Does not need to be processed. Check whether refreshAfterWrite needs to be processed
          // Step 2:!!!!!!! ExpireAfterXxxx Does not need to be processed. Check whether refreshAfterWrite needs to be processed
          return scheduleRefresh(e, key, hash, value, now, loader);
        }
        // The value == null is either expired or is being refreshed
        ValueReference<K, V> valueReference = e.getValueReference();
        // If value is loading, wait for refresh result
        if (valueReference.isLoading()) {
          returnwaitForLoadingValue(e, key, valueReference); }}}// at this point e is either null or expired;
    // At this point, e is either empty or timed out, requiring a lock to load
    // Step 3: call lockedGetOrLoad(), which will be called if step 1 times out. It locks and determines whether to block waiting or fetch data directly.
    // Step 3: call lockedGetOrLoad(), which will be called if step 1 times out. It locks and determines whether to block waiting or fetch data directly.
    return lockedGetOrLoad(key, hash, loader);
  } catch (ExecutionException ee) {
    Throwable cause = ee.getCause();
    if (cause instanceof Error) {
      throw new ExecutionError((Error) cause);
    } else if (cause instanceof RuntimeException) {
      throw new UncheckedExecutionException(cause);
    }
    throw ee;
  } finally{ postReadCleanup(); }}V scheduleRefresh(
    ReferenceEntry<K, V> entry,
    K key,
    int hash,
    V oldValue,
    long now,
    CacheLoader<? super K, V> loader) {
  // Enable timed refresh (refreshAfterWrite(n), n > 0)
  // && Current time - Last update time > Refresh time
  // && except not LoadingValueReference
  if(map.refreshes() && (now - entry.getWriteTime() > map.refreshNanos) && ! entry.getValueReference().isLoading()) { V newValue = refresh(key, hash, loader,true);// Refresh
    if(newValue ! =null) {
      returnnewValue; }}return oldValue;
}
V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {
  // Insert loadingValueReference to indicate that the value is loading. This tells another thread that it is loading.
  // - There are two cases:
  // -- First type: expireAfterXxxx time expires. If loading is detected, the system blocks and waits for loading to end before obtaining a value
  // -- Second type: expireAfterXxxx set time does not expire. If loading is detected, the asynchronous refresh step is skipped and return oldValue is returned.
  final LoadingValueReference<K, V> loadingValueReference =
      insertLoadingValueReference(key, hash, checkTime);
  if (loadingValueReference == null) {
    return null;
  }
  // Block with future
  ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader);
  if (result.isDone()) {
    try {
      return Uninterruptibles.getUninterruptibly(result);
    } catch (Throwable t) {
      // don't let refresh exceptions propagate; error was already logged}}return null;
}
/** * Clear expired data */
@GuardedBy("this")
void expireEntries(long now) {
  drainRecencyQueue();

  ReferenceEntry<K, V> e;
  // Clear write and Access queue timeout elements
  while((e = writeQueue.peek()) ! =null && map.isExpired(e, now)) {
    if(! removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {throw newAssertionError(); }}while((e = accessQueue.peek()) ! =null && map.isExpired(e, now)) {
    if(! removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {throw newAssertionError(); }}}Copy the code

Obsolescence – reference based collection (weak references are vulnerable to GC)

Reference-based reclamation takes three input parameters:

Weak reference key: The current entry is reclaimed when a key is found during get/ PUT. Xxx. WeakKeys () Weak reference value: When a value is found to be reclaimed during GET /put, the current entry is reclaimed. WeakValues () soft reference value: When a value is recovered when GET/PUT, the current entry is reclaimed. Xxx.softValues()

When setting a weak reference key, the keyReferenceQueue is traversed to see if it needs to be reclaimed. When setting a weak reference value, valueReferenceQueue is traversed to see if it needs to be reclaimed. By default, valueReferenceQueue is traversed at most 16 times.

  • Reference reclamation based scenario demo:
Cache<Object, Object> cache = CacheBuilder.newBuilder()
        .weakKeys().build();
// If some key is detected by GC during the scan, the corresponding entry is eliminated (not necessarily the entry key corresponding to "A" is reclaimed, and other scanned entries are removed. Maximum 16 at a time)
cache.get("a"."aa");
Copy the code
  • How does the above code do the recycling?

One of the call links is: LocalCache#put() -> LocalCache#preWriteCleanup() -> LocalCache#runLockedCleanup(now) -> LocalCache#drainReferenceQueues() puts, gets, and repurchases do not automatically drain.

void runLockedCleanup(long now) {
  if (tryLock()) {
    try {
      // Clears entries that are not strong key/value
      drainReferenceQueues();
      // Clear expired data
      expireEntries(now); // calls drainRecencyQueue
      readCount.set(0);
    } finally{ unlock(); }}}void drainReferenceQueues(a) {
    // Key is not a strong reference type
    if (map.usesKeyReferences()) {
        drainKeyReferenceQueue();
    }
    // value is not a strong reference type
    if(map.usesValueReferences()) { drainValueReferenceQueue(); }}void drainKeyReferenceQueue(a) {
  Reference<? extends K> ref;
  int i = 0;
  // keyReferenceQueue is only used when weakKeys() is set
  while((ref = keyReferenceQueue.poll()) ! =null) {
    ReferenceEntry<K, V> entry = (ReferenceEntry<K, V>) ref;
    map.reclaimKey(entry);RemoveValueFromChain is called to remove each step
    if (++i == DRAIN_MAX) {// The maximum number of cleanups at a time
      break; }}}void drainValueReferenceQueue(a) {
  Reference<? extends V> ref;
  int i = 0;
  // keyReferenceQueue is used only when weakValues() and softValues() are set
  while((ref = valueReferenceQueue.poll()) ! =null) {
    ValueReference<K, V> valueReference = (ValueReference<K, V>) ref;
    map.reclaimValue(valueReference);RemoveValueFromChain is called to remove each step
    if (++i == DRAIN_MAX) {// The maximum number of cleanups at a time
      break; }}}Copy the code

Supplement knowledge

Creating Entry Mode

Creating an entry is done by enumerating factories as follows:

Step 1: Initialize the entryFactory with the constructor LocalCache() (here is an enumeration factory, see code analysis for details)

entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries());

Step 2: Call the newEntry() method to create an entry from entryFactory

The code analysis is as follows

/** The factory used to create entry * can produce a variety of methods, which are cartesian product of reference mode x access mode * - Reference mode: strong, week, soft * - Access mode: Access, write, access_write * * Factory used to create new entries. * * */
final EntryFactory entryFactory;
/** * Create a new empty entry with a specific policy, Setting concurrency levels * Creates a new, empty map with the Specified strategy, initial capacity and concurrency level
LocalCache(
    CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader){
    ....
    // Use the reference type, whether to access the queue, whether to write the queue to do the ternary expression + bit operation to get the subscript, get the corresponding factory enumeration instanceentryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries()); . }ReferenceEntry<K, V> newEntry(K key, int hash, @Nullable ReferenceEntry<K, V> next) {
    return map.entryFactory.newEntry(this, checkNotNull(key), hash, next);
}
Look-up table for factories. */
static final EntryFactory[] factories = {
    STRONG,
    STRONG_ACCESS,
    STRONG_WRITE,
    STRONG_ACCESS_WRITE,
    WEAK,
    WEAK_ACCESS,
    WEAK_WRITE,
    WEAK_ACCESS_WRITE,
};
/** * get factory (depending on the type of key) */
static EntryFactory getFactory(
    Strength keyStrength, boolean usesAccessQueue, boolean usesWriteQueue) {
    // The bit operation of this ternary expression is somewhat clever, and it can eventually locate the specific factory enumeration
    // If the WEAK reference is set to 4, WEAK=0100;
    / / if allowed to access, | ACCESS_MASK, equal to + 1
    / / if allowed to write, | WRITE_MASK, equivalent to + 2
    / / note: use this way: the premise of the first element behind with no conflicts, such as WEAK = 0100, his low 2 to 0, can directly use "|" implementation accumulative effect
    int flags = ((keyStrength == Strength.WEAK) ? WEAK_MASK : 0)// WEAK_MASK=0100
        | (usesAccessQueue ? ACCESS_MASK : 0)/ / ACCESS_MASK: 0001
        | (usesWriteQueue ? WRITE_MASK : 0);/ / WRITE_MASK: 0010
    return factories[flags];
}
Copy the code

Remove listening events

Remove listening events. It means that the entry will be triggered when it is removed, and then we can write the corresponding business processing logic here. Printf (” Trigger removal, remove key=%s%n”, removal))

Cache<Object, Object> cache = CacheBuilder.newBuilder()
    // Remove the listening event
    .removalListener(removal -> {
        // You can perform some post-deletion actions, such as reporting deleted data for statistics
        System.out.printf(Trigger delete action, delete key=%s%n, removal);
    }).build();
Copy the code

The realization process is as follows:

Step 1: The Guava cache maintains a removalNotificationQueue, which pushes data into the removalNotificationQueue when the original entry is dropped.

The specific removal enumeration that triggers enqueueing is RemovalCause, including: timeout, capacity, recycle, replace, and manual kill

The second step: after the execution of the program can be carried in the finally call routine cleaning method or other cache can be set to removalNotificationQueue. The poll () out of the team.

/** * Enumerations of specific events to remove: timeout, capacity, recycle, replace, manual kill */
public enum RemovalCause {
  EXPLICIT {// The business invokes the cache cleanup related method
    @Override
    boolean wasEvicted(a) {
      return false;
    }
  },
  REPLACED {/ / replace
    @Override
    boolean wasEvicted(a) {
      return false;
    }
  },
  COLLECTED {// Key or value is collected by GC
    @Override
    boolean wasEvicted(a) {
      return true;
    }
  },
  EXPIRED {/ / timeout
    @Override
    boolean wasEvicted(a) {
      return true;
    }
  },
  SIZE {// The capacity is insufficient
    @Override
    boolean wasEvicted(a) {
      return true; }}abstract boolean wasEvicted(a);
}
/ * * * here is remove the call to the team way = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * here is remove the call to the team way = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * The need to deal with the news of the pressure into the map. RemovalNotificationQueue. The corresponding callback method is then used to process */
@GuardedBy("this")
void enqueueNotification(
    @Nullable K key, int hash, @Nullable V value, int weight, RemovalCause cause) {
  totalWeight -= weight;// subtract the weight
  if (cause.wasEvicted()) {// If automatic removal is performed, return true, cause=COLLECTED, EXPIRED, SIZE
    statsCounter.recordEviction();Evictioncount.increment ()
  }
  // If the queue is not deprecated, it is pushed into the remove call callback notification queue
  // Set expireAfterXxx, refreshAfterXxx, removalListener to DISCARDING_QUEUE
  if(map.removalNotificationQueue ! = DISCARDING_QUEUE) { RemovalNotification<K, V> notification = RemovalNotification.create(key, value, cause);// Remove the callback queue, because the queue is obstructed, so after production, the consumer will consume processing. Consumer: processPendingNotificationsmap.removalNotificationQueue.offer(notification); }}/ * * * here is remove the call to the team way = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * here is remove the call to the team way = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = * EnqueueNotification process is called when the node does not exist, the node has expired, or the cache is clear. The producer is enqueueNotification */
void processPendingNotifications(a) {
    RemovalNotification<K, V> notification;
    while((notification = removalNotificationQueue.poll()) ! =null) {
      try {
        removalListener.onRemoval(notification);// Call remove trigger method, do remove after processing logic
      } catch (Throwable e) {
        logger.log(Level.WARNING, "Exception thrown by removal listener", e); }}}Copy the code