From 17814a1468002d8e9a844a4696fa73808189c927 Mon Sep 17 00:00:00 2001 From: Arash Ariani Date: Sat, 17 Jul 2021 14:11:26 +0430 Subject: [PATCH] BAEL-4464 : how to implement LRU-Cache in java codes added (#11036) * BAEL-4464 : how to implement LRU-Cache in java codes added * BAEL-4464 : how to implement LRU-Cache in java codes added - package named fixed * BAEL-4464 : how to implement LRU-Cache in java codes added - package named changed * BAEL-4464 : how to implement LRU-Cache in java codes added - unitTest fixed --- .../java/com/baeldung/lrucache/Cache.java | 15 ++ .../com/baeldung/lrucache/CacheElement.java | 31 ++++ .../baeldung/lrucache/DoublyLinkedList.java | 168 ++++++++++++++++++ .../java/com/baeldung/lrucache/DummyNode.java | 62 +++++++ .../java/com/baeldung/lrucache/LRUCache.java | 105 +++++++++++ .../com/baeldung/lrucache/LinkedListNode.java | 23 +++ .../main/java/com/baeldung/lrucache/Node.java | 71 ++++++++ .../baeldung/lrucache/LRUCacheUnitTest.java | 59 ++++++ 8 files changed, 534 insertions(+) create mode 100644 data-structures/src/main/java/com/baeldung/lrucache/Cache.java create mode 100644 data-structures/src/main/java/com/baeldung/lrucache/CacheElement.java create mode 100644 data-structures/src/main/java/com/baeldung/lrucache/DoublyLinkedList.java create mode 100644 data-structures/src/main/java/com/baeldung/lrucache/DummyNode.java create mode 100644 data-structures/src/main/java/com/baeldung/lrucache/LRUCache.java create mode 100644 data-structures/src/main/java/com/baeldung/lrucache/LinkedListNode.java create mode 100644 data-structures/src/main/java/com/baeldung/lrucache/Node.java create mode 100644 data-structures/src/test/java/com/baeldung/lrucache/LRUCacheUnitTest.java diff --git a/data-structures/src/main/java/com/baeldung/lrucache/Cache.java b/data-structures/src/main/java/com/baeldung/lrucache/Cache.java new file mode 100644 index 0000000000..f070f66191 --- /dev/null +++ b/data-structures/src/main/java/com/baeldung/lrucache/Cache.java @@ -0,0 +1,15 @@ +package com.baeldung.lrucache; + +import java.util.Optional; + +public interface Cache { + boolean put(K key, V value); + + Optional get(K key); + + int size(); + + boolean isEmpty(); + + void clear(); +} diff --git a/data-structures/src/main/java/com/baeldung/lrucache/CacheElement.java b/data-structures/src/main/java/com/baeldung/lrucache/CacheElement.java new file mode 100644 index 0000000000..7d260c4531 --- /dev/null +++ b/data-structures/src/main/java/com/baeldung/lrucache/CacheElement.java @@ -0,0 +1,31 @@ +package com.baeldung.lrucache; + +/** + * Created by arash on 09.07.21. + */ + +public class CacheElement { + private K key; + private V value; + + public CacheElement(K key, V value) { + this.value = value; + this.key = key; + } + + public K getKey() { + return key; + } + + public void setKey(K key) { + this.key = key; + } + + public V getValue() { + return value; + } + + public void setValue(V value) { + this.value = value; + } +} diff --git a/data-structures/src/main/java/com/baeldung/lrucache/DoublyLinkedList.java b/data-structures/src/main/java/com/baeldung/lrucache/DoublyLinkedList.java new file mode 100644 index 0000000000..a2b56741a2 --- /dev/null +++ b/data-structures/src/main/java/com/baeldung/lrucache/DoublyLinkedList.java @@ -0,0 +1,168 @@ +package com.baeldung.lrucache; + +import java.util.Collection; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class DoublyLinkedList { + + private DummyNode dummyNode; + private LinkedListNode head; + private LinkedListNode tail; + private AtomicInteger size; + private ReentrantReadWriteLock.ReadLock readLock; + private ReentrantReadWriteLock.WriteLock writeLock; + + + public DoublyLinkedList() { + this.dummyNode = new DummyNode(this); + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + readLock = lock.readLock(); + writeLock = lock.writeLock(); + clear(); + } + + public void clear() { + writeLock.lock(); + try { + head = dummyNode; + tail = dummyNode; + size = new AtomicInteger(0); + } finally { + writeLock.unlock(); + } + } + + public int size() { + readLock.lock(); + try { + return size.get(); + } finally { + readLock.unlock(); + } + } + + public boolean isEmpty() { + readLock.lock(); + try { + return head.isEmpty(); + } finally { + readLock.unlock(); + } + } + + public boolean contains(T value) { + readLock.lock(); + try { + return search(value).hasElement(); + } finally { + readLock.unlock(); + } + } + + public LinkedListNode search(T value) { + readLock.lock(); + try { + return head.search(value); + } finally { + readLock.unlock(); + } + } + + public LinkedListNode add(T value) { + writeLock.lock(); + try { + head = new Node(value, head, this); + if (tail.isEmpty()) { + tail = head; + } + size.incrementAndGet(); + return head; + } finally { + writeLock.unlock(); + } + } + + public boolean addAll(Collection values) { + writeLock.lock(); + try { + for (T value : values) { + if (add(value).isEmpty()) { + return false; + } + } + return true; + } finally { + writeLock.unlock(); + } + } + + public LinkedListNode remove(T value) { + writeLock.lock(); + try { + LinkedListNode linkedListNode = head.search(value); + if (!linkedListNode.isEmpty()) { + if (linkedListNode == tail) { + tail = tail.getPrev(); + } + if (linkedListNode == head) { + head = head.getNext(); + } + linkedListNode.detach(); + size.decrementAndGet(); + } + return linkedListNode; + } finally { + writeLock.unlock(); + } + } + + public LinkedListNode removeTail() { + writeLock.lock(); + try { + LinkedListNode oldTail = tail; + if (oldTail == head) { + tail = head = dummyNode; + } else { + tail = tail.getPrev(); + oldTail.detach(); + } + if (!oldTail.isEmpty()) { + size.decrementAndGet(); + } + return oldTail; + } finally { + writeLock.unlock(); + } + } + + public LinkedListNode moveToFront(LinkedListNode node) { + return node.isEmpty() ? dummyNode : updateAndMoveToFront(node, node.getElement()); + } + + public LinkedListNode updateAndMoveToFront(LinkedListNode node, T newValue) { + writeLock.lock(); + try { + if (node.isEmpty() || (this != (node.getListReference()))) { + return dummyNode; + } + detach(node); + add(newValue); + return head; + } finally { + writeLock.unlock(); + } + } + + private void detach(LinkedListNode node) { + if (node != tail) { + node.detach(); + if (node == head) { + head = head.getNext(); + } + size.decrementAndGet(); + } else { + removeTail(); + } + } +} diff --git a/data-structures/src/main/java/com/baeldung/lrucache/DummyNode.java b/data-structures/src/main/java/com/baeldung/lrucache/DummyNode.java new file mode 100644 index 0000000000..4965b70bcf --- /dev/null +++ b/data-structures/src/main/java/com/baeldung/lrucache/DummyNode.java @@ -0,0 +1,62 @@ +package com.baeldung.lrucache; + +/** + * Created by arash on 09.07.21. + */ +public class DummyNode implements LinkedListNode { + private DoublyLinkedList list; + + public DummyNode(DoublyLinkedList list) { + this.list = list; + } + + @Override + public boolean hasElement() { + return false; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public T getElement() throws NullPointerException { + throw new NullPointerException(); + } + + @Override + public void detach() { + return; + } + + @Override + public DoublyLinkedList getListReference() { + return list; + } + + @Override + public LinkedListNode setPrev(LinkedListNode next) { + return next; + } + + @Override + public LinkedListNode setNext(LinkedListNode prev) { + return prev; + } + + @Override + public LinkedListNode getPrev() { + return this; + } + + @Override + public LinkedListNode getNext() { + return this; + } + + @Override + public LinkedListNode search(T value) { + return this; + } +} diff --git a/data-structures/src/main/java/com/baeldung/lrucache/LRUCache.java b/data-structures/src/main/java/com/baeldung/lrucache/LRUCache.java new file mode 100644 index 0000000000..0128cadf78 --- /dev/null +++ b/data-structures/src/main/java/com/baeldung/lrucache/LRUCache.java @@ -0,0 +1,105 @@ +package com.baeldung.lrucache; + +import java.util.Hashtable; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class LRUCache implements Cache { + private int size; + private Map>> linkedListNodeMap; + private DoublyLinkedList> doublyLinkedList; + private ReentrantReadWriteLock.ReadLock readLock; + private ReentrantReadWriteLock.WriteLock writeLock; + + public LRUCache(int size) { + this.size = size; + this.linkedListNodeMap = new Hashtable<>(size); + this.doublyLinkedList = new DoublyLinkedList<>(); + ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + this.readLock = lock.readLock(); + this.writeLock = lock.writeLock(); + } + + @Override + public boolean put(K key, V value) { + writeLock.lock(); + try { + CacheElement item = new CacheElement(key, value); + LinkedListNode> newNode; + if (this.linkedListNodeMap.containsKey(key)) { + LinkedListNode> node = this.linkedListNodeMap.get(key); + newNode = doublyLinkedList.updateAndMoveToFront(node, item); + } else { + if (this.size() >= this.size) { + this.evictElement(); + } + newNode = this.doublyLinkedList.add(item); + } + if (newNode.isEmpty()) { + return false; + } + this.linkedListNodeMap.put(key, newNode); + return true; + } finally { + writeLock.unlock(); + } + } + + @Override + public Optional get(K key) { + readLock.lock(); + try { + LinkedListNode> linkedListNode = this.linkedListNodeMap.get(key); + if (linkedListNode != null && !linkedListNode.isEmpty()) { + linkedListNodeMap.put(key, this.doublyLinkedList.moveToFront(linkedListNode)); + return Optional.of(linkedListNode.getElement().getValue()); + } + return Optional.empty(); + } finally { + readLock.unlock(); + } + } + + @Override + public int size() { + readLock.lock(); + try { + return doublyLinkedList.size(); + } finally { + readLock.unlock(); + } + } + + @Override + public boolean isEmpty() { + return size() == 0; + } + + @Override + public void clear() { + writeLock.lock(); + try { + linkedListNodeMap.clear(); + doublyLinkedList.clear(); + } finally { + writeLock.unlock(); + } + } + + + private boolean evictElement() { + writeLock.lock(); + try { + LinkedListNode> linkedListNode = doublyLinkedList.removeTail(); + if (linkedListNode.isEmpty()) { + return false; + } + linkedListNodeMap.remove(linkedListNode.getElement().getKey()); + return true; + } finally { + writeLock.unlock(); + } + } +} diff --git a/data-structures/src/main/java/com/baeldung/lrucache/LinkedListNode.java b/data-structures/src/main/java/com/baeldung/lrucache/LinkedListNode.java new file mode 100644 index 0000000000..219ee13496 --- /dev/null +++ b/data-structures/src/main/java/com/baeldung/lrucache/LinkedListNode.java @@ -0,0 +1,23 @@ +package com.baeldung.lrucache; + +public interface LinkedListNode { + boolean hasElement(); + + boolean isEmpty(); + + V getElement() throws NullPointerException; + + void detach(); + + DoublyLinkedList getListReference(); + + LinkedListNode setPrev(LinkedListNode prev); + + LinkedListNode setNext(LinkedListNode next); + + LinkedListNode getPrev(); + + LinkedListNode getNext(); + + LinkedListNode search(V value); +} diff --git a/data-structures/src/main/java/com/baeldung/lrucache/Node.java b/data-structures/src/main/java/com/baeldung/lrucache/Node.java new file mode 100644 index 0000000000..340bb1dd82 --- /dev/null +++ b/data-structures/src/main/java/com/baeldung/lrucache/Node.java @@ -0,0 +1,71 @@ +package com.baeldung.lrucache; + +/** + * Created by arash on 09.07.21. + */ +public class Node implements LinkedListNode { + private T value; + private DoublyLinkedList list; + private LinkedListNode next; + private LinkedListNode prev; + + public Node(T value, LinkedListNode next, DoublyLinkedList list) { + this.value = value; + this.next = next; + this.setPrev(next.getPrev()); + this.prev.setNext(this); + this.next.setPrev(this); + this.list = list; + } + + @Override + public boolean hasElement() { + return true; + } + + @Override + public boolean isEmpty() { + return false; + } + + public T getElement() { + return value; + } + + public void detach() { + this.prev.setNext(this.getNext()); + this.next.setPrev(this.getPrev()); + } + + @Override + public DoublyLinkedList getListReference() { + return this.list; + } + + @Override + public LinkedListNode setPrev(LinkedListNode prev) { + this.prev = prev; + return this; + } + + @Override + public LinkedListNode setNext(LinkedListNode next) { + this.next = next; + return this; + } + + @Override + public LinkedListNode getPrev() { + return this.prev; + } + + @Override + public LinkedListNode getNext() { + return this.next; + } + + @Override + public LinkedListNode search(T value) { + return this.getElement() == value ? this : this.getNext().search(value); + } +} diff --git a/data-structures/src/test/java/com/baeldung/lrucache/LRUCacheUnitTest.java b/data-structures/src/test/java/com/baeldung/lrucache/LRUCacheUnitTest.java new file mode 100644 index 0000000000..f6e236342c --- /dev/null +++ b/data-structures/src/test/java/com/baeldung/lrucache/LRUCacheUnitTest.java @@ -0,0 +1,59 @@ +package com.baeldung.lrucache; + +import com.baeldung.lrucache.Cache; +import com.baeldung.lrucache.LRUCache; +import org.hamcrest.core.AllOf; +import org.junit.Before; +import org.junit.Test; +import java.util.*; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.IntStream; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.*; + +public class LRUCacheUnitTest { + + @Test + public void addSomeDataToCache_WhenGetData_ThenIsEqualWithCacheElement() { + LRUCache lruCache = new LRUCache<>(3); + lruCache.put("1", "test1"); + lruCache.put("2", "test2"); + lruCache.put("3", "test3"); + assertEquals(lruCache.get("1").get(), "test1"); + assertEquals(lruCache.get("2").get(), "test2"); + assertEquals(lruCache.get("3").get(), "test3"); + } + + @Test + public void addDataToCacheToTheNumberOfSize_WhenAddOneMoreData_ThenLeastRecentlyDataWillEvict() { + LRUCache lruCache = new LRUCache<>(3); + lruCache.put("1", "test1"); + lruCache.put("2", "test2"); + lruCache.put("3", "test3"); + lruCache.put("4", "test4"); + assertEquals(lruCache.get("1").isPresent(), false); + } + + @Test + public void runMultiThreadTask_WhenPutDataInConcurrentToCache_ThenNoDataLost() throws Exception { + final int size = 50; + final ExecutorService executorService = Executors.newFixedThreadPool(5); + Cache cache = new LRUCache<>(size); + CountDownLatch countDownLatch = new CountDownLatch(size); + try { + IntStream.range(0, size).mapToObj(key -> () -> { + cache.put(key, "value" + key); + System.out.println(Thread.currentThread().getName() + " " + key); + countDownLatch.countDown(); + }).forEach(executorService::submit); + countDownLatch.await(); + } finally { + executorService.shutdown(); + } + assertEquals(cache.size(), size); + IntStream.range(0, size).forEach(i -> assertEquals(cache.get(i).get(), "value" + i)); + } +}