CodeWithAbdessamad

Threads

Threads

In the world of Java concurrency, threads are the building blocks that enable applications to perform multiple tasks simultaneously. Understanding how to create and manage threads is fundamental to developing responsive, scalable enterprise applications. This section dives deep into the core concepts of thread creation and synchronization—two pillars of reliable concurrent programming.

The Runnable Interface: Your Thread’s Workhorse

The Runnable interface is Java’s most flexible and recommended approach to creating threads. Unlike extending the Thread class (which we’ll cover in a later section), Runnable decouples thread logic from thread management, promoting cleaner code and better scalability. By implementing Runnable, you define a task that can be executed in a thread without tying the task to a specific thread object.

Why prefer Runnable?

  • Avoids the “single inheritance” problem (Java only allows one parent class per class)
  • Enables shared tasks across multiple threads (e.g., thread pools)
  • Encourages testability and separation of concerns

Here’s a concrete example demonstrating a Runnable that prints a message in a new thread:

<code class="language-java">public class SimpleRunnableExample {
<p>    public static void main(String[] args) {</p>
<p>        // Create a Runnable task (using lambda for brevity)</p>
<p>        Runnable task = () -> System.out.println("Hello from a new thread!");</p>
<p>        </p>
<p>        // Create and start the thread</p>
<p>        Thread thread = new Thread(task);</p>
<p>        thread.start();</p>
<p>        </p>
<p>        // Wait for the thread to complete (for demonstration)</p>
<p>        try {</p>
<p>            Thread.sleep(100);</p>
<p>        } catch (InterruptedException e) {</p>
<p>            Thread.currentThread().interrupt();</p>
<p>        }</p>
<p>    }</p>
<p>}</code>

Output
Hello from a new thread!

This example shows how Runnable allows you to focus purely on the task logic (task), while the Thread class handles thread lifecycle. For enterprise applications, we often use Runnable with ExecutorService to manage thread pools—this pattern scales efficiently across thousands of requests.

Key Takeaway for Runnable

Use Runnable when you need to define a reusable task that can run in multiple threads. It’s the industry standard for production code because it avoids thread inheritance pitfalls and enables scalable task execution.

Synchronization: Guarding Shared State

Synchronization is the mechanism that prevents race conditions when multiple threads access shared resources. Without proper synchronization, concurrent operations can corrupt data or produce unpredictable results. In Java, we use synchronized blocks and methods to create mutual exclusion around critical sections of code.

Why synchronization matters:

Imagine two threads incrementing a shared counter. If they run without synchronization, the counter might end up less than expected because they read the same value and write back without coordination. Synchronization ensures only one thread modifies the shared state at a time.

Here’s a classic race condition example without synchronization:

<code class="language-java">public class RaceConditionExample {
<p>    private int count = 0;</p>

<p>    public void increment() {</p>
<p>        count++; // Problematic without synchronization</p>
<p>    }</p>

<p>    public static void main(String[] args) {</p>
<p>        RaceConditionExample counter = new RaceConditionExample();</p>
<p>        Thread t1 = new Thread(() -> {</p>
<p>            for (int i = 0; i < 1000; i++) {</p>
<p>                counter.increment();</p>
<p>            }</p>
<p>        });</p>
<p>        Thread t2 = new Thread(() -> {</p>
<p>            for (int i = 0; i < 1000; i++) {</p>
<p>                counter.increment();</p>
<p>            }</p>
<p>        });</p>
<p>        t1.start();</p>
<p>        t2.start();</p>
<p>        try {</p>
<p>            t1.join();</p>
<p>            t2.join();</p>
<p>        } catch (InterruptedException e) {</p>
<p>            e.printStackTrace();</p>
<p>        }</p>
<p>        System.out.println("Final count: " + counter.count);</p>
<p>    }</p>
<p>}</code>

Output (likely less than 2000)
Final count: 1998

This output demonstrates a race condition—threads interfere with each other’s access to count. Now, let’s fix it with synchronization:

<code class="language-java">public class SynchronizedExample {
<p>    private int count = 0;</p>

<p>    public synchronized void increment() {</p>
<p>        count++; // Synchronized method</p>
<p>    }</p>

<p>    public static void main(String[] args) {</p>
<p>        SynchronizedExample counter = new SynchronizedExample();</p>
<p>        Thread t1 = new Thread(() -> {</p>
<p>            for (int i = 0; i < 1000; i++) {</p>
<p>                counter.increment();</p>
<p>            }</p>
<p>        });</p>
<p>        Thread t2 = new Thread(() -> {</p>
<p>            for (int i = 0; i < 1000; i++) {</p>
<p>                counter.increment();</p>
<p>            }</p>
<p>        });</p>
<p>        t1.start();</p>
<p>        t2.start();</p>
<p>        try {</p>
<p>            t1.join();</p>
<p>            t2.join();</p>
<p>        } catch (InterruptedException e) {</p>
<p>            e.printStackTrace();</p>
<p>        }</p>
<p>        System.out.println("Final count: " + counter.count);</p>
<p>    }</p>
<p>}</code>

Output
Final count: 2000

This works because synchronized ensures only one thread enters increment() at a time. For finer-grained control (e.g., locking specific resources), we use synchronized blocks:

<code class="language-java">public class SynchronizedBlockExample {
<p>    private int count = 0;</p>
<p>    private final Object lock = new Object();</p>

<p>    public void increment() {</p>
<p>        synchronized (lock) {</p>
<p>            count++; // Critical section protected by lock</p>
<p>        }</p>
<p>    }</p>

<p>    public static void main(String[] args) {</p>
<p>        SynchronizedBlockExample counter = new SynchronizedBlockExample();</p>
<p>        Thread t1 = new Thread(() -> {</p>
<p>            for (int i = 0; i < 1000; i++) {</p>
<p>                counter.increment();</p>
<p>            }</p>
<p>        });</p>
<p>        Thread t2 = new Thread(() -> {</p>
<p>            for (int i = 0; i < 1000; i++) {</p>
<p>                counter.increment();</p>
<p>            }</p>
<p>        });</p>
<p>        t1.start();</p>
<p>        t2.start();</p>
<p>        try {</p>
<p>            t1.join();</p>
<p>            t2.join();</p>
<p>        } catch (InterruptedException e) {</p>
<p>            e.printStackTrace();</p>
<p>        }</p>
<p>        System.out.println("Final count: " + counter.count);</p>
<p>    }</p>
<p>}</code>

Why blocks are better:

Using synchronized blocks (instead of methods) gives you more control. You can lock only the critical section without affecting the entire method. This is crucial for large applications where you don’t want to block non-critical operations.

Synchronization Tradeoffs

Approach When to Use Pros Cons
synchronized methods Simple shared state Less boilerplate Locks entire method
synchronized blocks Fine-grained control More precise locking Requires careful object handling
ReentrantLock Advanced scenarios (e.g., timeouts) More flexible Higher learning curve

Pro Tip: Always prefer synchronized blocks over methods for production code. They minimize contention and prevent thread starvation.

Summary

  • Runnable is the industry-standard way to define thread tasks—it decouples logic from thread management and enables scalable task execution (e.g., via thread pools).
  • Synchronization prevents race conditions by ensuring only one thread accesses shared resources at a time. Use synchronized blocks for fine-grained control and synchronized methods for simpler cases.
  • Always prioritize mutual exclusion over speed in concurrent systems—corrupted data costs far more than minor performance overhead.

Mastering these concepts lets you build threads that are both reliable and scalable—the foundation of enterprise-grade Java applications. 🌟