Understanding and Writing High‑Quality Concurrent Code in Java (Java 5 and Beyond)

This article explains why concurrency is essential, debunks common misconceptions, presents design principles and practical techniques, reviews Java 5 and later concurrency utilities such as atomic classes, explicit locks, CountDownLatch, ConcurrentHashMap, CopyOnWriteArrayList, BlockingQueue, and advanced models like producer‑consumer, reader‑writer, and the dining philosophers problem, and offers testing advice for robust multithreaded applications.

Java Captain
Java Captain
Java Captain
Understanding and Writing High‑Quality Concurrent Code in Java (Java 5 and Beyond)

Why Concurrency Is Needed

Concurrency decouples what to do from when to do it, improving throughput and allowing multiple parts of a program to work together; servlet containers already handle concurrency for Java web applications.

Common Misconceptions and Correct Views

- Concurrency always improves performance (it helps when CPU is idle but can degrade performance with excessive threads). - Writing concurrent code requires no design changes (decoupling often forces major architectural revisions). - Containers eliminate the need to consider concurrency (understanding container behavior is essential).

Correct observations:

- Concurrency adds code overhead. - Correct concurrency is complex, even for simple problems. - Bugs are hard to reproduce and detect. - Designing for concurrency often requires fundamental design changes.

Principles and Techniques for Concurrent Programming

Single‑Responsibility Principle

Separate concurrency‑related code from other code so it has its own lifecycle for development, modification, and tuning.

Limit Data Scope

When two threads modify the same field of a shared object, they can interfere; use critical sections sparingly.

Use Data Copies

CopyOnWriteArrayList creates a read‑only snapshot for each write, avoiding shared‑data conflicts.

Make Threads As Independent As Possible

Keep thread‑local variables; avoid sharing mutable state between threads, as demonstrated by servlet and Spring MVC controllers.

Concurrency Before Java 5

Java’s early thread model relied on pre‑emptive scheduling, shared objects, and synchronized locks, which often turned concurrency into simple queuing.

Key points about the synchronized keyword:

Locks only objects, not primitive types.

Only the specific object instance is locked.

Methods are equivalent to synchronized(this) blocks.

Static synchronized methods lock the Class object.

Locks are re‑entrant.

The volatile keyword ensures visibility of variable changes across threads but should be used only when updates do not depend on the current value.

Immutable objects (e.g., String) are naturally thread‑safe.

Concurrency in Java 5 and Later

Java 5 introduced the java.util.concurrent package, providing better thread‑safe containers, thread pools, non‑blocking solutions, and explicit lock mechanisms.

Atomic Classes

Classes such as AtomicInteger and AtomicLong perform lock‑free atomic operations.

/**
 * ID sequence generator
 */
public class IdGenerator {
    private final AtomicLong sequenceNumber = new AtomicLong(0);
    public long next() {
        return sequenceNumber.getAndIncrement();
    }
}

Explicit Locks

Replaces synchronized with more flexible locks like ReentrantLock and ReentrantReadWriteLock, supporting try‑lock and timed lock acquisition.

CountDownLatch

Allows one thread to wait for a set of other threads to complete.

import java.util.concurrent.CountDownLatch;

class Worker {
    private String name;
    private long workDuration;
    public Worker(String name, long workDuration) { this.name = name; this.workDuration = workDuration; }
    public void doWork() {
        System.out.println(name + " begins to work...");
        try { Thread.sleep(workDuration); } catch (InterruptedException ex) { ex.printStackTrace(); }
        System.out.println(name + " has finished the job...");
    }
}

class WorkerTestThread implements Runnable {
    private Worker worker; private CountDownLatch cdLatch;
    public WorkerTestThread(Worker worker, CountDownLatch cdLatch) { this.worker = worker; this.cdLatch = cdLatch; }
    public void run() { worker.doWork(); cdLatch.countDown(); }
}

class CountDownLatchTest {
    private static final int MAX_WORK_DURATION = 5000;
    private static final int MIN_WORK_DURATION = 1000;
    private static long getRandomWorkDuration(long min, long max) { return (long) (Math.random() * (max - min) + min); }
    public static void main(String[] args) {
        CountDownLatch latch = new CountDownLatch(2);
        Worker w1 = new Worker("骆昊", getRandomWorkDuration(MIN_WORK_DURATION, MAX_WORK_DURATION));
        Worker w2 = new Worker("王大锤", getRandomWorkDuration(MIN_WORK_DURATION, MAX_WORK_DURATION));
        new Thread(new WorkerTestThread(w1, latch)).start();
        new Thread(new WorkerTestThread(w2, latch)).start();
        try { latch.await(); System.out.println("All jobs have been finished!"); }
        catch (InterruptedException e) { e.printStackTrace(); }
    }
}

ConcurrentHashMap

Provides fine‑grained locking on individual buckets, avoiding the global lock of Collections.synchronizedMap. Supports atomic methods like putIfAbsent, remove, and replace.

CopyOnWriteArrayList

Creates a new copy on each modification, ensuring iterators see a stable snapshot; useful when reads dominate writes.

List<Double> list = new CopyOnWriteArrayList<>();

Queue and BlockingQueue

Queues distribute work among threads; BlockingQueue blocks on put when full and on take when empty, simplifying producer‑consumer patterns.

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class Producer implements Runnable {
    private BlockingQueue<Task> buffer;
    public Producer(BlockingQueue<Task> buffer) { this.buffer = buffer; }
    public void run() {
        while (true) {
            try { Task task = new Task(); buffer.put(task); System.out.println("Producer[" + Thread.currentThread().getName() + "] put " + task); }
            catch (InterruptedException e) { e.printStackTrace(); }
        }
    }
}

Concurrent Models

Key concepts such as critical resources, mutual exclusion, starvation, deadlock, and livelock are revisited before discussing specific models.

Producer‑Consumer

Multiple producers generate work items placed in a shared buffer; multiple consumers retrieve and process them, with blocking when the buffer is full or empty.

Reader‑Writer

Balancing many readers with occasional writers to maximize throughput while preventing starvation.

Dining Philosophers

Classic synchronization problem solved using Semaphore to avoid deadlock and livelock.

class AppContext {
    public static final int NUM_OF_FORKS = 5;
    public static final int NUM_OF_PHILO = 5;
    public static Semaphore[] forks;
    public static Semaphore counter;
    static {
        forks = new Semaphore[NUM_OF_FORKS];
        for (int i = 0; i < forks.length; ++i) forks[i] = new Semaphore(1);
        counter = new Semaphore(NUM_OF_PHILO - 1);
    }
    public static void putOnFork(int index, boolean leftFirst) throws InterruptedException { /* acquire forks */ }
    public static void putDownFork(int index, boolean leftFirst) throws InterruptedException { /* release forks */ }
}

class Philosopher implements Runnable {
    private int index; private String name;
    public Philosopher(int index, String name) { this.index = index; this.name = name; }
    public void run() {
        while (true) {
            try {
                AppContext.counter.acquire();
                boolean leftFirst = index % 2 == 0;
                AppContext.putOnFork(index, leftFirst);
                System.out.println(name + " is eating...");
                AppContext.putDownFork(index, leftFirst);
                AppContext.counter.release();
            } catch (InterruptedException e) { e.printStackTrace(); }
        }
    }
}

Testing Concurrent Code

Testing is challenging; write tests that expose race conditions, run them under varied configurations, increase thread count beyond CPU cores, and automate failure detection.

Separate non‑concurrent code first.

Make thread count configurable.

Run on multiple platforms.

Inject test hooks automatically.

Java 7 Concurrency Additions

Introduced TransferQueue for immediate hand‑off, Callable, Future, and FutureTask for tasks that return results, and the fork/join framework for divide‑and‑conquer parallelism.

import java.util.concurrent.Callable;
import java.util.concurrent.Future;

class CalcThread implements Callable<Double> {
    private List<Double> dataList = new ArrayList<>();
    public CalcThread() { for (int i = 0; i < 10000; ++i) dataList.add(Math.random()); }
    public Double call() {
        double total = 0;
        for (Double d : dataList) total += d;
        return total / dataList.size();
    }
}

The fork/join example computes the sum of 1‑10000 by recursively splitting the range.

class Calculator extends RecursiveTask<Integer> {
    private static final int THRESHOLD = 10;
    private int start, end;
    public Calculator(int start, int end) { this.start = start; this.end = end; }
    protected Integer compute() {
        if ((end - start) < THRESHOLD) {
            int sum = 0; for (int i = start; i <= end; i++) sum += i; return sum;
        } else {
            int middle = (start + end) >>> 1;
            Calculator left = new Calculator(start, middle);
            Calculator right = new Calculator(middle + 1, end);
            left.fork(); right.fork();
            return left.join() + right.join();
        }
    }
}

Java 7 also switched the default array sort to TimSort, which leverages the fork/join framework for better performance on modern multicore CPUs.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

ThreadPooljava.util.concurrent
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.