View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.myfaces.shared.util;
20  
21  import java.lang.ref.WeakReference;
22  import java.util.Arrays;
23  import java.util.Collections;
24  import java.util.LinkedHashMap;
25  import java.util.Map;
26  import java.util.TreeSet;
27  import java.util.concurrent.ConcurrentHashMap;
28  import java.util.concurrent.atomic.AtomicInteger;
29  import java.util.concurrent.atomic.AtomicLong;
30  import java.util.concurrent.locks.ReentrantLock;
31  
32  /**
33   * A LRU cache implementation based upon ConcurrentHashMap and other techniques to reduce
34   * contention and synchronization overhead to utilize multiple CPU cores more effectively.
35   * <p/>
36   * Note that the implementation does not follow a true LRU (least-recently-used) eviction
37   * strategy. Instead it strives to remove least recently used items but when the initial
38   * cleanup does not remove enough items to reach the 'acceptableWaterMark' limit, it can
39   * remove more items forcefully regardless of access order.
40   *
41   *
42   * @since solr 1.4
43   * @see org.apache.solr.util.ConcurrentLRUCache
44   */
45  public class ConcurrentLRUCache<K, V>
46  {
47      //private static Logger log = Logger.getLogger(ConcurrentLRUCache.class
48      //        .getName());
49  
50      private final ConcurrentHashMap<Object, CacheEntry<K, V>> map;
51      private final int upperWaterMark;
52      private final int lowerWaterMark;
53      private final ReentrantLock markAndSweepLock = new ReentrantLock(true);
54      private boolean isCleaning = false; // not volatile... piggybacked on other volatile vars
55      private final boolean newThreadForCleanup;
56      private volatile boolean islive = true;
57      private final Stats stats = new Stats();
58      private final int acceptableWaterMark;
59      private long oldestEntry = 0; // not volatile, only accessed in the cleaning method
60      private final EvictionListener<K, V> evictionListener;
61      private CleanupThread cleanupThread;
62  
63      public ConcurrentLRUCache(int upperWaterMark, final int lowerWaterMark,
64              int acceptableWatermark, int initialSize, boolean runCleanupThread,
65              boolean runNewThreadForCleanup,
66              EvictionListener<K, V> evictionListener)
67      {
68          if (upperWaterMark < 1)
69          {
70              throw new IllegalArgumentException("upperWaterMark must be > 0");
71          }
72          if (lowerWaterMark >= upperWaterMark)
73          {
74              throw new IllegalArgumentException(
75                      "lowerWaterMark must be  < upperWaterMark");
76          }
77          map = new ConcurrentHashMap<Object, CacheEntry<K, V>>(initialSize);
78          newThreadForCleanup = runNewThreadForCleanup;
79          this.upperWaterMark = upperWaterMark;
80          this.lowerWaterMark = lowerWaterMark;
81          this.acceptableWaterMark = acceptableWatermark;
82          this.evictionListener = evictionListener;
83          if (runCleanupThread)
84          {
85              cleanupThread = new CleanupThread(this);
86              cleanupThread.start();
87          }
88      }
89  
90      public ConcurrentLRUCache(int size, int lowerWatermark)
91      {
92          this(size, lowerWatermark, (int) Math
93                  .floor((lowerWatermark + size) / 2), (int) Math
94                  .ceil(0.75 * size), false, false, null);
95      }
96  
97      public void setAlive(boolean live)
98      {
99          islive = live;
100     }
101 
102     public V get(K key)
103     {
104         CacheEntry<K, V> e = map.get(key);
105         if (e == null)
106         {
107             if (islive)
108             {
109                 stats.missCounter.incrementAndGet();
110             }
111             return null;
112         }
113         if (islive)
114         {
115             e.lastAccessed = stats.accessCounter.incrementAndGet();
116         }
117         return e.value;
118     }
119 
120     public V remove(K key)
121     {
122         CacheEntry<K, V> cacheEntry = map.remove(key);
123         if (cacheEntry != null)
124         {
125             stats.size.decrementAndGet();
126             return cacheEntry.value;
127         }
128         return null;
129     }
130 
131     public V put(K key, V val)
132     {
133         if (val == null)
134         {
135             return null;
136         }
137         CacheEntry<K, V> e = new CacheEntry<K, V>(key, val,
138                 stats.accessCounter.incrementAndGet());
139         CacheEntry<K, V> oldCacheEntry = map.put(key, e);
140         int currentSize;
141         if (oldCacheEntry == null)
142         {
143             currentSize = stats.size.incrementAndGet();
144         }
145         else
146         {
147             currentSize = stats.size.get();
148         }
149         if (islive)
150         {
151             stats.putCounter.incrementAndGet();
152         }
153         else
154         {
155             stats.nonLivePutCounter.incrementAndGet();
156         }
157 
158         // Check if we need to clear out old entries from the cache.
159         // isCleaning variable is checked instead of markAndSweepLock.isLocked()
160         // for performance because every put invokation will check until
161         // the size is back to an acceptable level.
162         //
163         // There is a race between the check and the call to markAndSweep, but
164         // it's unimportant because markAndSweep actually aquires the lock or returns if it can't.
165         //
166         // Thread safety note: isCleaning read is piggybacked (comes after) other volatile reads
167         // in this method.
168         if (currentSize > upperWaterMark && !isCleaning)
169         {
170             if (newThreadForCleanup)
171             {
172                 new Thread()
173                 {
174                     @Override
175                     public void run()
176                     {
177                         markAndSweep();
178                     }
179                 }.start();
180             }
181             else if (cleanupThread != null)
182             {
183                 cleanupThread.wakeThread();
184             }
185             else
186             {
187                 markAndSweep();
188             }
189         }
190         return oldCacheEntry == null ? null : oldCacheEntry.value;
191     }
192 
193     /**
194      * Removes items from the cache to bring the size down
195      * to an acceptable value ('acceptableWaterMark').
196      * <p/>
197      * It is done in two stages. In the first stage, least recently used items are evicted.
198      * If, after the first stage, the cache size is still greater than 'acceptableSize'
199      * config parameter, the second stage takes over.
200      * <p/>
201      * The second stage is more intensive and tries to bring down the cache size
202      * to the 'lowerWaterMark' config parameter.
203      */
204     private void markAndSweep()
205     {
206         // if we want to keep at least 1000 entries, then timestamps of
207         // current through current-1000 are guaranteed not to be the oldest (but that does
208         // not mean there are 1000 entries in that group... it's acutally anywhere between
209         // 1 and 1000).
210         // Also, if we want to remove 500 entries, then
211         // oldestEntry through oldestEntry+500 are guaranteed to be
212         // removed (however many there are there).
213 
214         if (!markAndSweepLock.tryLock())
215         {
216             return;
217         }
218         try
219         {
220             long oldestEntry = this.oldestEntry;
221             isCleaning = true;
222             this.oldestEntry = oldestEntry; // volatile write to make isCleaning visible
223 
224             long timeCurrent = stats.accessCounter.get();
225             int sz = stats.size.get();
226 
227             int numRemoved = 0;
228             int numKept = 0;
229             long newestEntry = timeCurrent;
230             long newNewestEntry = -1;
231             long newOldestEntry = Long.MAX_VALUE;
232 
233             int wantToKeep = lowerWaterMark;
234             int wantToRemove = sz - lowerWaterMark;
235 
236             @SuppressWarnings("unchecked")
237             // generic array's are anoying
238             CacheEntry<K, V>[] eset = new CacheEntry[sz];
239             int eSize = 0;
240 
241             // System.out.println("newestEntry="+newestEntry + " oldestEntry="+oldestEntry);
242             // System.out.println("items removed:" + numRemoved + " numKept=" + numKept + 
243             //    " esetSz="+ eSize + " sz-numRemoved=" + (sz-numRemoved));
244 
245             for (CacheEntry<K, V> ce : map.values())
246             {
247                 // set lastAccessedCopy to avoid more volatile reads
248                 ce.lastAccessedCopy = ce.lastAccessed;
249                 long thisEntry = ce.lastAccessedCopy;
250 
251                 // since the wantToKeep group is likely to be bigger than wantToRemove, check it first
252                 if (thisEntry > newestEntry - wantToKeep)
253                 {
254                     // this entry is guaranteed not to be in the bottom
255                     // group, so do nothing.
256                     numKept++;
257                     newOldestEntry = Math.min(thisEntry, newOldestEntry);
258                 }
259                 else if (thisEntry < oldestEntry + wantToRemove)
260                 { // entry in bottom group?
261                     // this entry is guaranteed to be in the bottom group
262                     // so immediately remove it from the map.
263                     evictEntry(ce.key);
264                     numRemoved++;
265                 }
266                 else
267                 {
268                     // This entry *could* be in the bottom group.
269                     // Collect these entries to avoid another full pass... this is wasted
270                     // effort if enough entries are normally removed in this first pass.
271                     // An alternate impl could make a full second pass.
272                     if (eSize < eset.length - 1)
273                     {
274                         eset[eSize++] = ce;
275                         newNewestEntry = Math.max(thisEntry, newNewestEntry);
276                         newOldestEntry = Math.min(thisEntry, newOldestEntry);
277                     }
278                 }
279             }
280 
281             // System.out.println("items removed:" + numRemoved + " numKept=" + numKept + 
282             //    " esetSz="+ eSize + " sz-numRemoved=" + (sz-numRemoved));
283             // TODO: allow this to be customized in the constructor?
284             int numPasses = 1; // maximum number of linear passes over the data
285 
286             // if we didn't remove enough entries, then make more passes
287             // over the values we collected, with updated min and max values.
288             while (sz - numRemoved > acceptableWaterMark && --numPasses >= 0)
289             {
290 
291                 oldestEntry = newOldestEntry == Long.MAX_VALUE ? oldestEntry
292                         : newOldestEntry;
293                 newOldestEntry = Long.MAX_VALUE;
294                 newestEntry = newNewestEntry;
295                 newNewestEntry = -1;
296                 wantToKeep = lowerWaterMark - numKept;
297                 wantToRemove = sz - lowerWaterMark - numRemoved;
298 
299                 // iterate backward to make it easy to remove items.
300                 for (int i = eSize - 1; i >= 0; i--)
301                 {
302                     CacheEntry<K, V> ce = eset[i];
303                     long thisEntry = ce.lastAccessedCopy;
304 
305                     if (thisEntry > newestEntry - wantToKeep)
306                     {
307                         // this entry is guaranteed not to be in the bottom
308                         // group, so do nothing but remove it from the eset.
309                         numKept++;
310                         // remove the entry by moving the last element to it's position
311                         eset[i] = eset[eSize - 1];
312                         eSize--;
313 
314                         newOldestEntry = Math.min(thisEntry, newOldestEntry);
315 
316                     }
317                     else if (thisEntry < oldestEntry + wantToRemove)
318                     { // entry in bottom group?
319 
320                         // this entry is guaranteed to be in the bottom group
321                         // so immediately remove it from the map.
322                         evictEntry(ce.key);
323                         numRemoved++;
324 
325                         // remove the entry by moving the last element to it's position
326                         eset[i] = eset[eSize - 1];
327                         eSize--;
328                     }
329                     else
330                     {
331                         // This entry *could* be in the bottom group, so keep it in the eset,
332                         // and update the stats.
333                         newNewestEntry = Math.max(thisEntry, newNewestEntry);
334                         newOldestEntry = Math.min(thisEntry, newOldestEntry);
335                     }
336                 }
337                 // System.out.println("items removed:" + numRemoved + " numKept=" + 
338                 //    numKept + " esetSz="+ eSize + " sz-numRemoved=" + (sz-numRemoved));
339             }
340 
341             // if we still didn't remove enough entries, then make another pass while
342             // inserting into a priority queue
343             if (sz - numRemoved > acceptableWaterMark)
344             {
345 
346                 oldestEntry = newOldestEntry == Long.MAX_VALUE ? oldestEntry
347                         : newOldestEntry;
348                 newOldestEntry = Long.MAX_VALUE;
349                 newestEntry = newNewestEntry;
350                 newNewestEntry = -1;
351                 wantToKeep = lowerWaterMark - numKept;
352                 wantToRemove = sz - lowerWaterMark - numRemoved;
353 
354                 PQueue<K, V> queue = new PQueue<K, V>(wantToRemove);
355 
356                 for (int i = eSize - 1; i >= 0; i--)
357                 {
358                     CacheEntry<K, V> ce = eset[i];
359                     long thisEntry = ce.lastAccessedCopy;
360 
361                     if (thisEntry > newestEntry - wantToKeep)
362                     {
363                         // this entry is guaranteed not to be in the bottom
364                         // group, so do nothing but remove it from the eset.
365                         numKept++;
366                         // removal not necessary on last pass.
367                         // eset[i] = eset[eSize-1];
368                         // eSize--;
369 
370                         newOldestEntry = Math.min(thisEntry, newOldestEntry);
371 
372                     }
373                     else if (thisEntry < oldestEntry + wantToRemove)
374                     { // entry in bottom group?
375                         // this entry is guaranteed to be in the bottom group
376                         // so immediately remove it.
377                         evictEntry(ce.key);
378                         numRemoved++;
379 
380                         // removal not necessary on last pass.
381                         // eset[i] = eset[eSize-1];
382                         // eSize--;
383                     }
384                     else
385                     {
386                         // This entry *could* be in the bottom group.
387                         // add it to the priority queue
388 
389                         // everything in the priority queue will be removed, so keep track of
390                         // the lowest value that ever comes back out of the queue.
391 
392                         // first reduce the size of the priority queue to account for
393                         // the number of items we have already removed while executing
394                         // this loop so far.
395                         queue.myMaxSize = sz - lowerWaterMark - numRemoved;
396                         while (queue.size() > queue.myMaxSize
397                                 && queue.size() > 0)
398                         {
399                             CacheEntry otherEntry = (CacheEntry) queue.pop();
400                             newOldestEntry = Math
401                                     .min(otherEntry.lastAccessedCopy,
402                                             newOldestEntry);
403                         }
404                         if (queue.myMaxSize <= 0)
405                         {
406                             break;
407                         }
408 
409                         Object o = queue.myInsertWithOverflow(ce);
410                         if (o != null)
411                         {
412                             newOldestEntry = Math.min(
413                                     ((CacheEntry) o).lastAccessedCopy,
414                                     newOldestEntry);
415                         }
416                     }
417                 }
418 
419                 // Now delete everything in the priority queue.
420                 // avoid using pop() since order doesn't matter anymore
421                 for (CacheEntry<K, V> ce : queue.getValues())
422                 {
423                     if (ce == null)
424                     {
425                         continue;
426                     }
427                     evictEntry(ce.key);
428                     numRemoved++;
429                 }
430 
431                 // System.out.println("items removed:" + numRemoved + " numKept=" + numKept + 
432                 //    " initialQueueSize="+ wantToRemove + " finalQueueSize=" + 
433                 //      queue.size() + " sz-numRemoved=" + (sz-numRemoved));
434             }
435 
436             oldestEntry = newOldestEntry == Long.MAX_VALUE ? oldestEntry
437                     : newOldestEntry;
438             this.oldestEntry = oldestEntry;
439         }
440         finally
441         {
442             isCleaning = false; // set before markAndSweep.unlock() for visibility
443             markAndSweepLock.unlock();
444         }
445     }
446 
447     private static class PQueue<K, V> extends PriorityQueue<CacheEntry<K, V>>
448     {
449         int myMaxSize;
450         final Object[] heap;
451 
452         PQueue(int maxSz)
453         {
454             super(maxSz);
455             heap = getHeapArray();
456             myMaxSize = maxSz;
457         }
458 
459         @SuppressWarnings("unchecked")
460         Iterable<CacheEntry<K, V>> getValues()
461         {
462             return (Iterable) Collections.unmodifiableCollection(Arrays
463                     .asList(heap));
464         }
465 
466         @Override
467         protected boolean lessThan(CacheEntry a, CacheEntry b)
468         {
469             // reverse the parameter order so that the queue keeps the oldest items
470             return b.lastAccessedCopy < a.lastAccessedCopy;
471         }
472 
473         // necessary because maxSize is private in base class
474         @SuppressWarnings("unchecked")
475         public CacheEntry<K, V> myInsertWithOverflow(CacheEntry<K, V> element)
476         {
477             if (size() < myMaxSize)
478             {
479                 add(element);
480                 return null;
481             }
482             else if (size() > 0
483                     && !lessThan(element, (CacheEntry<K, V>) heap[1]))
484             {
485                 CacheEntry<K, V> ret = (CacheEntry<K, V>) heap[1];
486                 heap[1] = element;
487                 updateTop();
488                 return ret;
489             }
490             else
491             {
492                 return element;
493             }
494         }
495     }
496 
497     private void evictEntry(K key)
498     {
499         CacheEntry<K, V> o = map.remove(key);
500         if (o == null)
501         {
502             return;
503         }
504         stats.size.decrementAndGet();
505         stats.evictionCounter.incrementAndGet();
506         if (evictionListener != null)
507         {
508             evictionListener.evictedEntry(o.key, o.value);
509         }
510     }
511 
512     /**
513      * Returns 'n' number of oldest accessed entries present in this cache.
514      *
515      * This uses a TreeSet to collect the 'n' oldest items ordered by ascending last access time
516      *  and returns a LinkedHashMap containing 'n' or less than 'n' entries.
517      * @param n the number of oldest items needed
518      * @return a LinkedHashMap containing 'n' or less than 'n' entries
519      */
520     public Map<K, V> getOldestAccessedItems(int n)
521     {
522         Map<K, V> result = new LinkedHashMap<K, V>();
523         if (n <= 0)
524         {
525             return result;
526         }
527         TreeSet<CacheEntry<K, V>> tree = new TreeSet<CacheEntry<K, V>>();
528         markAndSweepLock.lock();
529         try
530         {
531             for (Map.Entry<Object, CacheEntry<K, V>> entry : map.entrySet())
532             {
533                 CacheEntry<K, V> ce = entry.getValue();
534                 ce.lastAccessedCopy = ce.lastAccessed;
535                 if (tree.size() < n)
536                 {
537                     tree.add(ce);
538                 }
539                 else
540                 {
541                     if (ce.lastAccessedCopy < tree.first().lastAccessedCopy)
542                     {
543                         tree.remove(tree.first());
544                         tree.add(ce);
545                     }
546                 }
547             }
548         }
549         finally
550         {
551             markAndSweepLock.unlock();
552         }
553         for (CacheEntry<K, V> e : tree)
554         {
555             result.put(e.key, e.value);
556         }
557         return result;
558     }
559 
560     public Map<K, V> getLatestAccessedItems(int n)
561     {
562         Map<K, V> result = new LinkedHashMap<K, V>();
563         if (n <= 0)
564         {
565             return result;
566         }
567         TreeSet<CacheEntry<K, V>> tree = new TreeSet<CacheEntry<K, V>>();
568         // we need to grab the lock since we are changing lastAccessedCopy
569         markAndSweepLock.lock();
570         try
571         {
572             for (Map.Entry<Object, CacheEntry<K, V>> entry : map.entrySet())
573             {
574                 CacheEntry<K, V> ce = entry.getValue();
575                 ce.lastAccessedCopy = ce.lastAccessed;
576                 if (tree.size() < n)
577                 {
578                     tree.add(ce);
579                 }
580                 else
581                 {
582                     if (ce.lastAccessedCopy > tree.last().lastAccessedCopy)
583                     {
584                         tree.remove(tree.last());
585                         tree.add(ce);
586                     }
587                 }
588             }
589         }
590         finally
591         {
592             markAndSweepLock.unlock();
593         }
594         for (CacheEntry<K, V> e : tree)
595         {
596             result.put(e.key, e.value);
597         }
598         return result;
599     }
600 
601     public int size()
602     {
603         return stats.size.get();
604     }
605 
606     public void clear()
607     {
608         map.clear();
609     }
610 
611     public Map<Object, CacheEntry<K, V>> getMap()
612     {
613         return map;
614     }
615 
616     private static class CacheEntry<K, V> implements
617             Comparable<CacheEntry<K, V>>
618     {
619         K key;
620         V value;
621         volatile long lastAccessed = 0;
622         long lastAccessedCopy = 0;
623 
624         public CacheEntry(K key, V value, long lastAccessed)
625         {
626             this.key = key;
627             this.value = value;
628             this.lastAccessed = lastAccessed;
629         }
630 
631         public void setLastAccessed(long lastAccessed)
632         {
633             this.lastAccessed = lastAccessed;
634         }
635 
636         public int compareTo(CacheEntry<K, V> that)
637         {
638             if (this.lastAccessedCopy == that.lastAccessedCopy)
639             {
640                 return 0;
641             }
642             return this.lastAccessedCopy < that.lastAccessedCopy ? 1 : -1;
643         }
644 
645         @Override
646         public int hashCode()
647         {
648             return value.hashCode();
649         }
650 
651         @Override
652         public boolean equals(Object obj)
653         {
654             return value.equals(obj);
655         }
656 
657         @Override
658         public String toString()
659         {
660             return "key: " + key + " value: " + value + " lastAccessed:"
661                     + lastAccessed;
662         }
663     }
664 
665     private boolean isDestroyed = false;
666 
667     public void destroy()
668     {
669         try
670         {
671             if (cleanupThread != null)
672             {
673                 cleanupThread.stopThread();
674             }
675         }
676         finally
677         {
678             isDestroyed = true;
679         }
680     }
681 
682     public Stats getStats()
683     {
684         return stats;
685     }
686 
687     public static class Stats
688     {
689         private final AtomicLong accessCounter = new AtomicLong(0);
690         private final AtomicLong putCounter = new AtomicLong(0);
691         private final AtomicLong nonLivePutCounter = new AtomicLong(0);
692         private final AtomicLong missCounter = new AtomicLong();
693         private final AtomicInteger size = new AtomicInteger();
694         private AtomicLong evictionCounter = new AtomicLong();
695 
696         public long getCumulativeLookups()
697         {
698             return (accessCounter.get() - putCounter.get() - nonLivePutCounter
699                     .get()) + missCounter.get();
700         }
701 
702         public long getCumulativeHits()
703         {
704             return accessCounter.get() - putCounter.get()
705                     - nonLivePutCounter.get();
706         }
707 
708         public long getCumulativePuts()
709         {
710             return putCounter.get();
711         }
712 
713         public long getCumulativeEvictions()
714         {
715             return evictionCounter.get();
716         }
717 
718         public int getCurrentSize()
719         {
720             return size.get();
721         }
722 
723         public long getCumulativeNonLivePuts()
724         {
725             return nonLivePutCounter.get();
726         }
727 
728         public long getCumulativeMisses()
729         {
730             return missCounter.get();
731         }
732 
733         public void add(Stats other)
734         {
735             accessCounter.addAndGet(other.accessCounter.get());
736             putCounter.addAndGet(other.putCounter.get());
737             nonLivePutCounter.addAndGet(other.nonLivePutCounter.get());
738             missCounter.addAndGet(other.missCounter.get());
739             evictionCounter.addAndGet(other.evictionCounter.get());
740             size.set(Math.max(size.get(), other.size.get()));
741         }
742     }
743 
744     public static interface EvictionListener<K, V>
745     {
746         public void evictedEntry(K key, V value);
747     }
748 
749     private static class CleanupThread extends Thread
750     {
751         private WeakReference<ConcurrentLRUCache> cache;
752 
753         private boolean stop = false;
754 
755         public CleanupThread(ConcurrentLRUCache c)
756         {
757             cache = new WeakReference<ConcurrentLRUCache>(c);
758         }
759 
760         @Override
761         public void run()
762         {
763             while (true)
764             {
765                 synchronized (this)
766                 {
767                     if (stop)
768                     {
769                         break;
770                     }
771                     try
772                     {
773                         this.wait();
774                     }
775                     catch (InterruptedException e)
776                     {
777                     }
778                 }
779                 if (stop)
780                 {
781                     break;
782                 }
783                 ConcurrentLRUCache c = cache.get();
784                 if (c == null)
785                 {
786                     break;
787                 }
788                 c.markAndSweep();
789             }
790         }
791 
792         void wakeThread()
793         {
794             synchronized (this)
795             {
796                 this.notify();
797             }
798         }
799 
800         void stopThread()
801         {
802             synchronized (this)
803             {
804                 stop = true;
805                 this.notify();
806             }
807         }
808     }
809 
810     @Override
811     protected void finalize() throws Throwable
812     {
813         try
814         {
815             if (!isDestroyed)
816             {
817                 // This log message is useless, because it is not supposed to use
818                 // thread cleanup strategy for this class.
819                 //log.severe("ConcurrentLRUCache was not destroyed prior to finalize()," +
820                 //        " indicates a bug -- POSSIBLE RESOURCE LEAK!!!");
821                 destroy();
822             }
823         }
824         finally
825         {
826             super.finalize();
827         }
828     }
829 }