1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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
34
35
36
37
38
39
40
41
42
43
44
45 public class ConcurrentLRUCache<K, V>
46 {
47
48
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;
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;
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
159
160
161
162
163
164
165
166
167
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
195
196
197
198
199
200
201
202
203
204 private void markAndSweep()
205 {
206
207
208
209
210
211
212
213
214 if (!markAndSweepLock.tryLock())
215 {
216 return;
217 }
218 try
219 {
220 long oldestEntry = this.oldestEntry;
221 isCleaning = true;
222 this.oldestEntry = oldestEntry;
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
238 CacheEntry<K, V>[] eset = new CacheEntry[sz];
239 int eSize = 0;
240
241
242
243
244
245 for (CacheEntry<K, V> ce : map.values())
246 {
247
248 ce.lastAccessedCopy = ce.lastAccessed;
249 long thisEntry = ce.lastAccessedCopy;
250
251
252 if (thisEntry > newestEntry - wantToKeep)
253 {
254
255
256 numKept++;
257 newOldestEntry = Math.min(thisEntry, newOldestEntry);
258 }
259 else if (thisEntry < oldestEntry + wantToRemove)
260 {
261
262
263 evictEntry(ce.key);
264 numRemoved++;
265 }
266 else
267 {
268
269
270
271
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
282
283
284 int numPasses = 1;
285
286
287
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
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
308
309 numKept++;
310
311 eset[i] = eset[eSize - 1];
312 eSize--;
313
314 newOldestEntry = Math.min(thisEntry, newOldestEntry);
315
316 }
317 else if (thisEntry < oldestEntry + wantToRemove)
318 {
319
320
321
322 evictEntry(ce.key);
323 numRemoved++;
324
325
326 eset[i] = eset[eSize - 1];
327 eSize--;
328 }
329 else
330 {
331
332
333 newNewestEntry = Math.max(thisEntry, newNewestEntry);
334 newOldestEntry = Math.min(thisEntry, newOldestEntry);
335 }
336 }
337
338
339 }
340
341
342
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
364
365 numKept++;
366
367
368
369
370 newOldestEntry = Math.min(thisEntry, newOldestEntry);
371
372 }
373 else if (thisEntry < oldestEntry + wantToRemove)
374 {
375
376
377 evictEntry(ce.key);
378 numRemoved++;
379
380
381
382
383 }
384 else
385 {
386
387
388
389
390
391
392
393
394
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
420
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
432
433
434 }
435
436 oldestEntry = newOldestEntry == Long.MAX_VALUE ? oldestEntry
437 : newOldestEntry;
438 this.oldestEntry = oldestEntry;
439 }
440 finally
441 {
442 isCleaning = false;
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
470 return b.lastAccessedCopy < a.lastAccessedCopy;
471 }
472
473
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
514
515
516
517
518
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
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
818
819
820
821 destroy();
822 }
823 }
824 finally
825 {
826 super.finalize();
827 }
828 }
829 }