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"
synchronized
keyword uses intrinsic locks- Object monitor methods:
wait
,notify
andnotifyAll
- 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
Lock
objects over implicit locks is their ability to back out of an attempt to acquire a lock. synchronized
cannot have a timeout, whileLock
can calltryLock(long time, TimeUnit unit)
wheretime
is the max time to waitsynchronized
is within a single method, while aLock
can 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
synchronized
instance method, notstatic synchronized
method or methods withoutsynchronized
keyword. - static lock: attached to a class, only blocks other threads from invoking a
static synchronized
method, not asynchronized
instance method or methods withoutsynchronized
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 associatedCondition
objects - able to back out of an attempt to acquire a lock:
tryLock
method andlockInterruptibly
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:
- 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
long
anddouble
). - Reads and writes are atomic for all variables declared
volatile
(includinglong
anddouble
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.