Java - Concurrency
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"
synchronizedkeyword uses intrinsic locks- Object monitor methods:
wait,notifyandnotifyAll
- Explicit = you need to explicitly declare a
Lock- the interface:
Lock - implementations: e.g.
ReentrantLock,ReentrantReadWriteLock
- the interface:
synchronized vs Lock:
- The biggest advantage of
Lockobjects over implicit locks is their ability to back out of an attempt to acquire a lock. synchronizedcannot have a timeout, whileLockcan calltryLock(long time, TimeUnit unit)wheretimeis the max time to waitsynchronizedis within a single method, while aLockcan calllock()andunlock()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
synchronizedinstance method, notstatic synchronizedmethod or methods withoutsynchronizedkeyword. - static lock: attached to a class, only blocks other threads from invoking a
static synchronizedmethod, not asynchronizedinstance method or methods withoutsynchronizedkeyword.- 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 associatedConditionobjects - able to back out of an attempt to acquire a lock:
tryLockmethod andlockInterruptiblymethod.
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:
- The currently running thread exits the Runnable state ie either blocks or terminates.
- 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 priorityThread.MAX_PRIORITY- maximum thread priorityThread.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
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 valueRecursiveTask: 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
longanddouble). - Reads and writes are atomic for all variables declared
volatile(includinglonganddoublevariables).
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.