Java MultiThreading for Beginners: Basic Concepts and Code Examples

In the world of Java programming, multi-threading is a powerful concept that allows a program to perform multiple tasks simultaneously. This can significantly improve the performance and responsiveness of an application, especially when dealing with time - consuming operations. For beginners, understanding multi - threading in Java can be a bit challenging, but it is an essential skill to master for building efficient and complex applications. In this blog post, we will explore the basic concepts of Java multi - threading, how to use it, common practices, and best practices, along with some code examples to help you get started.

Table of Contents

  1. Fundamental Concepts of Java Multi - Threading
  2. Usage Methods
  3. Common Practices
  4. Best Practices
  5. Code Examples
  6. Conclusion
  7. References

Fundamental Concepts of Java Multi - Threading

Thread

A thread is the smallest unit of execution in a program. In Java, a thread can be thought of as an independent path of execution within a process. Each thread has its own call stack, program counter, and local variables. A Java program can have multiple threads running concurrently, which means that different parts of the program can execute at the same time.

Process

A process is a program in execution. It can contain one or more threads. For example, when you run a Java application, it is a process, and it can have multiple threads working together to perform different tasks.

Concurrency vs. Parallelism

  • Concurrency: Concurrency is the ability of a system to handle multiple tasks at the same time. In a concurrent system, the tasks are interleaved in time, which means that the CPU switches between different tasks very quickly. Java multi - threading provides concurrency by allowing multiple threads to share the CPU resources.
  • Parallelism: Parallelism is the ability of a system to execute multiple tasks simultaneously. This requires multiple CPU cores or processors. While concurrency can be achieved on a single - core CPU, parallelism requires a multi - core CPU.

Thread States

In Java, a thread can be in one of the following states:

  • New: A thread is in the new state when it is created but not yet started.
  • Runnable: A thread is in the runnable state when it has been started and is ready to run. It is waiting for the CPU to allocate time to it.
  • Blocked: A thread is in the blocked state when it is waiting for a monitor lock to enter a synchronized block or method.
  • Waiting: A thread is in the waiting state when it is waiting indefinitely for another thread to perform a particular action.
  • Timed Waiting: A thread is in the timed waiting state when it is waiting for a specified amount of time.
  • Terminated: A thread is in the terminated state when it has completed its execution.

Usage Methods

Extending the Thread Class

The first way to create a thread in Java is by extending the Thread class. You need to override the run() method, which contains the code that will be executed by the thread. Here is the basic syntax:

class MyThread extends Thread {
    @Override
    public void run() {
        // Code to be executed by the thread
        System.out.println("This is a thread created by extending Thread class.");
    }
}

To start the thread, you need to create an instance of the MyThread class and call the start() method:

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

Implementing the Runnable Interface

The second way to create a thread is by implementing the Runnable interface. You need to implement the run() method in the class that implements the Runnable interface. Here is the basic syntax:

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // Code to be executed by the thread
        System.out.println("This is a thread created by implementing Runnable interface.");
    }
}

To start the thread, you need to create an instance of the MyRunnable class and pass it to the Thread class constructor, and then call the start() method:

public class Main {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        Thread thread = new Thread(runnable);
        thread.start();
    }
}

Common Practices

Synchronization

When multiple threads access shared resources, there is a risk of data inconsistency. Synchronization is used to ensure that only one thread can access a shared resource at a time. In Java, you can use the synchronized keyword to achieve synchronization. Here is an example:

class Counter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

In the above example, the increment() method is synchronized, which means that only one thread can call this method at a time.

Thread Joining

The join() method is used to wait for a thread to complete its execution. This is useful when you want to ensure that one thread has finished its task before another thread starts. Here is an example:

class MyThread extends Thread {
    @Override
    public void run() {
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread has finished its execution.");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Main thread can continue now.");
    }
}

Best Practices

Use the ExecutorService Framework

The ExecutorService framework in Java provides a high - level way to manage threads. It allows you to create a pool of threads and submit tasks to them. This is more efficient than creating and managing threads manually. Here is an example:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println("Task is being executed by a thread from the pool.");
    }
}

public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            executor.submit(new MyTask());
        }
        executor.shutdown();
    }
}

Avoid Using stop(), suspend(), and resume()

The stop(), suspend() and resume() methods in the Thread class are deprecated because they can lead to inconsistent states in the program. Instead, use other mechanisms such as flags to control the execution of a thread.

Handle Exceptions Properly

When working with threads, it is important to handle exceptions properly. Unhandled exceptions in a thread can cause the thread to terminate unexpectedly. You can use try - catch blocks in the run() method to handle exceptions.

Code Examples

Example 1: Multiple Threads Printing Messages

class MessagePrinter implements Runnable {
    private String message;

    public MessagePrinter(String message) {
        this.message = message;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(message);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MessagePrinter("Hello"));
        Thread thread2 = new Thread(new MessagePrinter("World"));
        thread1.start();
        thread2.start();
    }
}

Example 2: Using ExecutorService

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class Task implements Runnable {
    private int taskId;

    public Task(int taskId) {
        this.taskId = taskId;
    }

    @Override
    public void run() {
        System.out.println("Task " + taskId + " is being executed.");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Task " + taskId + " has completed.");
    }
}

public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {
            executor.submit(new Task(i));
        }
        executor.shutdown();
    }
}

Conclusion

Java multi - threading is a powerful feature that allows you to write more efficient and responsive applications. By understanding the basic concepts, usage methods, common practices, and best practices, you can start using multi - threading in your Java programs. Remember to handle shared resources carefully, use synchronization when necessary, and follow the best practices to avoid common pitfalls. With practice, you will be able to write complex multi - threaded applications that can take full advantage of modern multi - core CPUs.

References