Introduction to Java
Basic Concepts of Multithreading

Basic Multithreading in Java

Multithreading is a powerful feature of Java that allows you to run multiple threads concurrently within a single process. Each thread represents a separate flow of control within the program, allowing you to perform multiple tasks simultaneously. Multithreading is commonly used in Java to improve the performance and responsiveness of applications by taking advantage of modern multi-core processors.

In this guide, we will explore the basics of multithreading in Java, including how to create and manage threads, synchronize access to shared resources, and communicate between threads.

Creating Threads

In Java, you can create threads by extending the Thread class or implementing the Runnable interface. Here's an example of creating a thread by extending the Thread class:

class MyThread extends Thread {
    public void run() {
        System.out.println("Hello from a thread!");
    }
}
 
public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
Hello from a thread!

In this example, we define a class MyThread that extends the Thread class and overrides the run method to define the code that the thread will execute. We then create an instance of MyThread and start the thread by calling the start method.

Alternatively, you can create a thread by implementing the Runnable interface. Here's an example of creating a thread using the Runnable interface:

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Hello from a runnable!");
    }
}
 
public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}
Hello from a runnable!

In this example, we define a class MyRunnable that implements the Runnable interface and provides an implementation for the run method. We then create a Thread instance and pass an instance of MyRunnable to its constructor. Finally, we start the thread by calling the start method.

Synchronization

When multiple threads access shared resources concurrently, it's important to synchronize access to prevent data corruption when reading or writing shared data. In Java, you can use the synchronized keyword to create synchronized blocks of code that ensure only one thread can access the block at a time. Here's an example of using synchronization to protect a shared counter:

// Class Counter represents a shared counter
class Counter {
    private int count = 0; // holds the count value
 
    // synchronized method to increment the count
    public synchronized void increment() {
        count++;
    }
 
    // synchronized method to get the count
    public synchronized int getCount() {
        return count;
    }
}
 
public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter(); // create a new counter
 
        // create a new thread that increments the counter 1000 times
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
 
        // create another thread that increments the counter 1000 times
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
 
        thread1.start(); // start the first thread
        thread2.start(); // start the second thread
 
        try {
            thread1.join(); // wait for the first thread to finish
            thread2.join(); // wait for the second thread to finish
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        // print the final count value
        System.out.println("Counter: " + counter.getCount());
    }
}
Counter: 2000

In this example, we define a Counter class with increment and getCount methods that increment and return the counter value, respectively. We mark these methods as synchronized to ensure that only one thread can access them at a time. We then create two threads that increment the counter value concurrently, and we use the join method to wait for the threads to finish before printing the final counter value.

Communication Between Threads

Threads can communicate with each other using methods such as wait, notify, and notifyAll provided by the Object class. These methods allow threads to coordinate their activities and synchronize access to shared resources. Here's an example of using wait and notify to implement a simple producer-consumer scenario:

// Class Buffer represents a buffer for producer-consumer problem
class Buffer {
    private int data; // holds the data in the buffer
    private boolean empty = true; // flag to check if the buffer is empty
 
    // synchronized method to produce data into the buffer
    public synchronized void produce(int value) {
        // wait if the buffer is not empty
        while (!empty) {
            try {
                wait(); // wait for the consumer to consume the data
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
 
        // produce data
        data = value;
        empty = false; // set the buffer as not empty
        notify(); // notify the consumer that data has been produced
    }
 
    // synchronized method to consume data from the buffer
    public synchronized int consume() {
        // wait if the buffer is empty
        while (empty) {
            try {
                wait(); // wait for the producer to produce the data
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
 
        // consume data
        empty = true; // set the buffer as empty
        notify(); // notify the producer that data has been consumed
        return data; // return the consumed data
    }
}
 
public class Main {
    public static void main(String[] args) {
        Buffer buffer = new Buffer(); // create a new buffer
 
        // create a new producer thread
        Thread producer = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                buffer.produce(i); // produce data
                System.out.println("Produced: " + i); // print the produced data
            }
        });
 
        // create a new consumer thread
        Thread consumer = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                int value = buffer.consume(); // consume data
                System.out.println("Consumed: " + value); // print the consumed data
            }
        });
 
        producer.start(); // start the producer thread
        consumer.start(); // start the consumer thread
 
        try {
            producer.join(); // wait for the producer thread to finish
            consumer.join(); // wait for the consumer thread to finish
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
Produced: 0
Consumed: 0
Produced: 1
Consumed: 1
Produced: 2
Consumed: 2
Produced: 3
Consumed: 3
Produced: 4
Consumed: 4
...