Java programming
concurrency
thread safety
non-final fields
synchronization

Synchronization of non-final field

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A Java field does not need to be final to be thread-safe. What matters is whether updates and reads obey the Java Memory Model through synchronization, volatile, or another concurrency primitive. Problems start when code mixes protected and unprotected access and assumes visibility will somehow still work out.

final and Synchronization Are Different Guarantees

final helps with immutability and safe initialization. Synchronization is about visibility and coordinated access to mutable state.

A mutable field cannot be final by definition, but it can still be safe if all access is protected consistently.

java
1public class Counter {
2    private int value;
3
4    public synchronized void increment() {
5        value++;
6    }
7
8    public synchronized int getValue() {
9        return value;
10    }
11}

This non-final field is safe because every access uses the same monitor.

The important point is not "is the field final." The important point is "how is the field published and accessed."

Why synchronized Works

Entering and leaving a synchronized block establishes a happens-before relationship. That means writes performed by one thread before releasing the lock become visible to another thread that later acquires the same lock.

Correct pattern:

java
1public class ConfigHolder {
2    private String config;
3    private final Object lock = new Object();
4
5    public void setConfig(String newConfig) {
6        synchronized (lock) {
7            config = newConfig;
8        }
9    }
10
11    public String getConfig() {
12        synchronized (lock) {
13            return config;
14        }
15    }
16}

Incorrect pattern:

  • synchronize writes
  • read without synchronization

That breaks the visibility guarantee, even if it "usually seems fine" on one machine.

When volatile Is Enough

If the field represents a simple value where each read and write is independent, volatile may be sufficient.

java
1public class ShutdownFlag {
2    private volatile boolean shutdownRequested;
3
4    public void requestShutdown() {
5        shutdownRequested = true;
6    }
7
8    public boolean isShutdownRequested() {
9        return shutdownRequested;
10    }
11}

Here volatile ensures visibility. When one thread sets the flag, another thread will observe the change without explicit locking.

But volatile is not a general substitute for synchronization.

Compound Operations Need Stronger Coordination

This is unsafe:

java
1public class UnsafeCounter {
2    private volatile int count;
3
4    public void increment() {
5        count++;
6    }
7}

count++ is not one indivisible action. It is:

  1. read the current value
  2. add one
  3. write the result

Two threads can interleave and lose updates.

Use synchronization or an atomic type instead:

java
1import java.util.concurrent.atomic.AtomicInteger;
2
3public class SafeCounter {
4    private final AtomicInteger count = new AtomicInteger();
5
6    public void increment() {
7        count.incrementAndGet();
8    }
9
10    public int getValue() {
11        return count.get();
12    }
13}

Safe Publication Matters Too

Even if a field is read and written correctly later, the containing object still needs to be published safely to other threads.

For example, if one thread constructs an object and another thread reads a reference to it through an unsafely shared field, the second thread may observe stale or partially initialized state.

Safe publication can happen through:

  • a properly synchronized handoff
  • a volatile reference
  • a thread-safe collection
  • static initialization

Thread safety is not only about the field accessor methods. It is about the entire lifecycle of the object.

Choose the Simplest Correct Primitive

A practical strategy is:

  1. immutable object if possible
  2. volatile for simple visibility-only flags or references
  3. synchronized for coordinated mutable state
  4. atomic classes for counters and compare-and-set style logic

Do not remove synchronization first and try to reason correctness back in later. Start with correctness, then optimize if measurement shows a real problem.

Common Pitfalls

  • Assuming a non-final field is automatically unsafe even when every access is properly synchronized.
  • Synchronizing writes but reading without the same lock.
  • Using volatile for compound updates such as increment or check-then-act.
  • Ignoring safe publication of the containing object.
  • Mixing several locking or visibility strategies for one field and losing a clear happens-before model.

Summary

  • A non-final field can be thread-safe if it is accessed under the right memory-visibility rules.
  • 'synchronized provides mutual exclusion and visibility when used consistently.'
  • 'volatile is useful for simple visibility cases, not for compound mutation logic.'
  • Safe publication matters just as much as safe access.
  • Choose the smallest concurrency primitive that correctly matches the field's behavior.

Course illustration
Course illustration

All Rights Reserved.