Skip to main content

Introduction to Multi-threading in Java

·1606 words·8 mins

The article explores multi-threading concepts in to Java.

Multithreading means that you have multiple threads of execution inside the same application. A thread is like a separate CPU executing your application. Thus, a multithreaded application is like an application that has multiple CPUs executing different parts of the code at the same time. A thread is not equal to a CPU though. Usually a single CPU will share its execution time among multiple threads, switching between executing each of the threads for a given amount of time. It is also possible to have the threads of an application be executed by different CPUs.

Concurrency means that an application is making progress on more than one task at the same time or at least seemingly at the same time (concurrently).

Parallel execution is when a computer has more than one CPU or CPU core, and makes progress on more than one task simultaneously. However, parallel execution is not referring to the same phenomenon as parallelism.

The term parallelism means that an application splits its tasks up into smaller subtasks which can be processed in parallel, for instance on multiple CPUs at the exact same time.

A race condition is a concurrency problem that may occur inside a critical section. A critical section is a section of code that is executed by multiple threads and where the sequence of execution for the threads makes a difference in the result of the concurrent execution of the critical section.

Race conditions can occur when two or more threads read and write the same variable according to one of these two patterns:

  • Read-modify-write
  • Check-then-act

The read-modify-write pattern means, that two or more threads first read a given variable, then modify its value and write it back to the variable.

The check-then-act pattern means, that two or more threads check a given condition, for instance if a Map contains a given value, and then go on to act based on that information

Thread
#

A Java Thread is like a virtual CPU that can execute your Java code.

Two ways to create threads

  1. Create a subclass of Thread and override the run() method. Two ways to implement Thread subclass:

    1. extends Thread class
    public class ThreadExample {
    	public static void main(String[] args) {
    		MyThread threadExample = new MyThread();
    		threadExample.start();
    		System.out.println("run by main thread. thread name: " + Thread.currentThread().getName());
    	}
    }
    
    class MyThread extends Thread {
    	public void run() {
    		System.out.println("class extends thread example. thread name: " + getName());
    	}
    }
    

    Output

    class extends thread example. thread name: Thread-0
    run by main thread. thread name: main
    
    1. Anonymous Thread class
    public class AnonymousThreadExample {
    	public static void main(String[] args) {
    		Thread thread = new Thread() {
    			public void run() {
    				System.out.println("anonymous thread example. thread name: " + getName());
    			}
    		};
    		thread.start();
    		System.out.println("run by main thread. name: " + Thread.currentThread().getName());
    	}
    
    }
    

    Output

    run by main thread. name: main
    anonymous thread example. thread name: Thread-0
    
  2. Pass an object that implements Runnable (java.lang.Runnable, method run()) to the Thread constructor. Three ways to implement Runnable:

    1. Create a Java class that implements the Runnable interface.
    public class RunnableThreadExampl {
    	public static void main(String[] args) {
    		Runnable runnable = new RunnableThread();
    
    		Thread thread = new Thread(runnable);
    		thread.start();
    		System.out.println("run by main thread. name: " + Thread.currentThread().getName());
    	}
    }
    
    class RunnableThread implements Runnable {
    	@Override
    	public void run() {
    		System.out.println("class implements runnable. thread name: " + Thread.currentThread().getName());
    	}
    }
    

    Output

    class implements runnable. thread name: Thread-0
    run by main thread. name: main
    
    1. Create an anonymous class that implements the Runnable interface.
    public class AnonymousRunnableThreadExample {
    	public static void main(String[] args) {
    		Runnable myRunnable = new Runnable() {
    			public void run() {
    				System.out
    						.println("anonymous runnable thread example. thread name: " + Thread.currentThread().getName());
    			}
    		};
    
    		Thread thread = new Thread(myRunnable);
    		thread.start();
    		System.out.println("run by main thread. name: " + Thread.currentThread().getName());
    	}
    }
    

    Output

    run by main thread. name: main
    anonymous runnable thread example. thread name: Thread-0
    
    1. Create a Java Lambda that implements the Runnable interface.
    public class LambdaRunnableThreadExample {
    	public static void main(String[] args) {
    		Runnable runnable = () -> System.out
    				.println("lambda runnable thread example. thread name: " + Thread.currentThread().getName());
    		Thread thread = new Thread(runnable);
    		thread.start();
    		System.out.println("run by main thread. name: " + Thread.currentThread().getName());
    	}
    }
    

    Output

    run by main thread. name: main
    lambda runnable thread example. thread name: Thread-0
    

When creating and starting a thread a common mistake is to call the run() method of the Thread instead of start(). Always start a thread with start().

  • The Thread.currentThread() method returns a reference to the Thread instance executing currentThread() .
  • The JVM and/or operating system determines the order in which the threads are executed. This order does not have to be the same order in which they were started.
  • (Pause a Thread) A thread can pause itself by calling the static method Thread.sleep() . The sleep() takes a number of milliseconds as parameter.

A daemon thread in Java is a thread that does not keep the Java Virtual Machine (JVM) running if the main thread exits the application. A non-daemon thread will keep the JVM running even if the main thread exits the application.

  • You tell a Thread to be a daemon thread via its setDaemon() method.

Virtual Thread
#

Java virtual threads are different from the original platform threads in that virtual threads are much more lightweight in terms of how many resources (RAM) they demand from the system to run. Java virtual threads are executed by platform threads. A platform thread can only execute one virtual thread at a time. While the virtual thread is being executed by a platform thread - the virtual thread is said to be mounted to that thread.

A platform thread is implemented as a thin wrapper around an operating system (OS) thread. A platform thread runs Java code on its underlying OS thread, and the platform thread captures its OS thread for the platform thread’s entire lifetime. This platform thread is called a carrier.

There is no time slicing happening between virtual threads. In other words, the platform thread does not switch between executing multiple virtual threads, except in the case of blocking network calls. As long as a virtual thread is running code and is not blocked waiting for a network response, the platform thread will keep executing the same virtual thread.

public class VirtualThreadsExample {
	public static void main(String[] args) {
		Runnable runnable = () -> System.out
				.println("virtual thread example. thread name: " + Thread.currentThread().getName());

		Thread vThread = Thread.ofVirtual().name("this-is-virtual-thread").start(runnable);
		try {
			vThread.join();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		System.out.println("run by main thread. name: " + Thread.currentThread().getName());
	}
}

Output

virtual thread example. thread name: this-is-virtual-thread
run by main thread. name: main

Executor Service
#

The Java ExecutorService interface, java.util.concurrent.ExecutorService, represents an asynchronous execution mechanism which is capable of executing tasks concurrently in the background

The implementation of the ExecutorService interface present in the java.util.concurrent package is a thread pool implementation.

Since ExecutorService is an interface, you need to its implementations in order to make any use of it. The ExecutorService has the following implementation in the java.util.concurrent package:

  • ThreadPoolExecutor
  • ScheduledThreadPoolExecutor

An Executor is an interface in Java that provides a way of decoupling task submission from the mechanics of how each task will be run. Instead of manually creating and managing threads, you can delegate tasks to an Executor, which handles the details of executing tasks in the background.

  • Task Submission Simplified: Instead of manually starting threads, you submit tasks (usually in the form of Runnable or Callable) to the Executor and let it decide when to execute them.

  • Thread Management: Executors handle the creation, reuse, and management of threads internally, allowing you to focus on the logic of your tasks.

  • The Runnable interface is very similar to the Callable interface. The Runnable interface represents a task that can be executed concurrently by a thread or an ExecutorService.

  • The Callable can only be executed by an ExecutorService.

The java.util.concurrent.ThreadPoolExecutor is an implementation of the ExecutorService interface. The ThreadPoolExecutor executes the given task (Callable or Runnable) using one of its internally pooled threads.

The java.util.concurrent.ScheduledExecutorService is an ExecutorService which can schedule tasks to run after a delay, or to execute repeatedly with a fixed interval of time in between each execution. Tasks are executed asynchronously by a worker thread, and not by the thread handing the task to the ScheduledExecutorService.

Executor Shutdown
#

If your application is started via a main() method and your main thread exits your application, the application will keep running if you have an active ExexutorService in your application. The active threads inside this ExecutorService prevents the JVM from shutting down.

Synchronized Keyword
#

A piece of logic marked with synchronized becomes a synchronized block, allowing only one thread to execute at any given time.

We can use the synchronized keyword on different levels:

  • Instance methods
  • Static methods
  • Code blocks

Volatile Keyword
#

To ensure that updates to variables propagate predictably to other threads, we should apply the volatile modifier to those variables. The synchronized methods and blocks provide both Mutual Exclusion and Visibility (changes made by one thread to the shared data are visible to other threads to maintain data consistency) properties at the cost of application performance. The volatile field is quite a useful mechanism because it can help ensure the visibility aspect of the data change without providing mutual exclusion.

A shared variable that includes the volatile modifier guarantees that all threads see a consistent value for the shared variable. Any update to a volatile field updates the shared value of the field immediately. In other words, a different thread cannot get an inconsistent value of the shared variable after its value is updated.

References
#

Vaibhav
Author
Vaibhav
Full Stack Developer