Concurrency and Multithreading in Java

Concurrency and Multithreading in Java

Table of Contents

Imagine a coffee shop with only one employee, a cashier who is supposed to do all the work alone. Gets an order, makes the coffee, delivers it to the customer, and repeats.. which may take forever without concurrency. 

But having one more employee as a barista would really help, right? The cashier would gather orders and pass them to the barista, so once the barista was preparing an order, the cashier could get other orders, and the customers would not have to wait there all the time. The coffee shop would sell more coffee eventually.

What you see in coffee shops, restaurants is all about concurrency. Split and execute tasks simultaneously in a way that will not affect the outcome. 

In the above example;

  • Process: Coffee Shop
  • Outcome:  Coffee
  • Throughput:  The number of coffee you can make in a period of time
  • Single thread: One person doing all the work in a certain order
  • Multi-thread: There are different people to achieve different tasks such as barista and cashier

What is concurrency?

Concurrency is the ability to execute multiple tasks simultaneously without a certain order and achieve this by providing the same outcome. 

Synchronization

Synchronization is the ability to coordinate access of multiple threads to a shared resource. Synchronization in Java guarantees that only one thread can access a shared resource at any time. 

Process vs. Threads

Processes and threads are two basic execution units; while process refers to a Java Virtual Machine execution instance, the threads are a subset of the process. 

A Java Virtual Machine starts with a single thread by default which is called the main thread. And additional threads can be created by either;

  1. Implementing the Runnable interface
  2. implementing the Callable interface.
  3. Extending the Thread class 

Implementing the Runnable Interface for Concurrent Programming

Oracle Java docs specify the Runnable Interface as “The Runnable interface should be implemented by any class whose instances are intended to be executed by a thread. The class must define a method of no arguments called run.” The Runnable Interface comes from Standart Java.

				
					public class TaskRunnable implements Runnable {
 @Override
 public void run() {
   System.out.println("Thread name: " +Thread.currentThread().getName());
 }
}

				
			

There are various ways that you can assign a Runnable task to a new thread.

Execute a Runnable task by creating new Threads

You can create a new thread from the constructor by passing the Runnable implementation. Please keep in mind that to spawn up a thread in parallel, you need to call the thread’s start method. If you end up calling the run method instead, it will run your Runnable task in the main thread with a certain order (sequentially). 

Java Thread start() Method.
				
					   for (int i = 0; i < 5; i++) {
     Thread thread = new Thread(new TaskRunnable());
     thread.start();
   }
 }
 
// Output 
Thread name: Thread-3
Thread name: Thread-2
Thread name: Thread-4
Thread name: Thread-1
Thread name: Thread-0

				
			
Java Thread run() Method
				
					 for (int i = 0; i < 3; i++) {
     Thread thread = new Thread(new TaskRunnable());
     thread.run();
   }

//Output
Thread name: main
Thread name: main
Thread name: main

				
			

Execute a Runnable task with ExecutorService asynchronously

The ExecutorService feature is released with Java 5 that allows you to run tasks asynchronously. With ExecutorService, you don’t need to create new threads each time but can use the already created threads from the pool.

				
					ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
 executor.submit(new TaskRunnable());
}

//Output
Thread name: pool-1-thread-2
Thread name: pool-1-thread-3
Thread name: pool-1-thread-1
Thread name: pool-1-thread-3
Thread name: pool-1-thread-1

				
			

Execute a Runnable Task with ExecutorService in a Lambda Expression

Runnable is considered a functional interface as it has only one abstract method. This allows us to run a task in lambda expression without bothering to implement the Runnable interface separately. 

				
					ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 3; i++) {
 executor.submit(() -> System.out.println("Thread name: " + Thread.currentThread().getName()));
}

//Output
Thread name: pool-1-thread-3
Thread name: pool-1-thread-1
Thread name: pool-1-thread-2

				
			

Implementing the Callable Interface with ExecutorService

The Callable interface is released with Java 5 within the concurrency package. 

It looks pretty similar to the Runnable interface. But Callable interface uses Java generics that means you can return any type based on your needs and throw exceptions. 

				
					ExecutorService executorService =  Executors.newSingleThreadExecutor();

int input = 20;
Callable<Double> callableTask = () -> Math.log(input);
Double result = executorService.submit(callableTask).get();
System.out.println(result);

//Output
2.995732273553991
				
			

Extending Thread Class to Execute Tasks

Most of the time, extending a raw Thread class is not preferred as it mixes up the duty of running a task with the implementation of the task. Additionally, you will not extend any class even if you require it, which is a lack of inheritance. 

				
					public class TaskThread extends Thread {
 public void run() {
   System.out.println("Thread name: " + Thread.currentThread().getName());
 }
}

for (int i = 0; i < 3; i++) {
 TaskThread taskThread = new TaskThread();
 taskThread.start();
}

//Output
Thread name: Thread-1
Thread name: Thread-2
Thread name: Thread-0

				
			

Why Concurrency?

Concurrent programming maximizes the throughput by using the current resources most efficiently. As a result, it reduces the response time and helps you better utilize the CPU. 

Even though concurrent programming techniques increase the performance, there are some concurrency problems listed below that you should be aware of;

Race Conditions 

A race condition occurs when multiple threads access shared data and write at the same time. As overlapped executions are manipulating the shared object, the outcome can be unpredictable and corrupted.  

Deadlocks

In concurrent programming, a deadlock is when multiple processes share the same resource and wait for each other to take any action or release a lock. 

Resource Starvation

Resource Starvation is a situation where a thread is being denied to gain access to a shared resource for an indefinite period.  

I have several posts about different concepts in Java and other programming languages. You might enjoy some of my other posts ​​5 Top Common Mistakes Every Beginner Java Programmer Makes, How to start coding Java or Canary vs. Blue-Green Deployments published on Exceptionly Blog.

One Response

  1. Pingback: Exceptionly

Leave a Reply

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

en_USEnglish