MultiThreaded Programming  «Prev  Next»

Lesson 7 Thread Synchronization
Objective Understand how synchronization ensures correctness in concurrent code, when to use it, and modern alternatives that reduce contention.

How Thread Synchronization Impacts Multithreaded Programming

When multiple threads read and write the same data concurrently, you risk data races and visibility bugs (seeing stale values). Synchronization coordinates access so only one thread at a time executes a critical section, and also provides the happens-before memory-visibility guarantee: writes in one synchronized block become visible to a thread that subsequently enters a synchronized block guarded by the same monitor.

What synchronized actually does

Important: Synchronization doesn’t make code “uninterruptible.” An interrupt does not force a monitor release. Threads blocked trying to enter a monitor are not made to throw InterruptedException, but once they acquire it, normal rules apply.

Method vs. block, and choosing the lock


// Method-level: monitor is 'this' (or the Class object if static)
public synchronized void increment() { count++; }

// Block-level: pick a specific lock to narrow scope and reduce contention
private final Object lock = new Object();
public void incrementSafely() {
    synchronized (lock) {
        count++;
    }
}

Prefer block-level synchronization to keep critical sections small, and synchronize on a private final lock to avoid accidental external locking.

Cost and good practice

Race condition demo → then fixes


// UNSAFE: no synchronization
class Counter {
    int value = 0;
    void increment() { value++; }   // data race: read-modify-write is not atomic
}

// FIX 1: synchronized block
class SyncCounter {
    private int value = 0;
    private final Object lock = new Object();
    void increment() { synchronized (lock) { value++; } }
    int get() { synchronized (lock) { return value; } }
}

// FIX 2: lock-free atomic
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
    private final AtomicInteger value = new AtomicInteger();
    void increment() { value.incrementAndGet(); }
    int get() { return value.get(); }
}

Updated example: prove two distinct threads

The original example started two threads with the same name but didn’t show they were distinct. Print the id and name, and join() so the main thread waits for both to finish.




class SimpleTask implements Runnable {
    private final String label;
    SimpleTask(String label) { this.label = label; }

    @Override public void run() {
        for (int i = 0; i < 10; i++) {
            Thread t = Thread.currentThread();
            System.out.println(i + " | id=" + t.getId() + " | name=" + t.getName() + " | " + label);
            try {
                Thread.sleep((long)(Math.random() * 300));
            } catch (InterruptedException ignored) {}
        }
        System.out.println("DONE | id=" + Thread.currentThread().getId() + " | " + label);
    }
}

public class TwoThreadsDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread a = new Thread(new SimpleTask("worker A"), "Java Programmer #1");
        Thread b = new Thread(new SimpleTask("worker B"), "Java Programmer #2");
        a.start();
        b.start();
        a.join();
        b.join();
        System.out.println("Both threads finished.");
    }
}


What you’ll see: interleaved lines containing two different ids and names, then two DONE lines, and finally “Both threads finished.” This demonstrates two separate threads executing.

Modern alternatives and when to use them

Self-check (multiple choice)

Question: What happens when you compile and run the updated example that starts two threads with distinct names?

  1. It fails to compile because Runnable must have a no-args constructor.
  2. It compiles but the second thread overwrites the first, so run() executes once.
  3. It compiles and runs, interleaving output from two threads with different ids/names, and both finish.
  4. It compiles but throws RuntimeException due to duplicate names.

Answer: c

  1. Thread names need not be unique; they’re descriptive only.
  2. Two separate Thread instances execute the Runnable independently; no overwriting occurs.
  3. join() ensures the main thread waits for both to complete, so you see two DONE lines.
  4. No exceptions are thrown by this code.

SEMrush Software