Java - Concurrency

Updated: 2021-11-19

Most implementations of the Java virtual machine run as a single process. When we talk about "concurrency" in Java, it is mostly multi-thread instead of multi-process.

Intrinsic Lock vs Explicit Lock

  • Intrinsic = it comes with every object or class, also referred to as "monitor"
    • synchronized keyword uses intrinsic locks
    • Object monitor methods: wait, notify and notifyAll
  • Explicit = you need to explicitly declare a Lock
    • the interface: Lock
    • implementations: e.g. ReentrantLock, ReentrantReadWriteLock

synchronized vs Lock:

  • The biggest advantage of Lock objects over implicit locks is their ability to back out of an attempt to acquire a lock.
  • synchronized cannot have a timeout, while Lock can call tryLock(long time, TimeUnit unit) where time is the max time to wait
  • synchronized is within a single method, while a Lock can call lock() and unlock() in different methods
  • A thread that’s blocked on an intrinsic lock is not interruptible.
  • There’s exactly one way to acquire an intrinsic lock: a synchronized block.
  • Deadlock on intrinsic lock has to be solved by killing the JVM

Intrinsic Lock

Enforcing exclusive access to an object's state and establishing happens-before relationships that are essential to visibility.

Two types of intrinsic lock:

  • instance lock: attached to an object, only blocks other threads from invoking a synchronized instance method, not static synchronized method or methods without synchronized keyword.
  • static lock: attached to a class, only blocks other threads from invoking a static synchronized method, not a synchronized instance method or methods without synchronized keyword.
    • A static method is associated with a class, not an object. In this case, the thread acquires the intrinsic lock for the Class object associated with the class. Thus access to class's static fields is controlled by a lock that's distinct from the lock for any instance of the class.

Explicit Lock

The benefits:

  • support wait/notify, through their associated Condition objects
  • able to back out of an attempt to acquire a lock: tryLock method and lockInterruptibly method.

Where a Lock replaces the use of synchronized methods and statements, a Condition replaces the use of the Object monitor methods.

Reentrant Lock: reentrant mutex (a.k.a. recursive mutex, recursive lock) is a particular type of mutual exclusion (mutex) device that may be locked multiple times by the same process/thread, without causing a deadlock.

  • Deadlock: it doesn’t avoid deadlock, it simply provides a way to recover when it happens
  • Livelock: if all the threads time out at the same time, it’s quite possible for them to immediately deadlock again.

happen-before

happens-before relationship: simply a guarantee that memory writes by one specific statement are visible to another specific statement.

The results of a write by one thread are guaranteed to be visible to a read by another thread only if the write operation happens-before the read operation.

The synchronized and volatile constructs, as well as the Thread.start() and Thread.join() methods, can form happens-before relationships.

synchronized vs volatile vs Atomic

Two factors to consider:

  • mutual exclusion: no two concurrent processes/threads are in their critical section at the same time
  • happen-before: data modified by one thread should be visible to other threads
Mutual Exclusion happen-before
synchronized Yes Yes
volatile No Yes
Atom Yes No

Keyword: synchronized

Synchronized on "intrinsic lock" ("intrinsic lock" is implied by each use of synchronized keyword).

Note: synchronization has no effect unless both read and write operations are synchronized.

Usage

Two ways to use synchronized keyword:

  • synchronized method
  • synchronized statements: Unlike synchronized methods, synchronized statements must specify the object that provides the intrinsic lock

These two are equivalent:

Synchronized methods:

public synchronized void blah() {
  // ...
}

Synchronized statement: (synchronized (this) acquires the instance lock)

public void blah() {
  synchronized (this) {
    // ...
  }
}

To acquire a static in a synchronized statement(not in method header):

synchronized(Foo.class)
// or
synchronized(this.getClass())

Java Runnable vs Callable

  • Runnable: does not return a result and cannot throw a checked exception.
  • Callable: return a result

Java Thread Scheduling

  • The JVM schedules using a preemptive, priority based scheduling algorithm.
  • All Java threads have a priority and the thread with he highest priority is scheduled to run by the JVM.
  • In case two threads have the same priority a FIFO ordering is followed.

A different thread is invoked to run in case one of the following events occur:

  1. The currently running thread exits the Runnable state ie either blocks or terminates.
  2. A thread with a higher priority than the thread currently running enters the Runnable state. The lower priority thread is preempted and the higher priority thread is scheduled to run.

Time Slicing is dependent on the implementation.

A thread can voluntarily yield control through the yield() method. Whenever a thread yields control of the CPU another thread of the same priority is scheduled to run. A thread voluntarily yielding control of the CPU is called Cooperative Multitasking.

Thread Priorities

JVM selects to run a Runnable thread with the highest priority.

All Java threads have a priority in the range 1-10.

Top priority is 10, lowest priority is 1.

Normal priority ie. priority by default is 5.

  • Thread.MIN_PRIORITY - minimum thread priority
  • Thread.MAX_PRIORITY - maximum thread priority
  • Thread.NORM_PRIORITY - normal thread priority

Whenever a new Java thread is created it has the same priority as the thread which created it.

Thread priority can be changed by the setpriority() method.

Fork/Join Framework

Official Doc

the idea is :

if (my portion of the work is small enough)
  do the work directly
else
  split my work into two pieces
  invoke the two pieces and wait for the results

Used in JDK:

  • java.util.Arrays Arrays.parallelSort()
  • java.util.streams

Fork-Join breaks the task at hand into mini-tasks until the mini-task is simple enough that it can be solved without further breakups.

Two key classes: ForkJoinPool and ForkJoinTask

  • ForkJoinPool
ForkJoinPool pool = new ForkJoinPool(numberOfProcessors);
  • ForkJoinTask: 2 impl
    • RecursiveAction: does not return a value
    • RecursiveTask: returns an object of specified type
public class MyForkJoinTask extends RecursiveAction {
    @Override
    protected void compute() {
        . . . // your problem invocation goes here
    }
}

To execute:

pool.invoke(task);

Avoid using wait and notifyAll

As suggested by Effective Java, wait and notify should NOT be used in new code.

When wait is invoked, the thread releases the lock and suspends execution

At some future time, another thread will acquire the same lock and invoke Object.notifyAll, informing all threads waiting on that lock that something important has happened:

Future

java.util.concurrent.Future holds the result of an async task but requires calling isDone() to check if the task is complete. Calling get() will block, a blocking get() call on a worker threadpool can result in all queued work being stopped.

Java 8 introduced CompletableFuture as an extension to Future. It is similar to Javascript's Promise: you can add a callback function in thenApply().

An alternative to CompletableFuture is Guava's ListenableFuture, where a callback can be added (addListener(Runnable, Executor)), so after the task is complete or errored, the callback function will be executed.

Atomicity

  • Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).
  • Reads and writes are atomic for all variables declared volatile (including long and double variables).

Third-party Libraries

RXJava: a library that is designed to help implement the Observer pattern. Stream support is one of the most compelling features of RXJava. It is good for implementing Streaming RPC API. The Flowable type relies on Java 9 Flow interfaces and is a good way to work with streams of data. It is primarily popular in Android, but it is losing popularity to Kotlin coroutine.