Search code examples
javamultithreadingconcurrencylrureentrantlock

ReentrantReadWriteLock readLock giving time limit exceeded


I'm trying to learn concurrency

I was playing around with below code:

https://leetcode.com/problems/lru-cache/discuss/724784/Detailed-Explanation-with-Threadsafe-LRU-Cache-in-Java

class LRUCache {

    /**              
         Algorithm :
         
         1. Everytime when we add the node check if it exists .
            1.1 if exists then update the value .
            1.2 move this node to head.
            1.3 if the capaicity reaches then remove node from tail .
            1.4 move the current node to head.
            
         2. Everytime when we get 
            2.1 then we need to move this node to head.
            
         3. Remove the node from tail thats it.   
          
       }
    **/
    
    private int  capacity;
    private AtomicInteger  curSize = new AtomicInteger() ;
    private ConcurrentHashMap<Integer,Node> map = new ConcurrentHashMap<>();
    private Node head = new Node();
    private Node tail = new Node();
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    
    public LRUCache(int capacity) {
        this.capacity = capacity;
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        if(!map.containsKey(key))
             return -1;
        Node node = map.get(key);
        // move this current Node to front
        moveToHead(node);
        return node.val;
        
    }
    
    public void put(int key, int value) {
        // The key is already present 
        Node curNode = map.get(key);
        if(curNode != null){
           // if value exist update
            curNode.val=value;
           // moveTo head , so now its used recently 
           moveToHead(curNode);
           return;
        }
        Node newNode = new Node(value,key);
        map.put(key,newNode);
        addToHead(newNode);
        if( curSize.incrementAndGet() > capacity){
            Node nodeToRemove = tail.prev;
            removeNode(nodeToRemove);
            map.remove(nodeToRemove.key);
            curSize.decrementAndGet();
        }
    }
    
    private void moveToHead(Node node){
        // remove 
        removeNode(node);
        addToHead(node);
        
    }
    
    private void removeNode(Node node){
        try{
            readWriteLock.writeLock().lock();
            Node prev = node.prev;
            Node next = node.next;
            prev.next = next;
            next.prev = prev;
        }finally{
            readWriteLock.writeLock().unlock();
        }
    }
    
    private void addToHead(Node node){
        try{
            readWriteLock.writeLock().lock();
            node.next = head.next;
            node.prev = head;
            head.next.prev = node;
            head.next = node;
        }finally{
            readWriteLock.writeLock().unlock();
        }
    }
}

class Node {
   Integer val;
   Integer key; 
   Node next ;
   Node prev;
    
   public Node(int val, int key){
         this.val = val;
         this.key = key;
    } 
    
   public Node(){
       
   } 
}

/**
 * Your LRUCache object will be instantiated and called as such:
 * LRUCache obj = new LRUCache(capacity);
 * int param_1 = obj.get(key);
 * obj.put(key,value);
 */

I observed the get method doesn't have any thread-safety, so I modified it this way

    public int get(int key) {
     readWriteLock.readLock().lock();

   try{
        if(!map.containsKey(key))
             return -1;
        Node node = map.get(key);
        // move this current Node to front
        moveToHead(node);
        return node.val;
       
         }finally{
            readWriteLock.readLock().unlock();
        }   
        
    }

I thought this would ensure, reads wouldn't take place when writes are going on.

But I'm getting Time Limit Exceeded in Leetcode after modifying the get function. Please can anyone explain why?


Solution

  • I'm trying to learn concurrency

    I was playing around with below code:

    I would strongly recommend you not to learn concurrency from this code, because it is full with all kinds of concurrency errors.

    From naive errors like this one:

            if(!map.containsKey(key))
                 return -1;
            Node node = map.get(key);
            // move this current Node to front
            moveToHead(node);
            return node.val;
    

    After containsKey and before get another thread might remove the key, as a result, we would get node==null and NullPointerExcetion.

    To more java-specific errors like this one:

    curNode.val=value;
    

    here the new value is writen to val without proper synchronization, this creates a so called data race (i.e. reads of val in other threads will return weird results).

    The easiest way to fix the code would be to make every public method synchronized and to throw away readWriteLock.
    Also in this case it would be reasonable to replace AtomicInteger and ConcurrentHashMap with int and HashMap (you don't need concurrent versions with synchronized).

    If you want to learn concurrency in java, then I would recommend:

    1. Java Concurrency in Practice — it is a book by one of java authors which gives simple practical rules for writing concurrent code in java. (Unfortunately IMHO it doesn't go deep enough)
    2. to learn Java Memory Model, which is a part of Java Language Specification that defines what happens when multiple java threads access the same data in memory.
      It is crucial at least to learn happens-before rules.
      Unfortunately, I don't know any simple and clear book/article on that.
    3. The Art of Multiprocessor Programming to learn about various lock-free and wait-free algorithms (for which things like AtomicInteger are really exist).