Concurrency and Multithreading - Java advanced (OCP)

Every Java program running has at least one thread, “main”, automatically created by the JVM. Each thread is represented by an object of type java.lang.Thread.

Thread.currentThread(); // returns currently running thread

This article is part of a series on advanced Java concepts, also corresponding to topics to study for upgrading from level I certification to level II certification (OCP level). The new Oracle Java certification framework (as from Java 11) only offers one certification covering both levels, so you’ll want to go through the level I (“OCA”) topics as well.

On parallelism and concurrency

Before starting, I’d like to share this definition of concurrency and parallelism I found on the Oracle blog:

The confusion is understandable and certainly isn’t unique to Java. For one, both concurrency and parallelism involve doing multiple things at the same time. For another, threads, which are often employed for both concurrency and parallelism, serve two different roles that are difficult to disentangle, one is more pertinent for parallelism, and the other for concurrency. (…)
Parallelism is the problem of doing a single job — say, inverting a matrix or sorting a list — faster by employing multiple processing units. This is done by breaking up the job into multiple cooperating subtasks, different subsets of which running on a different core. Concurrency, in contrast, is the problem of scheduling some computational resources to a set of largely independent tasks that compete over those resources. The main performance measure we care about when we speak of concurrency isn’t the duration (latency) of any single task, but throughput — the number of tasks we can process per time-unit.

Inside Java (Oracle), “On parallelism and concurrency

Thread

The thread scheduler allocates portions of CPU time to execute thread actions.

return <return>; in the main() or run() method terminates the thread

Threads action order is unpredictable.

Common practice for creating a thread is to create a class that implements Runnable, and override the run() method. Then create a Thread object and start it.

Thread t = new Thread(new myCustomRunnableObject());
t.start(); // schedules the thread but doesn't necessarily run it immediately

Another, less flexible way to create a thread (not recommended for most use cases) is to create a class that extends Thread and to override run(). The Thread class already implements Runnable. In that case, a new instance has to be created for each thread.

Thread t = new myCustomThreadExtension();

The last option, which is just ok for handling a small amount of actions, is to use a lambda expression (as Runnable is a functional interface).

Runnable r = () -> {
 @Override
 public void run() {
   // overriding 
 }
}

Thread t = new Thread(r); // using the anonymous class to instantiate the thread
t.start();

Thread constructors

Thread()
Allocates a new Thread object.
Thread(Runnable target)
Allocates a new Thread object.
Thread(Runnable target, String name)
Allocates a new Thread object.
Thread(String name)
Allocates a new Thread object.
Thread(ThreadGroup group, Runnable target)
Allocates a new Thread object.
Thread(ThreadGroup group, Runnable target, String name)
Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group.
Thread(ThreadGroup group, Runnable target, String name, long stackSize)
Allocates a new Thread object so that it has target as its run object, has the specified name as its name, and belongs to the thread group referred to by group, and has the specified stack size.
Thread(ThreadGroup group, String name)
Allocates a new Thread object.
Oracle Thread doc

Thread states

State is an enum which holds the thread possible states. It’s only possible to go from certain states to certain others, not all states can precede or follow all others.

NEW

The thread instance exists but has not started yet.

RUNNABLE

Thread is executing. It is ready to run, but might not be yet: it is started.

BLOCKED

The thread is waiting for a monitor lock to be released.

WAITING

Thread is waiting for another thread’s signal.

TIME_WAITING

Thread is waiting for a specific amount of time.

TERMINATED

The thread exited either because method run() completed or because an uncaught exception was thrown. This is a final state.


If thread is in WAITING or TIME_WAITING state, InterruptedException should be caught to set the state of the thread to Runnable. Otherwise, calling t.interrupt() wouldn’t do anything. Only a running thread can be interrupted.

The same thread can only be started once. Calling method .start() schedules the thread but doesn’t necessarily run it immediately. Once started, method .run() will be invoked automatically but we don’t know when.

Do not confuse methods run and start. You must invoke start if you’d like to execute your code inside run in another thread. If you invoke run directly, the code will be executed in the same thread.
If you try to start a thread more than once, the start throws IllegalThreadStateException.

JetBrains Academy/Hyperskill

The same Runnable object can be used by several Thread objects.

Method .wait() makes the thread wait until notified (state WAITING). Method .notify() awakes one thread (randomly if several threads are waiting!), .notifyAll() awakes all threads. The method .wait() must be invoked within a synchronized block against the same monitor instance.

try {
  synchronized (monitorInstance) {
    monitorInstance.wait();
  }
} catch (InterruptedException e) {
  // ...
}

Synchronized is a keyword that ensures exclusive access to a block of code.

  1. The thread that enters first remains in runnable state.
  2. All other threads accessing the same block are in blocked state.
  3. Lock is released when first thread exits.
  4. Another thread is allowed to enter the runnable state and place a new lock (random!).

Method .join() blocks the thread until another thread terminates.

Thread properties

Thread have following properties:

  • a name, generated or given;
  • a unique ID or identifier (long);
  • being daemon (won’t prevent JVM from exiting if all user threads are finished) or user (default; if at least one user thread is running, the JVM can’t exit). The method setDaemon(boolean) must be invoked before thread start.
  • being alive or not (being already started and not dead yet);
  • a priority, represented by a range between 1 and 10 and defaulting to 5. With priority, you may attempt to control the CPU time allowed for the thread, BUT the behavior is platform dependent and the order of the threads is random and unknown.
  • a state.

Further thread lifecycle management

Methods .sleep(time) and .join() allow to manage the lifecycle of threads. They both may throw InterruptedException.

Sleep takes a duration in milliseconds as a parameter (as long, so don’t forget to suffix with L if you write a literal value). It’s a static method. When called, the current thread pauses for the specified amount of time. It frees some processor time for other threads of the same program or other processes running on the same computer.

Another signature of the method exists as sleep(long millis, int nanos).

The current thread enters in TIME_WAITING state.

There is another way to make current thread sleep using the class TimeUnit for the java.util.concurrent package:

TimeUnit.HOURS.sleep(2);

Sleep method comes really handy to practice on threads because it allows to simulate async calls without creating a real whole asynchronous process.

Join is an instance method that forces current thread to wait for completion of the thread it was called with. Join method can take a duration parameter (long millis) to avoid blocking the current process for too long or forever.

The current thread enters in WAITING state if no duration was specified, or TIME_WAITING is it was.

Exceptions in threads

If a thread throws an exception that’s not caught, it will terminate. In a single-threaded program (only main thread), it causes the whole program to stop because the JVM terminates the running program when there a no more non-daemon (user) threads left. You can observe it when you make small tests in Java in the main method: if an exception is thrown an not handled (might it be a null pointer, an arithmetic exception…), the execution of the program stops even if not all lines of code were read.

In multi-threaded programs, running won’t be interrupted until all user threads are in terminated state. So a thread can encounter an error and stop, but others will continue to run until they reach the end of their process or raise an exception.

If an uncaught error happens in the main-thread, other user threads will continue running until they’re terminated. In the end thought, the program will stop with exit code 1 (error), which is determined by the exit code of main-thread!

Thread executors

Executors encapsulate 1-N threads into a pool and manage the queue of threads. It’s a high level structure while Thread is quite a low-level one. The executor manages threads creation and execution.

The java.util.concurrent package contains utility class Executors.

Some available classes in the package:

  • FixedThreadPool : create a fixed amount of threads
  • WorkStealingPool : parallelism level, reduce contention, free threads (which have finished their work) can “steal” work from another thread’s queue
  • SingleThread : create an executor for a single thread
  • CachedThreadPool : create a new thread if needed or reuse existing (cached) thread
  • ScheduledThreadPool : schedule with delay or according to periodicity
  • SingleThreadScheduled : self-speaking…
  • UnconfigurableExecutor : create a thread with the config of an existing thread which cannot be changed
ScheduldedExecutorService ses = Executors.newScheduledThreadPool(3);
ExecutorService es = Executors.unconfigurableExecutorService(ses);

Executor service lifecycle:

es.submit(() -> // overriding run); // add a task to the pool, returns a Future object to manage/inspect the task
es.execute(() -> // overriding run); // add a task to the queue, returns void
es.shutdown(); // stop accepting new tasks (not really a shutdown), returns void
es.shutdownNow(); // request cancellation of tasks still running (real shutdown)and returns a List<Runnable> with all tasks that weren't run
es.awaitTermination(30, TimeUnit.SECONDS); // = in 30 seconds, check that all tasks are terminated (returns a boolean)

Callable and Future

Callable is a functional interface that can be used instead of Runnable. The difference is it returns a value. Method is call() instead of run();

Future<Type> result = executorService.submit(callable);
Type value = result.get(10, TimeUnit.SECONDS); // blocks invoking the thread until timeout or returns the value within the Future object is available; may throw TimeoutException if no result is available at timeout
Type value = result.get(); // blocks invoking the thread and returns the value when available 
result.isDone(); // returns true if result is available
result.cancel(true); // try to cancel even if task is executing; task must handle InterruptedException
result.cancel(false); // try to cancel if task is not executing
result.isCancelled();
result.get(); // if cancelled, throws a CancellationException

Future wraps the result, which may not exist yet.

List<Future<Type>> results = executorService.invokeAll(collection of Callable);
Type result = executorService.invokeAny(collection of Callable); // executes one callable randomly and returns result (not a Future!)

Locks

Trying to control the execution order may lead to locks:

  • Starvation: a thread waiting for a resource blocked by another thread
  • Livelock: two indefinite loops in two threads waiting for confirmation from each other
  • Deadlock : two threads blocked forever and waiting for each other

Locks are almost impossible to debug because the bug is not reproducible due to asynchronous, random execution. Also, because CPU management is OS dependent, running the application in different environments would make it even less predictable. Program may also be running indefinitely without nothing happening, without throwing errors.

Writing thread safe code

  • Stack values (as local variables, arguments) are thread safe. Each thread operates on its own stack, no other thread can see it. On the other hand, the heap is shared among all threads.
  • Immutable objects are thread safe.
  • Compiler may choose to cache heap values locally, in which case other threads wouldn’t notice changes. The volatile keyword instructs compiler not to cache the variable value. It would always be read from main memory and changes be applied to the main memory before updating in running thread, so update is available to all. It makes the treatment slightly slower.
  • Non-blocking atomic actions. An atomic action is guaranteed to be performed by a thread without interruption. Only actions performed by a CPU in a single cycle are by default atomic. Variable assignments are atomic, except for long and double (64-bits values may take N-steps to be assigned on a 32-bits platform). Arithmetic operations are not atomic. Instead of using volatile blocks on each operation, with a risk of deadlock (slow treatment), classes from the java.util.concurrent.atomic package can be used. They ensure lock-free, thread-safe behaviors (behave as volatile). Types and operations are available.
AtomicBoolean/Integer/Long/Reference<V> atomic = new .....
atomicInteger.incrementAndGet(); // atomic operation on atomic type
  • Synchronized blocks (intrinsic lock). They enforce an exclusive access to shared objects. The order of execution and object consistency is controlled. I creates a bottleneck in a multithreaded application, slowing it down. Only one thread at a time can work, which results in low scalability.
  • Intrinsic lock automation. Use synchronized collections. For iteration, a synchronized block must be used.
List syncList = Collections.synchronizedList(aRandomList); // usual list actions are available
synchronized(synList) {
  // iteration
}
  • Non-blocking concurrency automation. Each threads get its own copy of a collection. Mutative operations (add, remove…) create a fresh copy of the collection. Access operations( read…) get snapshots from the collection, it doesn’t need a synchronized block for iteration. It suits small collections with lots of read-only operations and few mutations.
List<T> list = new CopyOnWriteList<>(aRandomList); // usual list actions are available
  • Alternative manual lock control (java.util.concurrent.locks).
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
Lock readLock = rwl.readLock();
Lock wl = rwl.writeLock();
rl.lock()
// perform actions; only one thread at a time can read
rl.unlock();

2 thoughts on “Concurrency and Multithreading – Java advanced (OCP)

Leave a Reply

Your email address will not be published. Required fields are marked *

Don’t miss out!

Receive every new article in your mailbox automatically.


Skip to content