Immutable Object Design and Implementation
Thread Safety Through Immutability
Mutable objects like SimpleDateFormat are not thread-safe. When shared across threads without synchronization, they can produce inconsistent or erroneous results:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i = 0; i < 20; ++i) {
new Thread(() -> {
try {
log.warn("{}", sdf.parse("1951-04-21"));
} catch (Exception e) {
log.error("{}", e);
}
}).start();
}
This often leads to NumberFormatException due to internal state corruption.
A thread-safe alternative is DateTimeFormatter, introduced in Java 8:
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 0; i < 20; ++i) {
new Thread(() -> {
TemporalAccessor parsed = formatter.parse("1951-04-21");
log.warn("{}", parsed);
}).start();
}
DateTimeFormatter is both final and immutable, ensuring safe concurrent access.
Why Is String Final?
The String class is declared final for:
- Enabling string interning (string pool)
- Guaranteeing thread safety
- Ensuring consistent hash code computation
Principles of Immutable Class Design
Key characteristics include:
- All fields are
privateandfinal - The class itself is
finalto prevent subclassing - No methods modify internal state
- Defensive copying is used when exposing internal mutable components
For example, String's constructor from a char[] performs a copy:
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
Similarly, substring() creates a new String with a copied segment of the original character array:
public String(char value[], int offset, int count) {
// ...
this.value = Arrays.copyOfRange(value, offset, offset + count);
}
Classes like BigInteger and BigDecimal follow the same pattern—operations return new instances rather than modifying existing ones.
Immutable Objects and the Flyweight Pattern
Flyweight Pattern Overview
The flyweight pattern reduces memory usage by sharing common object state among multiple objects. Only intrinsic (shared) state is stored in the flyweight; extrinsic (context-specific) state is passed in during operations.
Flyweight in JDK
Boxed Primitive Caches:
Integer,Byte,Short, andLongcache values in[-128, 127]Charactercaches[0, 127]BooleancachesTRUEandFALSE
These caches are initialized on first use and enable object reuse via autoboxing.
String Interning:
The JVM maintains a string pool where literal strings and interned strings are reused, reducing duplicate String instances.
Custom Resource Pool Using Flyweight Concepts
A connection pool can leverage atomic operations and wait/notify mechanics to manage limited resources:
class ConnectionPool {
private final int size;
private final Connection[] pool;
private final AtomicIntegerArray status; // 0 = free, 1 = in use
ConnectionPool(int size) {
this.size = size;
this.pool = new Connection[size];
this.status = new AtomicIntegerArray(size);
for (int i = 0; i < size; i++) {
pool[i] = new MockConnection();
}
}
public Connection acquire() {
while (true) {
for (int i = 0; i < size; i++) {
if (status.get(i) == 0 && status.compareAndSet(i, 0, 1)) {
return pool[i];
}
}
synchronized (this) {
try {
wait(); // Wait until a connection is freed
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
public void release(Connection conn) {
for (int i = 0; i < size; i++) {
if (pool[i] == conn) {
status.set(i, 0);
synchronized (this) {
notifyAll();
}
break;
}
}
}
}
Threads wait when no connections are available to avoid busy-waiting and excessive CAS contention.
Potential Optimizations
- Support timeout on acquisition
- Validate connection liveness
- Allow dynamic pool resizing
- Use more efficient waiting mechanisms (e.g.,
Condition)
Production systems typically use mature libraries like HikariCP, Druid, or Apache Commons Pool.
Final Fields and Memory Visibility
Final Field Semantics
- For primitives: value cannot change after initialization
- For references: reference cannot change, but referenced object may be mutable
- Must be initialized in declaration or all constructors
Memory Guarantees
The JVM inserts a write barrier after final field assignments in constructors. This ensures that once an object is visible to other threads, its final fields are properly initialized—no risk of seeing default values (e.g., 0 or null).
For static final constants within compile-time constant expression limits, the JVM inlines values directly into calling code, bypassing field reads entirely.
Stateless Classes Are Thread-Safe
A class with no instance fields (i.e., no state) is inherently thread-safe. This principle underlies recommendations to avoid member variables in servlets—since there’s no shared mutable state, no synchronization is needed.