Low-Level Concurrency in Java

Rahul Saha
Oct 11, 2020 · 6 min read
Photo by amirali mirhashemian on Unsplash

Java is a multi-threaded language. Since the beginning of the language it had first class support of multiple threads for our concurrency needs. After that with every release of Java the multi-thread model has been simpler and more accessible to users. From ThreadPoolExecutor, Future in Java 5 to CompletableFuture, ForkJoin framework etc in Java 8. Also powerful third-party frameworks like RxJava evolved in the wild.

Though these abstractions make concurrency more available to the average user, one still should learn about the nuts and bolts of the language (and JVM). It will help clear the picture of concurrency and appreciate the skillfully designed concurrency abstractions we use.

Higher level constructs like ThreadPoolExecutor, CompletableFuture, Latch etc. are intentionally omitted.

A thread is a thread of execution in a program. You can run as many threads in parallel as many CPU cores you have. Although there can be hundreds of them in the waiting state. A thread represents a system thread as provided by the underlying OS (in most JVM implementations). Just like processes in an OS, threads are scheduled and have a life-cycle.

A thread can be created by creating a thread object and passing a runnable to it. Bear in mind creating a runnable does not create a thread. If you only create a runnable and run it directly it will just be executed by the current thread.

Threads can also be set daemon, meaning JVM can exit while these threads are running.

Output

This is Thread-0
And this is main

Execution of a thread can be paused for an amount of time using Thread.sleep() call. Threads can be interrupted when sleeping.

Threads can optionally call Thread.yield() during execution. This serves as a hint that the thread is willing to give up core for other threads. Though there is no guarantee that the scheduler will take the hint.

A thread can wait for another thread to complete by anotherThread.join()method. Optionally a timeout can be provided.

Synchronization is the most common way of orchestrating multiple-threads. The synchronized keyword makes it extremely easy to use this. Under the hood this is also a locking mechanism. All objects automatically contain a lock object aka monitor.

public class Test {    public synchronized void method1(){    }    public synchronized void method2(){    }}

If a method is decorated with synchronized keyword calling threads will be required to lock the object monitor. If no other thread currently holds the lock, the lock will be acquired by this thread. When the method call ends (even with exception) the lock will be released. In the meantime if another thread calls one any other synchronized method on this object it will go waiting until it acquires the lock.

Locks are on object level. So two threads can in parallel execute on the same synchronized method on different objects. Similarly if one thread enters a synchronized method on an object, other synchronized methods also become unavailable for other threads. However the lock is reentrant, so lock acquired thread can call other synchronized methods as well.

Also, blocks can also be synchronized if the whole method doesn’t need synchronization.

public void method(){
// Some regular stuff
synchronized (this){
// Critical section
}
// Some regular stuff
}

Or a different object can be used (better be final)

public class Test {
private final Object lock = new Object();
public void method() {
// Some regular stuff
synchronized (lock) {
// Critical section
}
// Some regular stuff
}
}

Just like synchronized which uses locks implicitly, locks can be explicitly used too. When using synchronized threads will wait indefinitely to acquire lock. The locking mechanism is fixed. In most cases this works fine.

But to design a low level synchronized structure, much more fine-grained control may be required. Locks allow us to add additional behavior in locking.

For example using lock objects a thread can ‘try’ to acquire a lock and there can be a different control flow if it fails to acquire it. Or maybe threads can wait for a given time interval to acquire the lock.

Following is a simple example,

Output (yes it is little hard to read):

[1602432762934] Thread-1 is buying ticket
[1602432763935] Thread-0 could not buy a ticket. Will try again.
[1602432763935] Thread-3 could not buy a ticket. Will try again.
[1602432763936] Thread-4 could not buy a ticket. Will try again.
[1602432763936] Thread-2 could not buy a ticket. Will try again.
[1602432763950] Thread-0 counted till 915318571
[1602432764027] Thread-3 counted till 1887855699
[1602432764028] Thread-4 counted till 690348044
[1602432764027] Thread-2 counted till 338949936
[1602432764954] Thread-0 could not buy a ticket. Will try again.
[1602432764955] Thread-0 counted till 777823324
[1602432764978] Thread-1 has bought the ticket
[1602432764979] Thread-3 is buying ticket
[1602432765032] Thread-4 could not buy a ticket. Will try again.
[1602432765032] Thread-2 could not buy a ticket. Will try again.
[1602432765033] Thread-2 counted till 694490797
[1602432765032] Thread-4 counted till 1587458175
[1602432765956] Thread-0 could not buy a ticket. Will try again.
[1602432765956] Thread-0 counted till 1309044232
[1602432766034] Thread-2 could not buy a ticket. Will try again.
[1602432766034] Thread-2 counted till 576683558
[1602432766034] Thread-4 could not buy a ticket. Will try again.
[1602432766036] Thread-4 counted till 1556272683
[1602432766957] Thread-0 could not buy a ticket. Will try again.
[1602432766958] Thread-0 counted till 1502171882
[1602432766979] Thread-3 has bought the ticket
[1602432766980] Thread-2 is buying ticket
[1602432767036] Thread-4 could not buy a ticket. Will try again.
[1602432767037] Thread-4 counted till 653610791
[1602432767959] Thread-0 could not buy a ticket. Will try again.
[1602432767959] Thread-0 counted till 922781680
[1602432768037] Thread-4 could not buy a ticket. Will try again.
[1602432768038] Thread-4 counted till 1612037559
[1602432768960] Thread-0 could not buy a ticket. Will try again.
[1602432768960] Thread-0 counted till 543446309
[1602432768981] Thread-2 has bought the ticket
[1602432768981] Thread-4 is buying ticket
[1602432769961] Thread-0 could not buy a ticket. Will try again.
[1602432769961] Thread-0 counted till 345614347
[1602432770962] Thread-0 could not buy a ticket. Will try again.
[1602432770962] Thread-0 counted till 1405541878
[1602432770982] Thread-4 has bought the ticket
[1602432770982] Thread-0 is buying ticket
[1602432772983] Thread-0 has bought the ticket

Threads get blocked when calling methods like wait(), join() , sleep() etc. Thread.interrupt() can be used to wake up the thread. If a thread is blocked and is interrupted, an exception is thrown. This exception can be caught and the next course of action can be decided.

However if the thread is not blocked interrupting it will only set the interruption flag. Interruption is often used to gracefully stop a thread.

Modern CPUs are multicore and contain processor level caches (l1 , l2 etc). The data is not always read or written to the main memory. So in a multithreaded environment it is possible threads work on dirty data and thus the result is not as expected. To solve this there is a volatile keyword for variables. This guarantees that the value of the variable will always be written to the main memory. Note when using synchronization it automatically guarantees to read the latest data so marking variables volatile is not required.

An atomic operation is a unit of work that is too small to be interrupted.

Eg. incrementing an integer is 3 instructions.

  1. Loading data into a register from memory.
  2. Incrementing the value
  3. Writing back the value to memory

Each of these instructions are atomic operations on their own. Operations like these do not suffer from the same concurrency problem. Classes like AtomicLong, AtomicInterger are based on this idea.

It is sometimes required to wait for a certain condition to be met before some operation. One can run while loop (maybe with sleep). But this will not be the most optimized solution. This is where wait() and notify() comes in.

Firstly, to use wait and notify threads needs to acquire the object lock. Hence synchronization needs to be used. Using synchronization a thread acquires lock on an object. Next it will check some conditions to be met, if not it will call object.wait() method. This will release the object lock and other threads can now access the synchronized methods. Now some other thread can do some changes and call the notify()/notifyAll() methods. Now our first thread will be awakened. It can again acquire the lock again and check if the condition is met. If not it can go to wait again.

Output

Waiting for pizza
Preparing pizza. Please wait 5 seconds.
Pizza ready
Yay! Got the pizza!

notify() vs notifyAll()

Multiple threads can wait() on an object. notify() awakens only one waiting thread while notifyAll() awakens all of them. So depending on the situation one or all of them can be awakened.

The Startup

Get smarter at building your thing. Join The Startup’s +745K followers.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +745K followers.

Rahul Saha

Written by

A passionate software engineer from Kolkata, India. Also a linux enthusiast, photographer and accidental drawing artist.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +745K followers.