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.