In this blog post, we’ll look in-depth into the world of threads, learning what they are, how they work, and why they’re crucial in Java programming. Whether you’re a beginner just starting with Java or an experienced developer looking to improve your skills, this post will provide the foundational knowledge you need to understand threads and use them effectively in your projects.
Watch our YouTube video to boost your Java abilities and begin coding like a pro!
Introduction to Threads in Java
In Java, a thread is a lightweight sub-process allowing concurrent execution of two or more program parts. Each thread has its call stack but shares the same memory space as the other threads in the process.
This enables threads to efficiently share data and resources without the need for costly inter-process communication.
Threads are implemented in Java using the Thread class and the Runnable interface. The thread class provides methods for creating, initiating, stopping, and controlling threads, whereas the Runnable interface defines the run() function, which contains thread code.
Java Multithreading is a powerful feature that allows developers to create programs that can handle several tasks at once, improving performance and responsiveness. However, careful planning and management are required to avoid issues such as race conditions, deadlocks, and resource scarcity.
Life Cycle of a Thread in Java
Understanding a thread’s life cycle is essential for proper thread management and control in Java. A thread’s life cycle consists of various phases, and a thread can move between these states throughout execution. The following are the major states of the thread life cycle in Java
- New – A thread is in the new state when it is created but has not yet begun.
- Runnable – A thread enters the runnable state when it is begun. The thread is ready to run but may not be executed at this time.
- Running – When the thread executes, it is in the running state.
- Blocked – When a thread is blocked, it is waiting for a monitor lock to be released or an input/output operation to complete.
- Timed Waiting – When a thread waits for another thread to perform a specific action, it enters the timed waiting state.
- Terminated – When a thread completes its execution or is terminated unexpectedly, it enters the terminated state.
Creating a Thread in Java
There are two ways to create threads in Java :
- Extending the Thread Class – To make a thread by extending the Thread class, follow the below-mentioned processes:
- Make a new class by extending the Thread class.
- Replace the code in the run() method with the code you want the thread to run.
- Make a new class object and invoke the start() function on it.
Let us look at an example to understand how to create a thread in Java.
We will create a new category called ‘MyThread’ that will extend the old ‘Thread’ category and then utilize the ‘run()’ function to send a message to the console. Once the initial task is complete, we will begin a new implementation of ‘MyThread’ within the ‘main()’ function and call the ‘start()’ function to start the thread.
class MyThread extends Thread {
public void run() {
System.out.println("MyThread is running");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
}
}
When the ‘start()’ function is called, the ‘run()’ method of the ‘MyThread’ class is executed on a separate thread. The output of the program will be
Output –
MyThread is running
Get 100% Hike!
Master Most in Demand Skills Now!
- Implementing the Runnable Interface – When dealing with tasks for a Java thread, the ‘Runnable’ interface is required. To accomplish this, the following procedures must be followed:
First, create a class that properly implements the ‘Runnable’ interface. Remember, this interface has only one function that the class must implement: ‘run()’. The ‘run()’ method implementation must include code the user wants to run on a different thread.
After that, create an instance of the class and assign it to a ‘Thread’ object. Then, thread by invoking the ‘start()’ function on the ‘Thread’ object.
Let’s look at an example to understand it better.
We’ll create a ‘MyTask’ category that implements the ‘Runnable’ interface. Within this category, the ‘run()’ function must display a message indicating which thread executes the code.
In the ‘main()’ function, we will create an instance of ‘MyTask’ and pass it to a ‘Thread’ entity. We will start the thread with the ‘start()’ function on the ‘Thread’ item. This will execute the ‘run()’ method of ‘MyTask’ in a separate thread.
public class MyTask implements Runnable {
public void run() {
// Code to be executed by the thread
for (int i = 0; i < 10; i++) {
System.out.println("Hello from thread " + Thread.currentThread().getId() + " - " + I);
}
}
}
public class Main {
public static void main(String[] args) {
MyTask task = new MyTask();
Thread thread = new Thread(task);
thread.start();
}
}
Thread Methods in Java
Thread class includes numerous thread management techniques. Some of the most prevalent ways are as follows:
- public void start() – This method starts the execution of a thread.
- public void yield() – This method causes the thread to yield the CPU to other threads of the same priority.
- public void sleep(long milliseconds) – This method causes the thread to sleep for a specified number of milliseconds.
- public void interrupt() – This method interrupts a waiting or sleeping thread.
- public void join() – This method waits for a thread to terminate before continuing the execution of the current thread.
Thread Priority in Java
We can assign priorities to threads to indicate which thread is more important. The thread scheduler employs priorities to determine the order in which threads execute. Lower-priority threads are executed first. The default priority of a thread is 5 (normal priority). The range is from 1 (lowest) to 10 (highest). We can set and get thread priorities in Java using the setPriority() and getPriority() methods.
It is important to remember that the exact behavior of thread priority may differ between platforms and operating systems. As a result, there needs to be more than just thread priorities for program execution.
Thread t1 = new Thread();
t1.setPriority(7); // Set priority to 7
t1.getPriority(); // Returns 7
Thread Synchronization in Java
Thread synchronization is essential when many threads interact with shared resources to guarantee that they do not interfere with each other’s execution. Java uses synchronized blocks and methods to perform synchronization. Use the synchronized keyword to synchronize access to shared resources. A monitor lock is obtained when a thread enters a synchronized block or function, which stops other threads from accessing the same resource until the lock is surrendered.
Thread synchronization mechanisms in Java include the synchronized keyword, locks, and atomic variables.
Synchronized Keyword – In Java, the keyword synchronized refers to a block of code or a method that may only be accessed by one thread at a time. This is known as synchronization. When a thread enters a synchronized block, it receives a lock on the related object. Other threads cannot enter the same block until the lock is released. There are two ways to utilize the synchronized keyword.
- Synchronized method – We can also use the synchronized keyword to synchronize an entire method, like in this case.
public synchronized void myMethod() {
// Synchronized method
}
Only one thread can execute the’myMethod()’ method at a time in this example.
- Synchronized block –To synchronize a piece of code, we can use the synchronized keyword, as seen below
synchronized (object) {
// Synchronized code block
}
The ‘object’ in this example is the object that will be used as the lock. Regardless of how many threads are ready to execute the code block, only one can do so now.
Locks – Locks in Java allow only one thread to access a shared resource at a time. They can be implemented using the ‘Lock’ interface and its implementations, such as ‘ReentrantLock’. In multi-threaded programs, locks provide thread safety and synchronization.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class SharedResource {
private Lock lock = new ReentrantLock();
public void method() {
lock.lock();
try {
// Code to be executed
} finally {
lock.unlock();
}
}
}
Atomic Variables – Atomic variables in Java are variables that can be read and written atomically without the requirement for locking. They are built with classes like ‘AtomicInteger,’ ‘AtomicLong,’ and ‘AtomicBoolean,’ among others. In multi-threaded systems, atomic variables ensure thread safety and synchronization. They provide methods like ‘get()’, ‘set()’, ‘incrementAndGet()’, and ‘compareAndSet()’, among others, that can be used to perform atomic actions without the need for explicit locking. This makes them beneficial for improving the performance of multi-threaded programs.
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
Multithreading in Java
Multithreading in Java refers to the concurrent execution of multiple threads within a Java program. Threads are lightweight subprocesses that allow a program to perform multiple tasks simultaneously, taking advantage of the available CPU cores and improving the overall performance of the application.
Let’s look at a brief example of multithreading in Java using the Runnable interface:
public class MultithreadingExample {
public static void main(String[] args) {
// Create two Runnable instances
MyRunnableTask task1 = new MyRunnableTask("Task 1");
MyRunnableTask task2 = new MyRunnableTask("Task 2");
// Create two threads and pass the Runnable instances to them
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
// Start the threads
thread1.start();
thread2.start();
}
}
class MyRunnableTask implements Runnable {
private String taskName;
public MyRunnableTask(String taskName) {
this.taskName = taskName;
}
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println(taskName + " - Step " + I);
try {
Thread.sleep(1000); // Pause the thread for 1 second
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
In this example, we create a simple multithreading scenario with two tasks represented by the MyRunnableTask class, each printing a sequence of steps. The run() method in the MyRunnableTask class defines the code to be executed concurrently by the threads.
When you run this program, you’ll observe that both threads execute their tasks concurrently. They’ll print their respective steps one after another, and the pause introduced by Thread.sleep(1000) gives the illusion of parallel execution.
Connecting JDBC to Thread
Connecting JDBC (Java Database Connectivity) to threads involves using multithreading to execute database operations concurrently. The primary purpose of this approach is to improve the overall performance of the application, especially in scenarios where multiple database operations need to be executed simultaneously.
Here’s an example of how to use JDBC with threads in Java:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JdbcThreadExample {
private static final String DB_URL = "jdbc:mysql://localhost:3306/mydatabase";
private static final String DB_USER = "username";
private static final String DB_PASSWORD = "password";
public static void main(String[] args) {
// Create multiple threads to execute database operations concurrently
Thread thread1 = new Thread(new DatabaseOperationTask("SELECT * FROM employees"));
Thread thread2 = new Thread(new DatabaseOperationTask("SELECT * FROM orders"));
// Start the threads
thread1.start();
thread2.start();
}
static class DatabaseOperationTask implements Runnable {
private final String sqlQuery;
public DatabaseOperationTask(String sqlQuery) {
this.sqlQuery = sqlQuery;
}
@Override
public void run() {
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement stmt = conn.prepareStatement(sqlQuery);
ResultSet rs = stmt.executeQuery()) {
// Process the result set or perform other database operations
while (rs.next()) {
// Do something with the data String columnValue = rs.getString("column_name");
System.out.println("Thread " + Thread.currentThread().getId() + ": " + columnValue);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
In this example, we have a DatabaseOperationTask class that implements the Runnable interface. The constructor of this class takes an SQL query as a parameter. Each thread will have its own instance of the DatabaseOperationTask, executing a different SQL query.
When the threads are started, each one establishes its database connection, prepares the SQL statement, and executes the query. Since each thread has its own connection and resources, they can perform their database operations concurrently, thus potentially improving the overall performance of the application.
Best Practices for Thread Management
Thread management is critical for designing scalable and efficient Java programs. The following are some best practices regarding thread management
- Avoid creating too many threads, as it can lead to performance degradation.
- Use thread priority with caution, as it does not ensure thread execution order.
- Use thread pools to manage threads efficiently.
- Use thread synchronization to prevent race conditions and thread interference.
- Always release monitor locks as soon as they are no longer needed.
Conclusion
Threads allow you to run several tasks in a program at the same time. In Java, you may create threads by extending the Thread class or implementing the Runnable interface. Threads go through many stages during their life cycle. To construct a strong multithreaded program in Java, you may additionally set thread priorities, and employ synchronization with numerous threads methods.