Multithreading is considered to be one of the more complex topics in computer programming and in Java specifically. To correctly use multithreading in applications requires a high level of skill, knowledge, and experience.
In this article, I introduce multithreading concepts from a developer standpoint and correlate them to Java threading. Developers, software architects and engineers, data scientists, students, and coding professionals – especially the subset using Core Java may use this tutorial to refresh their memory and learn all about the intricacies of multithreading in Java.
Multithreading Concepts in Computer Science
The first thing to understand about multithreading as it relates to Computer Science is what a thread actually is. Threads are a resource-light process that differs from normal processes in a few key ways. The primary features of a thread are:
- Threads create a sense of process executions, either concurrently or in parallel.
- Threads distribute and execute a single task or similar tasks.
- Threads can execute tasks, either on a schedule or asynchronously.
- Threads have better context switching mechanisms than processes.
- Threads share memory space with other threads and, at the same time, can have their own storage.
Below are some of the core concepts of multithreading from a CS perspective:
Critical Section: This is the section of code that gets accessed by multiple threads at the same time – or concurrently – which can also modify the state. Because of the nature of concurrent modification during the sharing of a resource, access control is necessary.
Semaphores: Semaphores are a resource count; they are used to help control access to a resource that is being shared, particularly in multithreaded environments.
Mutex: Mutex is a construct used for synchronization; it limits one thread to use a given resource at a time. It has ownership associated with it.
Monitor: Another construct use for synchronization. Monitors allow mutual exclusion, as well as the ability to wait for a conditional to be true.
Deadlocks: There are situations where, in the critical section of code, more than one resource is accessed by several threads. In these instances, if a single thread needs access to a resource that is shared, it might need to relieve the currently shared resource. This might also be the case for other threads that are executing concurrently with the same resources. When this mutual exclusion is not possible, it results in a Deadlock.
Deadlock Prevention: The techniques used to prevent deadlock conditions.
Deadlock Handling: The detection of deadlocks and their removal.
Reentrancy: Defines the point where a method – or subroutine – can be entered again without the need to complete its prior invocation.
Java: Multithreading Interfaces and Classes
The following are some core concepts and explanations of multithreading interfaces and classes in Java:
Thread: In Java, the thread class is the most important class related to multithreading. Threads can be implemented by extending this class. Its usage is demonstrated later in this article. In production or in development environments, developers may find it is extremely tough to fix issues related to multithreading. Understanding locks may help solve the most confusing and common errors that even experienced Java programmers have trouble wrapping their heads around.
Runnable: Runnables are an alternative way that you can create a thread. You can either create a thread in Java by implementing a Runnable interface or by extending a thread class. In this case, a developer has to override the run() method from this interface to implement logic. (Note: the Thread Class already implements Runnable). To understand multithreading better, try to remember the lock status on shared objects during each of the methods shown below (where applicable).
start(), Lock Status: It can obtain locks. Call this method when scheduling a thread to run. The thread will run once it is scheduled and a CPU cycle is available.
run(), Lock Status: It can obtain locks; It is called – implicitly – by the Thread runtime in order to start the execution of a thread.
yield(), Lock Status: Locks are held; This particular method either yields or transfers control to a different thread that has an equal priority. The thread it transfers control to – or whether it will eve transfer control – is not guaranteed. The sample code below shows how to create a Thread in Java by extending the thread class.
sleep(), Lock Status: Locks are held; Using this method will cause the current thread being executed to pause its execution for a given amount of time. The amount of time the execution is paused can be specified in milliseconds. An interrupted exception will be thrown, which a programmer will need to address.
join(), Lock Status: Locks are held; Using this will lead to an execution mode, causing all threads to join at the end of the current thread. In this scenario, the current thread continues to completion before it switches to a different thread. This also throws an interrupted exception, which a developer will need to address.
suspend(), Lock Status: Locks are held; This has been deprecated.
resume(), Lock Status: Locks are held; This has been deprecated; Both of these threads have been deprecated as they lead to deadlocks and frozen processes.
stop(), Lock Status: Locks are released; This has been deprecated; This particular thread method has been deprecated because it created irregular states due to damaged objects. It is not recommended for use.
Java Object Classes and Multithreading
Java’s Object class inherently contains methods used to control access this object, particularly when shared or in multithreaded applications.
wait(), Lock Status: current object lock released, other locks are held; Using the wait() method will cause the current thread to halt execution and go into to a wait state. This will also release the lock it places on the present object, while retaining all other locks on the other objects.
notify(), Lock Status: Lock is acquired by a random waiting thread; The notify() method will notify a thread which is waiting to obtain a lock on the present shared object.
notifyAll(), Lock Status: Lock is acquired by an arbitrary thread that is waiting to obtain a lock on the current shared object. All threads that are waiting to acquire a lock on the thread will be notified using this method.
Java: Understanding Locks in Multithreading
When you think of locks or monitors in Java, the important thing to remember is that concurrent modification performed by threads should never lead to a damaged object. There is one exception to this rule, however: the wait() and notify() parameters that might cause a change to a resource being shared or to an object prior to swapping or switching control. A damaged object is defined as when an undesired or corrupted change in state has occurred.
synchronized: In Java, the synchronized keyword controls access to the critical section of the code. It is also used in the implementation of thread monitors in Java. You can apply the synchronized keyword to static methods and instance level methods, as well as, blocks. Once a thread enters a block or method that is synchronized, it obtains the lock on that object or class. With regards to static synchronized methods, a single lock is held at the class level; it is different than an instance level lock which is held per instance of the class. The synchronized keyword grants mutual exclusion on a shared resource. You may only call wait(), notify(), and notifyAll() in synchronized blocks or methods. Mutexes are not inherently supported in Java.
For an in-depth study on locks, please check out this guide to Java Concurrent Locks.
Asynchronous Task Execution in Java
In the following section, we look at some tools that were introduced in JDK 5 and JDK 6 that provide more controlled access for asynchronous task execution in Java.
Callable: Similar to Runnable, this interface has instances that might b =e executed by another thread.
Executors: Executors are interfaces used for the creation of a thread pool.
ExecutorService: You can use this asynchronous task executor to send Runnable or Callable tasks for execution. You can then track their status through a Future object.
Future: An object return from task submission to an asynchronous task executor from which a task state could be monitored.
AtomicInteger: In Java, this is a type of Integer object that performs concurrent lock-free updates using hardware instructions.
Case Study for Multithreading in Java
In this example code, we will design a multi-threaded system with a shared resource that can take only two values: 0 or 1. It should have two methods – one for incrementing and one for decrementing – that get called by two threads concurrently. One of the threads can only constantly increment, and the other can only constantly decrement. Their operations should be mutually exclusive.
Editor’s Note: You can read more about Multithreading in Java at our multithreading page.
The solution is a simplified version of the “Producer-Consumer Problem”.
package com.developer.skp.java.multithreading; public class DevDecrementer implements Runnable { DevSharedObject devSharedObject; DevDecrementer(DevSharedObject devSharedObject) { this.devSharedObject=devSharedObject; public void run() { while(true) devSharedObject.decrementerAccess(); } }
package com.developer.skp.java.multithreading; public class DevIncrementer implements Runnable { DevSharedObject devSharedObject; DevIncrementer(DevSharedObject devSharedObject) { this.devSharedObject=devSharedObject; } public void run() { while(true) devSharedObject.incrementerAccess(); } }
package com.developer.skp.java.multithreading; public class DevWorkbench extends Thread { public static void main(String[] args) { DevSharedObject devShared=new DevSharedObject(); Thread devThread01=new Thread(new DevIncrementer(devShared)); devThread01.start(); Thread devThread02=new Thread(new DevDecrementer(devShared)); devThread02.start(); } }
package com.developer.skp.java.multithreading; public class DevSharedObject { // access from within this class only private int x; public synchronized void decrementerAccess() { try { if (x == 1) { x--; notify(); } else { wait(); } } catch (InterruptedException e) { System.out.println("thread interrupted"); } } public synchronized void incrementerAccess() { try { if (x == 0) { x++; notify(); } else { wait(); } } catch (InterruptedException e) { System.out.println("thread interrupted"); } } }
Practice Java Coding Riddle for the Reader
Most of the classes below were introduced in JDK 7. They provide an alternate and, in some ways, easier way of programming multithreaded applications in Java.
Condition: Condition is used to factor out Object monitor methods such as wait(), notify(), and notifyAll() into separate, distinct objects in order to give the effect of having multiple wait-sets per object. This is achieved by combining them with the use of arbitrary lock implementations. Whereas a lock replaces the use of synchronized methods and statements, a Condition replaces the use of the Object monitor methods. Condition is a Java interface.
signal(): Wakes up one waiting thread.
signalAll(): Wakes up all waiting threads.
await(): This will cause the current thread to wait until it is either signaled or interrupted. Implementing lock provides more robust locking operations than you can obtain with synchronized methods and statements. Lock is an interface.
ReentrantLock: A re-entrant mutual exclusion lock with the same basic behavior as an implicit monitor lock. It is a concrete implementation of Lock.
Your task: design an asynchronous task executor modeled as a thread pool executor.
Hint for Solution: It is a simplified version of the “Producer-Consumer Problem”.
Happy Multithreading in Java!
Sumith Puri is a Principal Java/Java EE Architect, a Hard-Core Java // Jakarta EE Developer with 16 (and counting) years of experience. He’s a Senior Member of ACM and IEEE, DZone Core, Member, CSI*; DZone MVB, and Java Code Geek. He holds a Bachelor of Engineering (Information Science & Engineering) from Sri Revana Siddeshwara Institute of Technology, completed the Executive Program in Data Mining & Analytics at the Indian Institute of Technology, and the Executive Certificate Program in Entrepreneurship at the Indian Institute of Management. His experience includes SCJP 1.4, SCJP 5.0, SCBCD 1.3, SCBCD 5.0, BB Spring 2.x*, BB Hibernate 3.x*, BB Java EE 6.x*, Quest C, Quest C++ and Quest Data Structures.