Concurrency in Java

Concurrency is a fundamental concept in Java that enables multiple tasks to run in parallel. Java provides a robust multithreading model that helps in improving application performance, responsiveness, and efficient resource utilization. This article explores different ways to create and manage threads, synchronization techniques, and inter-thread communication.

Creating Threads in Java

There are three primary ways to create threads in Java:

Extending the Thread Class

The simplest way to create a thread is by extending the Thread class and overriding the run method.

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Thread running: " + i);
        }
    }
}

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

Implementing the Runnable Interface

Another approach is to implement the Runnable interface and pass it to a Thread object.

class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("Runnable thread running: " + i);
        }
    }
}

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

Using Lambda Expressions

With Java 8 and later, we can use lambda expressions to define a Runnable more concisely.

public class LambdaThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Lambda thread running: " + i);
            }
        });
        thread.start();
    }
}

Starting Threads with .start()

The start() method begins execution of a new thread.

public class StartThreadExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> System.out.println("Thread started"));
        thread.start();
    }
}

Pausing Execution with Thread.sleep(long millis)

We can pause a thread using Thread.sleep().

public class SleepExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                for (int i = 0; i < 5; i++) {
                    System.out.println("Thread sleeping: " + i);
                    Thread.sleep(1000);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread.start();
    }
}

Observing Thread Execution with the Supervisor Pattern

A supervisor thread can monitor other threads and take action when needed.

public class SupervisorPattern {
    public static void main(String[] args) {
        Thread worker = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("Worker thread running: " + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    System.out.println("Worker interrupted");
                }
            }
        });
        worker.start();

        Thread supervisor = new Thread(() -> {
            while (worker.isAlive()) {
                System.out.println("Supervisor: Worker is still running...");
                try {
                    Thread.sleep(700);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Supervisor: Worker has finished.");
        });
        supervisor.start();
    }
}

Blocking on Thread Completion with .join()

The join() method makes one thread wait for another to complete.

public class JoinExample {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(2000);
                System.out.println("Worker thread finished");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread.start();

        try {
            thread.join();
            System.out.println("Main thread continues after worker thread");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Synchronizing Access to Shared Resources

Using synchronized prevents race conditions when multiple threads access shared data.

class SharedResource {
    private int count = 0;

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

    public synchronized int getCount() {
        return count;
    }
}

public class SynchronizedExample {
    public static void main(String[] args) {
        SharedResource resource = new SharedResource();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                resource.increment();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                resource.increment();
            }
        });
        thread1.start();
        thread2.start();

        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final count: " + resource.getCount());
    }
}

Communicating Between Threads with .wait() and .notify()

Threads can communicate using wait() and notify().

class SharedQueue {
    private boolean dataAvailable = false;

    public synchronized void produce() {
        dataAvailable = true;
        notify();
    }

    public synchronized void consume() {
        while (!dataAvailable) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("Data consumed");
    }
}

public class WaitNotifyExample {
    public static void main(String[] args) {
        SharedQueue queue = new SharedQueue();
        Thread producer = new Thread(queue::produce);
        Thread consumer = new Thread(queue::consume);
        consumer.start();
        producer.start();
    }
}

Conclusion

Concurrency in Java is powerful but requires careful management to avoid race conditions, deadlocks, and inefficiencies. By understanding thread creation, synchronization, and communication, developers can build efficient and responsive applications.

0
Subscribe to my newsletter

Read articles from Ali Rıza Şahin directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ali Rıza Şahin
Ali Rıza Şahin

Product-oriented Software Engineer with a solid understanding of web programming fundamentals and software development methodologies such as agile and scrum.