Skip to main content

Command Palette

Search for a command to run...

Threads and Concurrency

Published
•4 min read•View as Markdown
Threads and Concurrency
E

I am a passionate software engineer in training at ALX. I have a strong preference for back-end development. I am on course to share my learning journey with you.

Introduction

In the previous lesson, we explored File Handling in Java, which allows reading and writing data efficiently. Now, we move on to Threads and Concurrency, which enables multitasking and efficient CPU utilization in Java programs.

Creating Threads using Runnable & Thread Class

Java provides two ways to create threads:

  1. By implementing the Runnable interface

  2. By extending the Thread class

Using Runnable Interface

class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Thread running: " + Thread.currentThread().getName());
    }

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

/**
* Output
* Thread running: Thread-0
*/

Extending the Thread Class

class MyThread extends Thread {
    public void run() {
        System.out.println("Thread running: " + Thread.currentThread().getName());
    }

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

/**
* Output
* Thread running: Thread-0
*/

Synchronization Basics

When multiple threads access shared resources, data inconsistency can occur. Synchronization ensures that only one thread accesses the shared resource at a time.

Example Without Synchronization (Data Corruption)

class Counter {
    private int count = 0;

    public void increment() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

class TestConcurrency {
    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("Final count: " + counter.getCount());
    }
}

From the above code, multiple threads modify the count variable at the same time. This can cause data corruption, because count++ is not atomic (it breaks down to: read → increment → write). Each thread calls the increment() 1000 times without synchronization, race conditions may occur.

Example With Synchronization (Thread-Safe)

class SynchronizedCounter {
    private int count = 0;

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

    public int getCount() {
        return count;
    }
}

class TestSynchronization {
    public static void main(String[] args) throws InterruptedException {
        SynchronizedCounter counter = new SynchronizedCounter();

        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("Final count: " + counter.getCount());
    }
}

In this version, increment() is marked as synchronized, ensuring that only one thread can access it at a time. This guarantees thread safety, even when multiple threads update the same variable. So even if t1 and t2 run simultaneously, the updates to count are done one at a time, safely.

Best Practices for Thread Safety

  • Use synchronized blocks or methods to prevent data corruption.

  • Prefer using ReentrantLock over synchronized for more flexibility.

  • Use volatile keyword for visibility of shared variables.

  • Leverage concurrent utilities like ExecutorService for thread management.

Concurrency

Concurrency is the ability of a program to run multiple tasks at the same time, often by interleaving execution across threads. In Java, concurrency is essential for:

  • Making applications faster by utilizing multiple CPU cores.

  • Keeping Graphical User Interfaces responsive.

  • Managing multiple users or tasks in parallel (like in web servers).

Common Concurrency Problems

  1. Race Conditions: Two or more threads access shared data and modify it simultaneously, causing unpredictable results.

  2. Data Corruption: This happens when shared variables are updated without proper synchronization.

  3. Deadlock: Two threads wait on each other to release a lock, resulting in a standstill.

Tools to Manage Concurrency in Java

  1. Use the synchronized keyword which prevents multiple threads from accessing a method or block at the same time.

     public synchronized void increment() {
         count++;
     }
    
  1. Locks (ReentrantLock). It is more flexible than synchronized, with try-locks, timeouts, etc.

     Lock lock = new ReentrantLock();
     lock.lock();
     try {
         // critical section
     } finally {
         lock.unlock();
     }
    
  2. Volatile variables ensures changes to a variable are visible across threads.

     private volatile boolean running = true;
    
  3. Atomic Variables (from java.util.concurrent.atomic) for thread-safe operations without locking:

     AtomicInteger count = new AtomicInteger(0);
     count.incrementAndGet();
    
  4. Thread Pools & Executors manage multiple threads efficiently with:

     ExecutorService executor = Executors.newFixedThreadPool(4);
     executor.submit(() -> doWork());
    

Conclusion

Threads and concurrency help in executing multiple tasks simultaneously, improving performance. Understanding how to create threads using the Runnable interface and Thread class, along with implementing synchronization, ensures efficient and thread-safe Java applications.

Stay tuned for the next topic in the Java Short Notes series!