Java
Thread Safety
Static Blocks
Concurrency
Multithreading

Thread safety of static blocks in Java

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Java static initializer blocks are thread-safe during class initialization, but that guarantee is often misunderstood. The JVM ensures one-time synchronized initialization per class loader. After initialization, normal concurrency rules still apply to any mutable static state.

JVM Guarantee for Class Initialization

When a class is first actively used, JVM runs class initialization before allowing regular access. Only one thread executes initialization for that class while other threads wait.

Active use includes:

  • reading or writing a static field
  • invoking a static method
  • creating an instance
java
1public final class AppConfig {
2    public static final String MODE;
3
4    static {
5        MODE = System.getProperty("app.mode", "dev");
6        System.out.println("AppConfig initialized");
7    }
8
9    private AppConfig() {}
10}

AppConfig static block runs once per class loader.

Initialization Safety Is Not Ongoing Mutation Safety

A frequent mistake is assuming static data remains thread-safe because it was initialized safely. That is false for mutable objects.

Unsafe example:

java
1import java.util.HashMap;
2import java.util.Map;
3
4public final class BadStore {
5    public static final Map<String, Integer> COUNTS;
6
7    static {
8        COUNTS = new HashMap<>();
9    }
10
11    private BadStore() {}
12}

Concurrent writes to this map are unsafe despite safe initialization.

Use Concurrent Structures for Shared Static State

If static state is mutable and shared, use concurrency-aware collections.

java
1import java.util.concurrent.ConcurrentHashMap;
2import java.util.concurrent.ConcurrentMap;
3
4public final class Metrics {
5    private static final ConcurrentMap<String, Long> COUNTS;
6
7    static {
8        COUNTS = new ConcurrentHashMap<>();
9    }
10
11    private Metrics() {}
12
13    public static void increment(String key) {
14        COUNTS.merge(key, 1L, Long::sum);
15    }
16
17    public static long get(String key) {
18        return COUNTS.getOrDefault(key, 0L);
19    }
20}

This protects runtime updates after initialization completes.

Keep Static Initialization Lightweight

Static blocks should avoid heavy I O and remote calls. Expensive initialization increases startup latency and can fail in unpredictable ways.

Good static-block candidates:

  • constants
  • immutable maps
  • light local setup
java
1import java.util.Map;
2
3public final class StatusCodes {
4    public static final Map<Integer, String> MAP;
5
6    static {
7        MAP = Map.of(
8            200, "OK",
9            404, "NOT_FOUND",
10            500, "ERROR"
11        );
12    }
13
14    private StatusCodes() {}
15}

Immutable static data reduces concurrency risk significantly.

Lazy Initialization with Holder Idiom

For expensive setup needed only on demand, use initialization-on-demand holder.

java
1public final class ServiceRegistry {
2    private ServiceRegistry() {}
3
4    private static final class Holder {
5        private static final ServiceRegistry INSTANCE = new ServiceRegistry();
6    }
7
8    public static ServiceRegistry instance() {
9        return Holder.INSTANCE;
10    }
11}

This keeps lazy behavior while using JVM class initialization guarantees.

Class Loader Nuance

The one-time guarantee is per class loader. In application servers or plugin systems, the same class can initialize once in each loader context. This matters when diagnostics seem to show repeated static initialization.

Log class loader identity during debugging if initialization appears to run more than once unexpectedly.

Quick Concurrency Validation

A simple test can confirm one-time initialization behavior.

java
1import java.util.concurrent.CountDownLatch;
2
3public class InitDemo {
4    public static void main(String[] args) throws Exception {
5        int n = 8;
6        CountDownLatch start = new CountDownLatch(1);
7        CountDownLatch done = new CountDownLatch(n);
8
9        for (int i = 0; i < n; i++) {
10            new Thread(() -> {
11                try {
12                    start.await();
13                    System.out.println(AppConfig.MODE);
14                } catch (InterruptedException e) {
15                    Thread.currentThread().interrupt();
16                } finally {
17                    done.countDown();
18                }
19            }).start();
20        }
21
22        start.countDown();
23        done.await();
24    }
25}

Initialization log should appear once for that loader.

Common Pitfalls

  • Assuming static initialization safety means mutable static fields are always thread-safe.
  • Performing slow network work in static blocks.
  • Catching and suppressing static initialization exceptions.
  • Using non-concurrent collections for shared static mutation.
  • Ignoring class loader boundaries in container environments.

Summary

  • Java static blocks are thread-safe for one-time class initialization.
  • The guarantee applies per class loader, not globally forever.
  • Mutable static state still needs proper concurrency controls.
  • Keep static initialization fast and deterministic.
  • Use holder idiom for lazy initialization when needed.

Related reading
Course
Intermediate
27 lessons
14 hours
OOD Fundamentals

Master object-oriented design from first principles, SOLID, design patterns, and classic interview problems with hands-on coding.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.