Immutable Objects in Java Concurrent Programming

Immutable Objects

Let's examine the date conversion problem first.

@Slf4j(topic = "c.DateParseDemo")
public class DateParseDemo {
    public static void main(String[] args) {
        runAnalysis();
    }

    private static void runAnalysis() {
        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    log.debug("Parsed: {}", formatter.parse("1951-04-21"));
                } catch (Exception e) {
                    log.error("Error: {}", e);
                }
            }).start();
        }
    }
}

The main issue here is that SimpleDateFormat is not thread-safe. When multiple threads access the same instance concurrently, they can corrupt the internal state, leading to incorrect results or exceptions.

The solution is to use DateTimeFormatter, which is thread-safe by design:

public static void main(String[] args) {
    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    for (int i = 0; i < 10; i++) {
        new Thread(() -> {
            TemporalAccessor result = formatter.parse("1951-04-21");
            log.debug("Parsed: {}", result);
        }).start();
    }
}

Immutable Design Principles

The String class serves as a perfect example of immutability. Once a String object is created, its content cannot be modified. Any operation that appears to change a String actually creates a new String object.

This immutability guarantees thread safety because no thread can modify the same String instance's state.

The internal value array in String is declared as final, meaning it cannot be reassigned after initialization.

Defensive Copy

Let's examine the substring implementation:

The substring method creates a new String object rather than sharing the underlying character array. When constructing a new string, it copies the content into a fresh char[] array. This technique of creating copies to prevent shared mutable state is called defensive copy.

Flyweight Pattern

The flyweight pattern minimzies memory usage by sharing instances of common objects. DateTimeFormatter uses this pattern - instead of creating a new formatter for each thread, you can create one formatter and share it across all threads.

Custom Connection Pool Implementation

Here's a practical implementation:

public class ConnectionPoolDemo {
    public static void main(String[] args) {
        ConnectionPool pool = new ConnectionPool(2);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                Connection conn = pool.acquire();
                try {
                    Thread.sleep(new Random().nextInt(1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                pool.release(conn);
            }).start();
        }
    }
}

@Slf4j(topic = "c.ConnectionPool")
class ConnectionPool {
    private final int poolSize;
    private final Connection[] connections;
    private final AtomicIntegerArray states;

    public ConnectionPool(int poolSize) {
        this.poolSize = poolSize;
        this.connections = new Connection[poolSize];
        this.states = new AtomicIntegerArray(new int[poolSize]);
        for (int i = 0; i < poolSize; i++) {
            connections[i] = new MockConnection("Connection-" + (i + 1));
        }
    }

    public Connection acquire() {
        while (true) {
            for (int i = 0; i < poolSize; i++) {
                if (states.get(i) == 0) {
                    if (states.compareAndSet(i, 0, 1)) {
                        log.debug("Acquired: {}", connections[i]);
                        return connections[i];
                    }
                }
            }
            synchronized (this) {
                try {
                    log.debug("Waiting for available connection...");
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void release(Connection conn) {
        for (int i = 0; i < poolSize; i++) {
            if (connections[i] == conn) {
                states.set(i, 0);
                synchronized (this) {
                    log.debug("Released: {}", conn);
                    this.notifyAll();
                }
                break;
            }
        }
    }
}

class MockConnection implements Connection {
    private final String identifier;

    public MockConnection(String identifier) {
        this.identifier = identifier;
    }

    @Override
    public String toString() {
        return "MockConnection{identifier='" + identifier + "'}";
    }

    @Override
    public Statement createStatement() throws SQLException {
        return null;
    }

    @Override
    public PreparedStatement prepareStatement(String sql) throws SQLException {
        return null;
    }

    @Override
    public CallableStatement prepareCall(String sql) throws SQLException {
        return null;
    }

    @Override
    public String nativeSQL(String sql) throws SQLException {
        return null;
    }

    @Override
    public void setAutoCommit(boolean autoCommit) throws SQLException {}

    @Override
    public boolean getAutoCommit() throws SQLException {
        return false;
    }

    @Override
    public void commit() throws SQLException {}

    @Override
    public void rollback() throws SQLException {}

    @Override
    public void close() throws SQLException {}

    @Override
    public boolean isClosed() throws SQLException {
        return false;
    }

    @Override
    public DatabaseMetaData getMetaData() throws SQLException {
        return null;
    }

    @Override
    public void setReadOnly(boolean readOnly) throws SQLException {}

    @Override
    public boolean isReadOnly() throws SQLException {
        return false;
    }

    @Override
    public void setCatalog(String catalog) throws SQLException {}

    @Override
    public String getCatalog() throws SQLException {
        return null;
    }

    @Override
    public void setTransactionIsolation(int level) throws SQLException {}

    @Override
    public int getTransactionIsolation() throws SQLException {
        return 0;
    }

    @Override
    public SQLWarning getWarnings() throws SQLException {
        return null;
    }

    @Override
    public void clearWarnings() throws SQLException {}

    @Override
    public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException {
        return null;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        return null;
    }

    @Override
    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException {
        return null;
    }

    @Override
    public Map<String, Class<?>> getTypeMap() throws SQLException {
        return null;
    }

    @Override
    public void setTypeMap(Map<String, Class<?>> map) throws SQLException {}

    @Override
    public void setHoldability(int holdability) throws SQLException {}

    @Override
    public int getHoldability() throws SQLException {
        return 0;
    }

    @Override
    public Savepoint setSavepoint() throws SQLException {
        return null;
    }

    @Override
    public Savepoint setSavepoint(String name) throws SQLException {
        return null;
    }

    @Override
    public void rollback(Savepoint savepoint) throws SQLException {}

    @Override
    public void releaseSavepoint(Savepoint savepoint) throws SQLException {}

    @Override
    public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        return null;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        return null;
    }

    @Override
    public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException {
        return null;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException {
        return null;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException {
        return null;
    }

    @Override
    public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException {
        return null;
    }

    @Override
    public Clob createClob() throws SQLException {
        return null;
    }

    @Override
    public Blob createBlob() throws SQLException {
        return null;
    }

    @Override
    public NClob createNClob() throws SQLException {
        return null;
    }

    @Override
    public SQLXML createSQLXML() throws SQLException {
        return null;
    }

    @Override
    public boolean isValid(int timeout) throws SQLException {
        return false;
    }

    @Override
    public void setClientInfo(String name, String value) throws SQLClientInfoException {}

    @Override
    public void setClientInfo(Properties properties) throws SQLClientInfoException {}

    @Override
    public String getClientInfo(String name) throws SQLException {
        return null;
    }

    @Override
    public Properties getClientInfo() throws SQLException {
        return null;
    }

    @Override
    public Array createArrayOf(String typeName, Object[] elements) throws SQLException {
        return null;
    }

    @Override
    public Struct createStruct(String typeName, Object[] attributes) throws SQLException {
        return null;
    }

    @Override
    public void setSchema(String schema) throws SQLException {}

    @Override
    public String getSchema() throws SQLException {
        return null;
    }

    @Override
    public void abort(Executor executor) throws SQLException {}

    @Override
    public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException {}

    @Override
    public int getNetworkTimeout() throws SQLException {
        return 0;
    }

    @Override
    public <T> T unwrap(Class<T> iface) throws SQLException {
        return null;
    }

    @Override
    public boolean isWrapperFor(Class<?> iface) throws SQLException {
        return false;
    }
}

Final Keyword Semantics

The final keyword in Java provides immutability guarantees at the reference level. When applied to:

  • Primitive types: the value cannot change
  • Object references: the reference cannot point to a different object
  • Arrays: the array reference cannot be reassigned

However, final does not make the contents of an array immutable. For true immutability, you need defensive copies.

Stateless Design

When designing Servlets for web applications, the recommended practice is to avoid instance variables. A class with no instance variables is inherently thread-safe because there is no shared state to corrupt. Each request operates on its own data, eliminating race conditions entirely.

This principle applies broad: stateless objects, immutable objects, and thread-local variables all provide thread safety through different mechanisms.

Tags: java Concurrency Immutable Objects Thread Safety Design Patterns

Posted on Thu, 07 May 2026 13:45:29 +0000 by rptasiuk