Unit 6: Concurrency, Localization, and Multithreading

CSE406 — Advanced Java Programming 8 min read

I. Foundations and Governing Principles

Concurrency allows multiple tasks to make progress during overlapping periods, while localization adapts an application’s language and regional behavior without changing its core logic. Java supports concurrency through threads, task abstractions, executors, and fork/join processing; it supports localization primarily through Locale and ResourceBundle.

  • Concurrency: Structures independently executable tasks whose operations may be interleaved by the operating system and Java Virtual Machine (JVM).
  • Parallelism: Executes tasks simultaneously on multiple processor cores; concurrency does not necessarily imply parallel execution.
  • Thread safety: Requires shared mutable data to remain correct under arbitrary scheduling, commonly through synchronization, locks, atomic classes, or immutability.
  • Task separation: Represents work with Runnable or Callable, keeping the task definition separate from thread creation and management.
  • Executor framework: Manages thread creation, reuse, scheduling, shutdown, and result handling through high-level APIs.
  • Localization principle: Separates locale-dependent resources, such as messages, from application code.
  • Locale convention: Identifies linguistic and regional preferences through components such as language, country, and optional variant.
  • Resource selection: Chooses the closest matching resource bundle using locale-specific fallback rules.

II. Scheduling and Concurrent Execution — Managing Independent Work

Java concurrency depends on scheduled threads, but applications should normally express work as tasks and delegate thread management to executor services.

A. Describing operating system task scheduling

Operating system scheduling determines which runnable thread receives processor time at a particular moment.

  • Scheduler role: The operating system selects threads from the runnable set and assigns them to available CPU cores.
  • Time slicing: On a preemptive system, a running thread may receive a short time quantum before another thread is scheduled.
  • Context switch: Switching execution requires saving one thread’s state, including registers and program counter, and restoring another’s state.
  • Thread priority: Java priorities range from Thread.MIN_PRIORITY (1) to Thread.MAX_PRIORITY (10), with NORM_PRIORITY equal to 5.
    • Priority is a scheduling hint, not a guarantee of execution order.
    • Behavior varies among operating systems and JVM implementations.
  • Scheduling states: A thread waiting for I/O, a lock, or a timer cannot use the processor until its waiting condition ends.
  • Nondeterminism: Two executions of the same program may interleave operations differently, so correctness must not depend on timing assumptions.
  • Starvation risk: A thread can be delayed indefinitely when other threads repeatedly obtain the required processor time or shared resource.

B. Creating worker threads using Runnable and Callable

Runnable and Callable define units of work that can be executed by worker threads.

  1. Runnable:

    • Method: Declares void run() and cannot return a computed result.
    • Exceptions: run() cannot declare checked exceptions.
    • Use case: Appropriate for actions such as logging, sending notifications, or updating independent records.
  2. Callable<V>:

    • Method: Declares V call() throws Exception, where V is the result type.
    • Result access: Submission returns a Future<V> whose get() method waits for completion.
    • Failure access: If call() fails, Future.get() throws ExecutionException wrapping the original cause.
JAVA
Runnable audit = () -> System.out.println("Audit complete");

Callable<Integer> total = () -> {
    int a = 12;
    int b = 8;
    return a + b;
};
  • Worker principle: The task describes what must run; an executor controls where and when it runs.
  • Cancellation: future.cancel(true) requests interruption when the task is already running, but cooperative task code must respond to interruption.

C. Using an ExecutorService to concurrently execute tasks

ExecutorService executes submitted tasks through a managed pool of reusable worker threads.

  • Creation: Executors.newFixedThreadPool(2) creates a pool containing at most two active worker threads.
  • Submission: execute(Runnable) starts work without producing a Future, whereas submit(...) returns a Future.
  • Concurrency limit: With two workers and five tasks, no more than two tasks normally execute simultaneously; remaining tasks wait in the pool’s queue.
  • Result retrieval: Future.get() blocks until the result is available, while isDone() provides a non-blocking completion check.
  • Order: Submission order does not guarantee completion order because task duration and scheduling differ.
  • Lifecycle: Executor threads usually keep the JVM alive, so the service must be shut down.
JAVA
ExecutorService pool = Executors.newFixedThreadPool(2);

Future<Integer> result = pool.submit(() -> 40 + 2);
pool.execute(() -> System.out.println("Independent task"));

try {
    System.out.println(result.get());
} finally {
    pool.shutdown();
}
  • Shutdown behavior:
    • shutdown() rejects new tasks but permits submitted tasks to finish.
    • shutdownNow() attempts to interrupt active tasks and returns tasks that never started.
    • awaitTermination(...) waits for orderly termination for a bounded period.

D. RecursiveTask

RecursiveTask<V> represents a fork/join computation that recursively divides work and returns a result of type V.

  • Framework: Tasks execute in a ForkJoinPool, commonly using work-stealing so idle workers can take queued tasks from busy workers.
  • Required method: A subclass overrides protected V compute().
  • Base case: A sufficiently small input is processed directly to prevent unlimited subdivision.
  • Recursive case: A large input is split into subtasks whose results are combined.
  • Fork and join: fork() schedules asynchronous execution; join() waits for and obtains the result.
JAVA
class SumTask extends RecursiveTask<Long> {
    private final long[] values;
    private final int start, end;

    SumTask(long[] values, int start, int end) {
        this.values = values;
        this.start = start;
        this.end = end;
    }

    protected Long compute() {
        if (end - start <= 1000) {
            long sum = 0;
            for (int i = start; i < end; i++) sum += values[i];
            return sum;
        }

        int middle = (start + end) / 2;
        SumTask left = new SumTask(values, start, middle);
        left.fork();
        long rightResult =
            new SumTask(values, middle, end).compute();
        return left.join() + rightResult;
    }
}
  • Best fit: CPU-bound, divisible operations such as array aggregation are suitable; blocking network or database calls can reduce pool efficiency.

III. Multithreading — Thread Structure and Execution States

Multithreading places multiple threads of execution within one process, allowing responsive and concurrent programs while introducing coordination requirements.

A. Overview of multithreading

Multithreading enables a Java process to perform several activities through threads sharing the same heap.

  • Shared process resources: Threads share objects, class data, and open process resources, but each thread has its own call stack and program counter.
  • Responsiveness: A user-interface thread can remain responsive while a worker performs file or network operations.
  • Throughput: Independent CPU-bound tasks may run in parallel when processor cores and suitable workers are available.
  • Low isolation: An uncaught thread failure does not necessarily terminate every thread, but corrupted shared state can affect the whole process.
  • Race condition: The result depends on interleaving, as when two threads perform a non-atomic count++.
  • Visibility: Without a happens-before relationship established by mechanisms such as synchronized, volatile, or thread start/join, one thread may not observe another’s latest writes.
  • Deadlock: Two threads can wait permanently when each holds one lock and requests the other.

B. Thread life cycle

A Java thread moves among states represented by the Thread.State enumeration.

  • NEW: The Thread object exists, but start() has not been called.
  • RUNNABLE: The thread is eligible to run or is currently executing; Java combines operating-system ready and running conditions in this state.
  • BLOCKED: The thread is waiting to enter a synchronized block or method whose monitor another thread owns.
  • WAITING: The thread waits indefinitely after operations such as Object.wait() without a timeout or Thread.join().
  • TIMED_WAITING: The thread waits for a bounded duration, as with Thread.sleep(500) or timed join().
  • TERMINATED: The run() method has completed normally or ended because of an uncaught exception.
  • One-start rule: Calling start() twice on the same Thread produces IllegalThreadStateException; create a new thread for another execution.
  • Interruption: interrupt() requests that a thread stop waiting or notice cancellation; it does not forcibly terminate arbitrary code.

C. Creating tasks and threads

Creating concurrent work involves defining a task, associating it with a thread or executor, and starting execution.

  • Task definition: A lambda can implement Runnable because it is a functional interface with one abstract method.
  • Thread construction: new Thread(task, "report-worker") associates the task with a named thread.
  • Starting execution: start() creates a new execution path that invokes run().
  • Direct call distinction: Calling run() directly executes on the current thread and creates no concurrency.
  • Completion coordination: join() makes the calling thread wait until the target thread terminates.
JAVA
Runnable task = () ->
    System.out.println(Thread.currentThread().getName());

Thread worker = new Thread(task, "report-worker");
worker.start();
worker.join();
  • Preferred scale: Direct thread creation is reasonable for simple demonstrations or dedicated long-lived workers; executors are preferable for numerous short tasks.

D. Thread class and Runnable interface

Thread represents an execution mechanism, whereas Runnable represents work to be performed.

  1. Extending Thread:

    • Override run() and invoke start() on the instance.
    • Java’s single inheritance rule prevents the class from extending another class.
    • Task logic becomes tightly coupled to thread management.
  2. Implementing Runnable:

    • Implement run() and pass the object to Thread or an executor.
    • The same task type can be tested directly or executed through different concurrency mechanisms.
    • The implementing class remains free to extend another class.
JAVA
class PrinterTask implements Runnable {
    public void run() {
        System.out.println("Printing");
    }
}

Thread thread = new Thread(new PrinterTask());
thread.start();
  • Design preference: Runnable generally provides better separation of concerns, reuse, and compatibility with ExecutorService.

IV. Localization — Adapting Applications to Language and Region

Localization externalizes language- and region-dependent content so the same application can serve users with different conventions.

A. Describing the advantages of localizing an application

Localization improves accessibility and maintainability by adapting presentation without duplicating business logic.

  • Language support: Labels such as "Save" can be supplied as "Enregistrer" for French users through external resources.
  • Regional conventions: Dates, numbers, currencies, and percentages can follow locale-specific formats through classes such as DateTimeFormatter and NumberFormat.
  • Maintainability: Translators can modify bundle values without changing Java control flow.
  • Consistency: A shared message key, such as menu.file, gives related screens a common translation source.
  • Scalability: Supporting an additional locale usually requires another resource bundle rather than a separate application build.
  • Limit: Localization must account for text expansion, plural rules, character encoding, and right-to-left layouts; message translation alone is insufficient.

B. Reading and setting the locale using the Locale object

A Locale identifies language and regional preferences used by locale-sensitive Java APIs.

  • Components: In Locale.US, the language is "en" and the country is "US"; language codes are generally lowercase and country codes uppercase.
  • Reading defaults: Locale.getDefault() obtains the JVM’s current default locale.
  • Setting defaults: Locale.setDefault(locale) changes the process-wide default and can affect unrelated locale-sensitive operations.
  • Constants: Java supplies values such as Locale.US, Locale.UK, Locale.FRANCE, and Locale.JAPAN.
  • Builder: new Locale.Builder() validates components and supports language tags.
JAVA
Locale current = Locale.getDefault();
Locale canadianFrench = new Locale.Builder()
    .setLanguage("fr")
    .setRegion("CA")
    .build();

Locale.setDefault(canadianFrench);
  • Explicit preference: Passing a locale directly to formatting or bundle APIs avoids hidden dependence on the machine’s default settings.

C. Building a resource bundle for each locale

A resource bundle stores locale-specific key-value resources under a shared base name.

  • Properties format: A base bundle named Messages.properties may contain welcome=Welcome.
  • Locale-specific file: Messages_fr.properties can define the same key as welcome=Bienvenue.
  • Regional specialization: Messages_fr_CA.properties can override values specifically for Canadian French.
  • Key consistency: Every locale should use stable keys such as button.submit; application code depends on keys, not translated values.
  • Java alternative: A bundle may extend ListResourceBundle and return an Object[][], allowing values other than strings.
  • Fallback chain: For fr_CA, Java searches progressively suitable bundles, including regional, language, and base candidates, subject to its bundle resolution rules.
  • Packaging: Bundle files must be available on the application classpath under the package corresponding to their base name.

D. Calling a resource bundle from an application

An application retrieves the appropriate bundle with ResourceBundle.getBundle() and accesses values by key.

  • Lookup: The base name excludes locale suffixes and file extensions.
  • Locale selection: Passing Locale.FRANCE requests the closest matching French bundle.
  • Value retrieval: getString("welcome") returns the text associated with that key.
  • Missing data: A missing bundle or unresolved key causes MissingResourceException.
  • Formatting: Parameterized localized messages can be processed with MessageFormat using the same locale.
JAVA
Locale locale = Locale.FRANCE;
ResourceBundle messages =
    ResourceBundle.getBundle("i18n.Messages", locale);

String welcome = messages.getString("welcome");
System.out.println(welcome);
  • Runtime switching: Selecting another locale and loading the bundle again changes displayed resources without altering the application’s business logic.