Search code examples
javamultithreadingsynchronized

How to make a thread skip to the next line of code when callling a synchronized method that's currently blocked?


Scenario

Multiple threads add objects to an ArrayList list, which is an attribute of a ListHolder object listHolder, which is shared by all the threads. The threads call ListHolder.addObject() to add an object to list. The addObject() method contains a synchronized block that passes list as the monitor object, to prevent multithreading errors. Periodically, (in an independent and asynchonous mannner) each thread will call a method ListHolder.processList() that performs operations on list and leaves it empty.

If processList() were made a synchronized method, a thread attempting to call it whilst it is in use will wait until the blocking thread is complete, and then it will call it.

Question

However, how do you make it so that a thread calling processList(), whilst it is already in use (blocked) by another thread, will not wait but rather move to the next line of code?

This would significantly boost efficiency since, once the blocked thread finishes it will then try to process an empty list, whereas it could, instead of waiting, be constructing the objects to add to list.


Solution

  • I can think of at least two potential solutions.

    1. Use a Semaphore.

    2. Process a copy of the list without holding the lock.

    Semaphore

    You can create a Semaphore with a single permit. That class has a method named tryAcquire() that tries to acquire a permit immediately; if one was available, it returns true, otherwise it returns false.

    For example:

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.Semaphore;
    
    public class ListHolder {
        
      private final Semaphore processPermit = new Semaphore(1);
      private final List<Foo> list = new ArrayList<>();
    
      public void addObject(Foo object) {
        synchronized (list) {
          list.add(object);
        }
      }
    
      public void processList() {
        if (processPermit.tryAcquire()) {
          try {
            synchronized (list) {
              // process list...
            }
          } finally {
            processPermit.release();
          }
        }
      }
    }
    

    This will only allow one thread to process the list at any given time. If a thread is already processing the list, then another thread will simply return from processList() immediately, without having done anything.

    A downside of this approach is that while a thread is processing the list, no other thread can add to the list, potentially bottlenecking your producers.

    Process a Copy

    If your scenario allows it, you could instead copy the list, clear it, and then process the copy without holding the lock. You would only synchronize on the list when creating the copy.

    For example:

    import java.util.ArrayList;
    import java.util.List;
    
    public class ListHolder {
        
      private final List<Foo> list = new ArrayList<>();
    
      public void addObject(Foo object) {
        synchronized (list) {
          list.add(object);
        }
      }
    
      public void processList() {
        List<Foo> copy = null;
    
        synchronized (list) {
          if (!list.isEmpty()) {
            copy = new ArrayList<>(list);
            list.clear();
          }
        }
    
        if (copy != null) {
          // do processing...
        }
      }
    }
    

    Copying the list should take only a short time relative to the processing, assuming the processing is expensive. That way a thread doesn't hold the lock for an undue amount of time.

    An upside to this approach is that multiple threads can process different sets of data simultaneously, while still allowing other threads to construct and add objects to the list relatively unimpeded. However, this approach is not feasible if, for reasons not stated in your question, you need to hold the lock during the entire processing operation, or if two threads must never process elements simultaneously.

    Bonus: Hybrid Solution

    You may also want to consider a hybrid of the two solutions above. Use a Semaphore so only one thread can process the list at any given time, but process a copy so that other threads can continue to add objects to the list.

    For example:

    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.Semaphore;
    
    public class ListHolder {
        
      private final Semaphore processPermit = new Semaphore(1);
      private final List<Foo> list = new ArrayList<>();
    
      public void addObject(Foo object) {
        synchronized (list) {
          list.add(object);
        }
      }
    
      public void processList() {
        if (processPermit.tryAcquire()) {
          try {
            List<Foo> copy;
            synchronized (list) {
              copy = new ArrayList<>(list);
              list.clear();
            }
            processList(copy);
          } finally {
            processPermit.release();
          }
        }
      }
    
      private void processList(List<Foo> copy) {
        // do processing...
      }
    }