Java Null Handling with Optional and Multithreading Fundamentals

Handling Null Values with Optional

The Optional class serves as a container object which may or may not contain a non-null value. It helps in avoiding NullPointerException by enforcing explicit checks for presence.

Key Methods

  • of(T value): Creates an Optional describing the given non-null value. Passing null here results in an exception.
  • ofNullable(T value): Creates an Optional describing the given value. If the value is null, it returns an empty Optional instead of throwing an error.
  • orElse(T other): Returns the value if present; otherwise, returns the specified default value.
  • isPresent(): Returns true if a value is present in the container.
  • get(): Retrieves the value if present. Throws an exception if the value is absent.

Understanding Concurrency and Parallelism

In Java development, distinguishing between concurrency and parallelism is crucial:

  • Concurrency: Multiple tasks make progress by alternating execution on a single CPU core.
  • Parallelism: Multiple tasks execute simultaneously across multiple CPU cores.

Strategies for Thread Creation

Java provides two primary ways to define tasks that run in separate threads.

Approach 1: Inheriting the Thread Class

This method involves creating a subclass of Thread and overriding the run method. While straightforward, it limits the class from extending any other parent class.

public class SequenceGenerator extends Thread {
    @Override
    public void run() {
        System.out.println("Worker thread initiated");
        for (int step = 0; step < 15; step++) {
            System.out.println("Processing step: " + step);
        }
    }
}

public class AppLauncher {
    public static void main(String[] args) {
        SequenceGenerator worker = new SequenceGenerator();
        worker.start(); // Invokes the run method in a new thread
    }
}

Approach 2: Implementing the Runnable Interface

This approach decouples the task logic from the thread control. A class implements Runnable, and an instance is passed to a Thread object. This allows the task class to extend other classes if needed.

public class PrintTask implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 15; i++) {
            System.out.println("Task iteration: " + i);
        }
    }
}

public class AppLauncher {
    public static void main(String[] args) {
        PrintTask job = new PrintTask();
        Thread worker = new Thread(job);
        worker.start();
    }
}

Thread Control Methods

The Thread class provides several methods to manage execution flow and identification.

  • getName() / setName(String): Retrieve or modify the thread's identifier.
  • currentThread(): Static method to get a reference to the currently executing thread.
  • sleep(long millis): Pauses the current thread for a specified duration.
  • join(): Causes the current thread to wait until the thread upon which join is called completes.

The following example demonstrates a worker thread performing operations while the main thread waits for its completion using join.

class DataProcessor implements Runnable {
    @Override
    public void run() {
        Thread current = Thread.currentThread();
        for (int k = 0; k < 100; k++) {
            System.out.println(current.getName() + " processing item: " + k);
        }
    }
}

public class ExecutionFlow {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(new DataProcessor());
        worker.start();
        
        // Main thread waits for worker to finish
        worker.join(); 
        
        for (int m = 0; m < 200; m++) {
            System.out.println("Main thread resuming: " + m);
            Thread.sleep(5);
        }
    }
}

Thread Safety and Race Conditions

When multiple threads access shared mutable data simultaneously, inconsistencies such as duplicate processing or negative values can occur. Consider a scenario where multiple threads decrement a shared stock counter.

public class InventoryManager implements Runnable {
    private int stockCount = 100;

    @Override
    public void run() {
        while (stockCount > 0) {
            Thread current = Thread.currentThread();
            try {
                Thread.sleep(2); // Simulate processing delay
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(current.getName() + " sold item. Remaining: " + stockCount);
            stockCount--;
        }
    }
}

public class StoreSimulation {
    public static void main(String[] args) {
        InventoryManager manager = new InventoryManager();
        Thread cashier1 = new Thread(manager, "Register 1");
        Thread cashier2 = new Thread(manager, "Register 2");
        Thread cashier3 = new Thread(manager, "Register 3");
        
        cashier1.start();
        cashier2.start();
        cashier3.start();
    }
}

Without synchronization, the output may show negative stock counts or duplicate sales due to race conditions.

Synchronization Mechanisms

To prevent data corruption, access to shared resources must be controlled.

1. Synchronized Blocks

Using the synchronized keyword ensures that only one thread can execute the protected block at a time. A lock object is required to coordinate access.

public class InventoryManager implements Runnable {
    private int stockCount = 100;
    private final Object mutex = new Object();

    @Override
    public void run() {
        while (true) {
            synchronized (mutex) {
                if (stockCount <= 0) break;
                
                Thread current = Thread.currentThread();
                try {
                    Thread.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(current.getName() + " sold item. Remaining: " + stockCount);
                stockCount--;
            }
        }
    }
}

2. Explicit Lock Interface

The Lock interface (e.g., ReentrantLock) offers more flexibility than synchronized blocks. It requires manual locking and unlocking, ideally within a try-finally structure to prevent deadlocks.

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class InventoryManager implements Runnable {
    private int stockCount = 100;
    private final Lock mutex = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            mutex.lock();
            try {
                if (stockCount <= 0) break;
                
                if (stockCount > 0) {
                    Thread current = Thread.currentThread();
                    try {
                        Thread.sleep(5);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(current.getName() + " sold item. Remaining: " + stockCount);
                    stockCount--;
                }
            } finally {
                mutex.unlock();
            }
        }
    }
}

Tags: java Optional multithreading Concurrency Synchronization

Posted on Thu, 29 Oct 2026 16:42:49 +0000 by sb