From Theory to Practice: Concurrency in Java MultiThreading

Concurrency is a fundamental concept in modern programming, especially when dealing with applications that require high performance and responsiveness. Java, being one of the most popular programming languages, provides a rich set of tools and libraries for implementing concurrency through multi - threading. This blog aims to take you from the theoretical understanding of concurrency in Java multi - threading to practical implementation, covering basic concepts, usage methods, common practices, and best practices.

Table of Contents

  1. Fundamental Concepts of Concurrency in Java MultiThreading
  2. Usage Methods
  3. Common Practices
  4. Best Practices
  5. Conclusion
  6. References

Fundamental Concepts of Concurrency in Java MultiThreading

Thread and Process

  • Process: A process is an instance of a program in execution. Each process has its own memory space, system resources, and a unique process ID. For example, when you open a web browser, a new process is created.
  • Thread: A thread is a lightweight sub - process within a process. Multiple threads can exist within a single process and share the same memory space. Threads are more lightweight than processes and are used to perform concurrent tasks within a program.

Concurrency vs. Parallelism

  • Concurrency: Concurrency is the ability of a system to handle multiple tasks in an overlapping manner. It doesn’t necessarily mean that tasks are executed simultaneously. For example, a single - core CPU can achieve concurrency by rapidly switching between different tasks.
  • Parallelism: Parallelism is the simultaneous execution of multiple tasks. It requires multiple processing units (such as multi - core CPUs) to execute tasks at the same time.

Race Conditions and Synchronization

  • Race Condition: A race condition occurs when multiple threads access and modify shared resources concurrently, and the final outcome depends on the relative timing of their execution. This can lead to inconsistent or unpredictable results.
  • Synchronization: Synchronization is a mechanism used to control access to shared resources by multiple threads. It ensures that only one thread can access the shared resource at a time, thus preventing race conditions.

Usage Methods

Creating Threads in Java

There are two main ways to create threads in Java:

  1. Extending the Thread class:
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread is running");
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}
  1. Implementing the Runnable interface:
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Runnable is running");
    }
}

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

Thread States and Lifecycle

A thread in Java can be in one of the following states:

  • New: A newly created thread that has not yet started.
  • Runnable: A thread that is ready to run and is waiting for the CPU to be allocated.
  • Running: A thread that is currently being executed by the CPU.
  • Blocked: A thread that is waiting for a monitor lock or some other resource.
  • Waiting: A thread that is waiting indefinitely for another thread to perform a particular action.
  • Timed Waiting: A thread that is waiting for a specified amount of time.
  • Terminated: A thread that has completed its execution.

Synchronization Mechanisms

  • synchronized keyword:
class Counter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Count: " + counter.getCount());
    }
}

Common Practices

Producer - Consumer Pattern

The producer - consumer pattern is a classic concurrency pattern where one or more producer threads produce data and put it into a shared buffer, and one or more consumer threads consume the data from the buffer.

import java.util.LinkedList;
import java.util.Queue;

class ProducerConsumerExample {
    private static final Queue<Integer> buffer = new LinkedList<>();
    private static final int MAX_SIZE = 5;

    static class Producer implements Runnable {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    synchronized (buffer) {
                        while (buffer.size() == MAX_SIZE) {
                            buffer.wait();
                        }
                        buffer.add(i);
                        System.out.println("Produced: " + i);
                        buffer.notifyAll();
                    }
                    Thread.sleep(100);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    static class Consumer implements Runnable {
        @Override
        public void run() {
            try {
                for (int i = 0; i < 10; i++) {
                    synchronized (buffer) {
                        while (buffer.isEmpty()) {
                            buffer.wait();
                        }
                        int item = buffer.poll();
                        System.out.println("Consumed: " + item);
                        buffer.notifyAll();
                    }
                    Thread.sleep(200);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        Thread producerThread = new Thread(new Producer());
        Thread consumerThread = new Thread(new Consumer());

        producerThread.start();
        consumerThread.start();
    }
}

Thread Pooling

Thread pooling is a technique where a fixed number of threads are created in advance and are reused to execute multiple tasks. Java provides the ExecutorService interface and its implementations for thread pooling.

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

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(3);

        for (int i = 0; i < 10; i++) {
            final int taskId = i;
            executor.submit(() -> {
                System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        executor.shutdown();
    }
}

Best Practices

Avoiding Deadlocks

  • Deadlock: A deadlock occurs when two or more threads are blocked forever, each waiting for the other to release a resource.
  • Avoidance Strategies:
    • Lock Ordering: Always acquire locks in the same order in all threads.
    • Use Timeouts: Use methods that allow you to specify a timeout when acquiring locks.

Using High - Level Concurrency Utilities

Java provides many high - level concurrency utilities in the java.util.concurrent package, such as ConcurrentHashMap, CountDownLatch, CyclicBarrier, etc. These utilities are more robust and easier to use than low - level synchronization mechanisms.

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("key1", 1);
        map.put("key2", 2);

        System.out.println(map.get("key1"));
    }
}

Conclusion

Concurrency in Java multi - threading is a powerful but complex topic. By understanding the fundamental concepts, usage methods, common practices, and best practices, you can write more efficient and reliable concurrent programs. Remember to always be aware of race conditions, deadlocks, and other concurrency issues, and use the appropriate synchronization mechanisms and high - level concurrency utilities provided by Java.

References