Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild
Back to Blog Explainer

Static Variables in Java: Shared State You Did Not Plan For

Sean

Platform Writer

Aug 30, 2026
8 min read

A static field belongs to the class rather than to any instance, so there is exactly one copy shared by every object and every thread in the JVM. That is the whole feature and the whole problem.

Static Variables in Java: Shared State You Did Not Plan For

Static is introduced early because public static void main demands it, and the usual explanation — “you can use it without creating an object” — is true and unhelpful. It describes the syntax and not the consequence.

The consequence is shared mutable state with process-wide scope. Sometimes that is exactly right. Often it is a bug that only appears under load or in the second test to run.

Table of contents

One copy, not one per object

public class Counter {
    static int total = 0;   // one copy, shared
    int instanceCount = 0;  // one copy per object

    void increment() {
        total++;
        instanceCount++;
    }
}

Counter a = new Counter();
Counter b = new Counter();
a.increment();
b.increment();

System.out.println(Counter.total);   // 2 -- shared
System.out.println(a.instanceCount); // 1 -- per instance

Access static members through the class name (Counter.total), not through an instance. a.total compiles and resolves to the same field, but it reads as instance state and is misleading enough that most style guides and linters flag it.

The lifetime is the class’s lifetime: a static field is initialised when the class is first loaded and lives until the classloader is discarded, which in a normal application means for the life of the process.

Initialisation order, and static blocks

Static fields initialise in source order when the class loads, and a static block runs at the same point:

public class Config {
    static final String ENV;
    static final Map<String, String> DEFAULTS = new HashMap<>();

    static {
        ENV = System.getenv().getOrDefault("APP_ENV", "development");
        DEFAULTS.put("timeout", "30");
        DEFAULTS.put("retries", "3");
    }
}

Class loading is lazy: it happens on first active use — instantiation, a static method call, or reading a non-constant static field. So the timing of that block is not obvious from reading the code, and it can be surprisingly late.

Two real hazards here. An exception thrown from a static initialiser becomes ExceptionInInitializerError, and every subsequent attempt to use the class throws NoClassDefFoundError — with the original cause nowhere in sight. That is one of the more confusing failures in Java, and it is why static initialisers should not do anything that can fail: no network calls, no file reads, no database connections.

The other is circular dependency between two classes’ static initialisers, which yields a partially initialised class and a null field that should be impossible. Both argue for keeping static initialisation trivial.

static final: constants and near-constants

The one uncontroversial use:

public static final int MAX_RETRIES = 3;
public static final String API_VERSION = "v2";

A static final primitive or String literal is a compile-time constant, inlined at every use site. Note the consequence: if a library changes such a constant, code compiled against the old value keeps the old value until recompiled.

The trap is static final on a mutable object, where final protects the reference and not the contents:

// Looks constant. Is not.
public static final List<String> ROLES = new ArrayList<>(List.of("admin", "user"));
ROLES.add("superuser");   // compiles and works

// Actually immutable
public static final List<String> ROLES = List.of("admin", "user");

List.of, Map.of and Set.of produce genuinely immutable collections and are the right choice for shared constants. A static final mutable collection is global mutable state wearing a constant’s clothes, and any code anywhere can modify it.

Thread safety, which is not automatic

Static fields are shared across threads with no synchronisation of any kind. total++ is a read, an increment and a write, and two threads interleaving those lose updates.

// Broken under concurrency
static int total = 0;
static void increment() { total++; }

// Correct
static final AtomicInteger TOTAL = new AtomicInteger();
static void increment() { TOTAL.incrementAndGet(); }

There is a second, subtler problem: without volatile or synchronisation there is no happens-before relationship, so one thread may never observe another’s write at all. That produces bugs that vanish under a debugger and reappear in production.

Also relevant: SimpleDateFormat is not thread-safe, and a static SimpleDateFormat shared across request threads is a well-known source of corrupted dates and occasional exceptions. Use DateTimeFormatter, which is immutable and thread-safe, or a ThreadLocal.

Why static state makes testing hard

The practical objection to static mutable state is testability. State persists between tests in the same JVM, so:

  • Tests pass alone and fail in a suite, or vice versa.
  • Results depend on execution order, and parallel execution breaks them non-deterministically.
  • You cannot substitute a static dependency without a bytecode-manipulating mocking library.
  • Cleanup requires a @BeforeEach that resets global state, which is easy to forget and easy to get wrong.

The alternative is dependency injection: pass collaborators in rather than reaching for them statically.

// Hard to test -- the dependency is unreachable from the test
public class OrderService {
    public void process(Order o) { Database.save(o); }
}

// Testable -- substitute the repository
public class OrderService {
    private final OrderRepository repo;
    public OrderService(OrderRepository repo) { this.repo = repo; }
    public void process(Order o) { repo.save(o); }
}

This is what dependency injection frameworks exist for, and it is why modern Java application code has far less static state than tutorials suggest.

Where static is genuinely right

It is not a smell in itself. Legitimate uses:

  • Constantsstatic final primitives, Strings, and immutable collections.
  • Pure utility methodsMath.max, Collections.sort. No state, no instance needed.
  • Factory methodsList.of, Optional.of, Instant.now.
  • Immutable shared instances — a thread-safe, stateless singleton.
  • Loggersprivate static final Logger LOG = ..., the near-universal idiom, and safe because loggers are thread-safe and stateless.

The test: is the state immutable, or is there genuinely one of these per process? If yes, static is fine. If it is mutable state that a test or a second instance would want to differ, it belongs on an instance.

Worth noting that static state is per-JVM, not per-application. When a service runs as several instances behind a load balancer — which is what autoscaling does — a static cache or counter exists separately in each one, and they will disagree. Anything that must be shared across instances belongs in a database or a shared cache.

Java services deploy on RunxBuild from a repository with a build log, a live route, runtime logs and rollback, with autoscaling between plans you choose and a managed Postgres or MySQL for the state that cannot live in a static field.

How this fits the rest of the stack

A static field is one copy per JVM shared by every instance and thread: correct for constants, utility methods and loggers, and a liability for mutable state that tests and concurrent requests both touch. Keep static initialisers trivial, use List.of rather than static final mutable collections, and remember that static state does not survive across instances. The RunxBuild hosting calculator shows the service and the database that shared state actually belongs in.

Useful related references:

FAQ

What is a static variable in Java?

A field declared with the static keyword, belonging to the class rather than to any instance. There is exactly one copy shared by every object and every thread in the JVM, initialised when the class is first loaded.

What is the difference between static and instance variables?

A static variable has one copy shared across all instances; an instance variable has a separate copy per object. Static variables live for the lifetime of the loaded class, instance variables for the lifetime of their object.

Are static variables thread-safe?

No. They are shared across threads with no synchronisation, so concurrent updates lose writes and, without volatile, one thread may never observe another’s changes. Use AtomicInteger, volatile, or proper synchronisation.

Is static final the same as a constant?

For primitives and String literals, yes — they are compile-time constants and get inlined. For object references, final protects only the reference, so a static final ArrayList can still be modified by anyone. Use List.of for genuinely immutable collections.

Why is static state bad for testing?

It persists between tests in the same JVM, so results depend on execution order and parallel runs become non-deterministic. It also cannot be substituted without bytecode-manipulating mocks. Prefer passing dependencies into constructors.

#static variable java#java static#class variables#thread safety#Java