Concurrency || Collection - ashish-ghub/docs GitHub Wiki

Difference Between CompletableFuture and Future in Java

Both Future and CompletableFuture in Java are used to represent the result of an asynchronous computation. However, CompletableFuture, introduced in Java 8, significantly enhances the capabilities of the older Future interface. Here's a comparison:

I. Future

  • Characteristics:

    • Represents the result of an asynchronous computation.
    • Provides methods to check if the computation is complete (isDone()), wait for its completion (get()), and cancel it (cancel()).
    • get() is a blocking call; the thread invoking it will wait until the result is available (or the future is cancelled or times out).
    • Limited error handling capabilities. Exceptions during computation are typically wrapped in an ExecutionException.
    • Difficult to compose or chain asynchronous operations. Requires manual checking and handling of completion.
    • Doesn't inherently support reactive programming or callbacks upon completion.
  • Limitations:

    • Blocking get(): The blocking nature of get() can lead to inefficient use of threads, especially when waiting for multiple independent asynchronous tasks.
    • Lack of Completion Notification: No easy way to trigger actions or execute further computations automatically when a Future completes.
    • Difficult Composition: Combining the results of multiple Futures or performing subsequent asynchronous operations based on a Future's result requires cumbersome and often nested code.
    • Limited Error Handling: Handling exceptions requires catching ExecutionException and then unwrapping the underlying cause.
  • Use Cases (Primarily Legacy):

    • Suitable for simple asynchronous tasks where you just need to submit a task and retrieve the result later, and blocking is acceptable.
    • In older codebases or libraries that haven't fully adopted CompletableFuture.
  • Example:

    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    import java.util.concurrent.Future;
    
    public class FutureExample {
        public static void main(String[] args) throws Exception {
            ExecutorService executor = Executors.newSingleThreadExecutor();
            Future<String> future = executor.submit(() -> {
                Thread.sleep(1000);
                return "Result from Future";
            });
    
            System.out.println("Doing something else...");
            String result = future.get(); // Blocking call
            System.out.println("Result: " + result);
            executor.shutdown();
        }
    }

II. CompletableFuture

  • Characteristics:

    • Extends Future and implements CompletionStage, providing a powerful framework for asynchronous programming.
    • Supports non-blocking operations and asynchronous callbacks.
    • Enables easy composition and chaining of asynchronous tasks using methods like thenApply(), thenCompose(), thenAccept(), thenCombine(), thenEither(), etc.
    • Offers comprehensive error handling mechanisms using methods like exceptionally(), handle(), whenComplete().
    • Supports explicit completion of the future using complete() and completeExceptionally().
    • Can be triggered manually or as a result of another asynchronous computation.
    • Facilitates building reactive pipelines and handling asynchronous streams of data.
  • Advantages:

    • Non-Blocking Operations: Allows for more efficient thread utilization by avoiding unnecessary blocking.
    • Completion Callbacks: Enables executing code automatically when a CompletableFuture completes (either successfully or with an error).
    • Easy Composition: Provides a fluent API for chaining and combining asynchronous operations in a clear and concise manner.
    • Enhanced Error Handling: Offers robust mechanisms for handling exceptions in asynchronous computations.
    • Manual Completion: Allows for creating futures that are completed based on external events or manual logic.
    • Integration with Reactive Programming: Its asynchronous and non-blocking nature aligns well with reactive programming paradigms.
  • Use Cases:

    • Building highly concurrent and responsive applications.
    • Orchestrating complex asynchronous workflows involving multiple dependent or independent tasks.
    • Implementing non-blocking I/O operations.
    • Handling asynchronous streams of data and events.
    • Integrating with reactive frameworks.
  • Example:

    import java.util.concurrent.CompletableFuture;
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class CompletableFutureExample {
        public static void main(String[] args) throws Exception {
            ExecutorService executor = Executors.newSingleThreadExecutor();
            CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                return "Result from CompletableFuture";
            }, executor)
            .thenApply(result -> "Processed: " + result)
            .exceptionally(Throwable::getMessage);
    
            System.out.println("Doing something else asynchronously...");
            String finalResult = future.get(); // Can still block if needed, but non-blocking alternatives exist
            System.out.println("Final Result: " + finalResult);
            executor.shutdown();
        }
    }

III. Key Differences Summarized

Feature Future CompletableFuture
Blocking Primarily blocking (get()) Supports both blocking (get()) and non-blocking (callbacks)
Composition Difficult, manual Easy and fluent API (thenApply, thenCompose, etc.)
Completion Notification Limited Extensive callback mechanisms (thenAccept, whenComplete, etc.)
Error Handling Basic (ExecutionException) Comprehensive (exceptionally, handle, whenComplete)
Manual Completion Not directly supported Yes (complete(), completeExceptionally())
Reactive Support Limited Strong alignment with reactive principles
Introduction Java 5 Java 8

Conclusion:

CompletableFuture is a significant improvement over Future, providing a more powerful and flexible way to handle asynchronous computations in Java. Its non-blocking nature, ease of composition, and robust error handling make it the preferred choice for building modern, concurrent applications. While Future still has its place, especially in legacy code, CompletableFuture should be favored for new asynchronous programming tasks in Java 8 and later.

Concurrency

Understanding Thread Interruption in Java

https://praveer09.github.io/technology/2015/12/06/understanding-thread-interruption-in-java/#:~:text=In%20Java%2C%20one%20thread%20cannot,as%20true%20on%20the%20instance.

In Java, one thread cannot stop the other thread. A thread can only request the other thread to stop. The request is made in the form of an interruption. Calling the interrupt() method on an instance of a Thread sets the interrupt status state as true on the instance.

Summary

The answers to the two questions that I had set out to answer are:

How to request a task, running on a separate thread, to finish early?

Use interruption. If using Thread directly in your code, you may call interrupt() on the instance of thread. If using Executor framework, you may cancel each task by calling cancel() on Future

If using Executor framework, you may shutdown the ExecutorService by calling the shutdownNow() method.

How to make a task responsive to such a finish request?

Handle interruption request, which in most of the cases is done by handling InterruptedException. Also preserve the interruption status by calling Thread.currentThread().interrupt().

The Java Memory Model - The Basics

https://www.youtube.com/watch?v=LCSqZyjBwWA&list=PLL8woMHwr36G7eI_3r4-sNKcEVQstTJck&index=0

what is thread stack?

  • a thread consist of own copy of local variable and local object references which is not shared between the threads.
  • local variables are thread safe

Java Happens Before Guarantee - Java Memory Model - Part 2 https://www.youtube.com/watch?v=oY14UyP61F8&list=PLL8woMHwr36G7eI_3r4-sNKcEVQstTJck&index=2

Java-future

https://www.baeldung.com/java-future

Future class represents a future result of an asynchronous computation – a result that will eventually appear in the Future after the processing is complete.

Let's see how to write methods that create and return a Future instance.

Long running methods are good candidates for asynchronous processing and the Future interface. This enables us to execute some other process while we are waiting for the task encapsulated in Future to complete.

Some examples of operations that would leverage the async nature of Future are:

  1. computational intensive processes (mathematical and scientific calculations)
  2. manipulating large data structures (big data)
  3. remote method calls (downloading files, HTML scrapping, web services).

Sample:

import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future;

public class CacheLoader {

private ExecutorService executor = Executors.newSingleThreadExecutor();

public Future<Cache> loadCache() {
    return executor.submit(() -> {
        // Perform the time-consuming task of loading the cache
        Cache cache = new Cache();
        // load data into cache here
        return cache;
    });
}

public static void main(String[] args) throws Exception {
    CacheLoader cacheLoader = new CacheLoader();
    Future<Cache> cacheFuture = cacheLoader.loadCache();

    // Do other tasks while the cache is loading
    // ...

    // Retrieve the cache when it's ready
    Cache cache = cacheFuture.get();

    // Use the cache
    // ...
 }

}


java8 -completablefuture useful article must read

Java 8 LongAdders: The Right Way To Manage Concurrent Counters:

https://blog.overops.com/java-8-longadders-the-fastest-way-to-add-numbers-concurrently/

Java 8 StampedLocks vs. ReadWriteLocks and Synchronized

https://blog.overops.com/java-8-stampedlocks-vs-readwritelocks-and-synchronized/

Java Concurrency: Read / Write Locks

https://dzone.com/articles/java-concurrency-read-write-lo

Immutable class

  • The class must be declared as final (So that child classes can’t be created)
  • Data members in the class must be declared as final (So that we can’t change the value of it after object creation)
  • A parameterized constructor
  • Getter method for all the variables in it and Getter for object reference needs to be made immutable (see below Date, List as a getter how it can break)
  • No setters(To not have the option to change the value of the instance variable)

Example to create Immutable class

Q. Create a immutable employee class has name, doj , salary, Hobbies list

public class Immutable {

private final String name;

private Date dateOfBirth;

private List<String> hobbies;

public Immutable(String name, Date dateOfBirth, List<String> hobbies) {
    this.name = name;
    this.dateOfBirth = dateOfBirth;
    this.hobbies = hobbies;
}

public String getName() {
    return name;
}

public Date getDateOfBirth() {
    return dateOfBirth;  // not safe date is not immutable
}

public List<String> hobbies() {
   return hobbies ;  // not safe as list is not immutable 
}

}

  • 1 getName() is fine as it returns immutable object as well. However the getDateOfBirth() method can break immutability because the client code can modify returned object, hence modifying the Immutable object as well:

Immutable imm = new Immutable("John", new Date());

imm.getName(); //safe

Date dateOfBirth = imm.getDateOfBirth();

dateOfBirth.setTime(0); //we just modified imm object not safe

  • 2 It is safe to return immutable objects and primitives (as they are returned by value). However you need to make defensive copies of mutable objects, like Date:

public Date getDateOfBirth() { return new Date(dateOfBirth.getTime()); }

  • 3 and wrap collections in immutable views (if they are mutable), e.g. see Collections.unmodifiableList():

public List<Integer> getHobbies() { return Collections.unmodifiableList(hobbies); }

Choosing between synchronized and ReentrantLock

  • ReentrantLock is an advanced tool for situations where intrinsic locking is not practical. Use it if you need its advanced features: timed, polled, or interruptible lock acquisition, fair queueing, or non-block-structured locking. Otherwise, prefer synchronized.

  • ReentrantLock provides the same locking and memory semantics as intrinsic locking, as well as additional features such as timed lock waits, interruptible lock waits, fairness, and the ability to implement non-block-structured locking. The performance of ReentrantLock appears to dominate that of intrinsic locking, winning slightly on Java 6 and dramatically on Java 5.0. So why not deprecate synchronized and encourage all new concurrent code to use ReentrantLock?

  • Intrinsic locks still have significant advantages over explicit locks. Reentrant- Lock is definitely a more dangerous tool than synchronization; if you forget to wrap the unlock call in a finally block, your code will probably appear to run properly, but you’ve created a time bomb that may well hurt innocent bystanders. Save ReentrantLock for situations in which you need something ReentrantLock provides that intrinsic locking doesn’t.

  • ReentrantLock is an advanced tool for situations where intrinsic locking is not practical. Use it if you need its advanced features: timed, polled, or interruptible lock acquisition, fair queueing, or non-block-structured locking. Otherwise, prefer synchronized.

Under Java 5.0, intrinsic locking has another advantage over ReentrantLock:

thread dumps show which call frames acquired which locks and can detect and identify deadlocked threads. The JVM knows nothing about which threads hold ReentrantLocks and therefore cannot help in debugging threading problems using ReentrantLock. This disparity is addressed in Java 6 by providing a management and monitoring interface with which locks can register, enabling locking information for ReentrantLocks to appear in thread dumps and through other management and debugging interfaces. The availability of this information for debugging is a substantial, if mostly temporary, advantage for synchronized; locking information in thread dumps has saved many programmers from utter consternation. The non-block-structured nature of ReentrantLock still means that lock acquisitions cannot be tied to specific stack frames, as they can with intrinsic locks.

High performing caching


public class LRUCache extends LinkedHashMap {
   /**
     * The read-write lock can be re-entered to ensure concurrent read-write security.
     */
    private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    private Lock readLock = readWriteLock.readLock();
    private Lock writeLock = readWriteLock.writeLock();
   /**
     * Cache Size Limit
     */
    private int maxSize;
   public LRUCache(int maxSize) {
        super(maxSize + 1, 1.0f, true);
        this.maxSize = maxSize;
    }
  @Override
    public Object get(Object key) {
        readLock.lock();
        try {
            return super.get(key);
        } finally {
            readLock.unlock();
        }
    }
    @Override
    public Object put(Object key, Object value) {
        writeLock.lock();
        try {
            return super.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return this.size() > maxSize;
    }
}

Semaphore : Semaphore limit the number of concurrent threads accessing a specific resource.


Use-case : To limit the resource utilization 1) allow N no of parallel computation or 2) allow N user to login to system

acquire() : Acquires a permit from this semaphore, blocking until one is available, or the thread is interrupted.

package com.concurrent;

import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

import java.util.concurrent.Semaphore;

public class SemaphoreBounded {

private final Map<String, String> lockMap = new ConcurrentHashMap<>();
private final Semaphore semaphore;

public SemaphoreBounded(int maxComputation)
{
  this.semaphore = new Semaphore(maxComputation);
}

public interface Task<T>
{
  T execute();
}

public <T> T process(String specIdLock, Task<T> task)
{
  String lockId = lockMap.putIfAbsent(specIdLock, specIdLock);
  String localLock = lockId != null?lockId:specIdLock;

  synchronized (localLock)
  {
     try
     {
        semaphore.acquire();
        task.execute();
     }
     catch (InterruptedException e)
     {
        // TODO log error here
     }
     finally
     {
        semaphore.release();
     }

  }

  return null;

}

public void removeFromCache(String specId)
{
  lockMap.remove(specId);
}

}


Collection

  • hashtable-vs-hashmap-vs-concurrenthashmap**

https://medium.com/@mr.anmolsehgal/hashtable-vs-hashmap-vs-concurrenthashmap-4aa0ff1eecc4

  • java-hashmap-internal-implementation

https://medium.com/@mr.anmolsehgal/java-hashmap-internal-implementation-21597e1efec3

  • concurrenthashmap-internal-working-in-java

https://medium.com/@mr.anmolsehgal/concurrenthashmap-internal-working-in-java-b2a1a48c7289

  • concurrenthashmap details

https://medium.com/@itsromiljain/curious-case-of-concurrenthashmap-90249632d335

  • ConcurrentHashMap isn't always enough

https://dzone.com/articles/concurrenthashmap-isnt-always-enough

  • Hashing

https://www.baeldung.com/cs/hashing

Issue : Race condition when 2 threads try it

class A {

 private Map<String, Object> theMap = new ConcurrentHashMap<>();
public Object getOrCreate(String key) {
	Object value = theMap.get(key);
	if (value == null) {
		value = new Object();
		theMap.put(key, value);
	}
	return value;
}

}

solution 1: not good

public synchronized Object getOrCreate(String key) {
	Object value = theMap.get(key);
	if (value == null) {
		value = new Object();
		theMap.put(key, value);
	}
	return value;
}

solution 2 : Good

A much better approach should be using Java 8 Map's computeIfAbsent(K key, Function mappingFunction), which, in ConcurrentHashMap's implementation runs atomically:

private Map<String, Object> theMap = new ConcurrentHashMap<>();

public Object getOrCreate(String key) {
	return theMap.computeIfAbsent(key, k -> new Object());
}

The atomicity of computeIfAbsent(..) assures that only one new Object will be created and put into theMap, and it'll be the exact same instance of Object that will be returned to all threads calling the getOrCreate function. Here, not only the code is correct, it's also cleaner and much shorter.

public class ListHelper { public List list = Collections.synchronizedList(new ArrayList()); ... public synchronized boolean putIfAbsent(E x) { boolean absent = !list.contains(x); if (absent) list.add(x); return absent; }

  • ArrayList Vs CopyOnWriteArray

Both ArrayList and CopyOnWriteArray implement List interface. But There are lots of differences between ArrayList and CopyOnWriteArrayList:

  1. CopyOnWriteArrayList creates a Cloned copy of underlying ArrayList, for every update operation at certain point both will synchronized automatically which is takes care by JVM. Therefore there is no effect for threads which are performing read operation. Therefore thread-safety is not there in ArrayList whereas CopyOnWriteArrayList is thread-safe.

  2. While Iterating ArrayList object by one thread if other thread try to do modification then we will get Runt-time exception saying ConcurrentModificationException. Where as We won’t get any Exception in the case of CopyOnWriteArrayList.

  3. ArrayList is introduced in JDK 1.2 whereas CopyOnWriteArrayList is introduced by SUN people in JDK 1.5.

  4. Iterator of ArrayList can perform remove operation while iteration. But Iterator of CopyOnWriteArrayList cant perform remove operation while iteration, otherwise it will throw run-time exception UnsupportedOperationException.

Below is the implementation of this point.

// Java program to illustrate ArrayList

import java.util.*;

class CopyDemo {

public static void main(String[] args)  
{ 
    ArrayList l = new ArrayList(); 
    l.add("A"); 
    l.add("B"); 
    l.add("C"); 
    Iterator itr = l.iterator(); 
      
    while (itr.hasNext())  
    { 
        String s = (String)itr.next(); 
          
        if (s.equals("B")) 
        { 
            // Can remove 
            itr.remove(); 
        } 
    } 
    System.out.println(l); 
} 

}

⚠️ **GitHub.com Fallback** ⚠️