Introduction to CopyOnWriteArrayList
CopyOnWriteArrayList is a thread-safe variant of ArrayList where all mutative operations (add, set, remove, etc.) create a new copy of the underlying array. This makes it ideal for read-heavy scenarios where traversal operations vastly outnumber mutations.
1. Basic Usage and Characteristics
Basic Operations Example
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteArrayListBasic {
public static void main(String[] args) {
// Create a CopyOnWriteArrayList
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
// Add elements - creates a new underlying array
list.add("Apple");
list.add("Banana");
list.add("Cherry");
System.out.println("Initial list: " + list);
// Iteration - uses the snapshot of the array
System.out.println("\nIterating (snapshot):");
for (String fruit : list) {
System.out.println("Fruit: " + fruit);
// Modifications during iteration don't affect the current iteration
if (fruit.equals("Banana")) {
list.add("Date"); // This won't appear in current iteration
}
}
System.out.println("List after iteration: " + list);
// Concurrent modification example
demonstrateConcurrentBehavior();
}
private static void demonstrateConcurrentBehavior() {
System.out.println("\n=== Concurrent Behavior Demo ===");
CopyOnWriteArrayList<Integer> numbers = new CopyOnWriteArrayList<>();
for (int i = 1; i <= 5; i++) {
numbers.add(i);
}
// Reader thread
Thread reader = new Thread(() -> {
System.out.println("Reader starting iteration...");
for (Integer num : numbers) {
System.out.println("Reader sees: " + num);
try {
Thread.sleep(100); // Slow reader
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
System.out.println("Reader finished iteration");
});
// Writer thread
Thread writer = new Thread(() -> {
try {
Thread.sleep(150); // Let reader start first
System.out.println("Writer adding 100...");
numbers.add(100);
System.out.println("Writer added 100");
Thread.sleep(200);
System.out.println("Writer removing element at index 0...");
numbers.remove(0);
System.out.println("Writer removed first element");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
reader.start();
writer.start();
try {
reader.join();
writer.join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("Final list: " + numbers);
}
}
Performance Characteristics
import java.util.*;
import java.util.concurrent.*;
public class COWListCharacteristics {
public static void main(String[] args) throws Exception {
System.out.println("=== CopyOnWriteArrayList Characteristics ===\n");
// Memory overhead demonstration
CopyOnWriteArrayList<Integer> cowList = new CopyOnWriteArrayList<>();
ArrayList<Integer> arrayList = new ArrayList<>();
int elementCount = 1000;
// Measure memory usage
long memoryBefore = Runtime.getRuntime().totalMemory() -
Runtime.getRuntime().freeMemory();
for (int i = 0; i < elementCount; i++) {
cowList.add(i);
}
long memoryAfter = Runtime.getRuntime().totalMemory() -
Runtime.getRuntime().freeMemory();
System.out.printf("Memory used by CopyOnWriteArrayList with %d elements: %d bytes%n",
elementCount, memoryAfter - memoryBefore);
// Performance comparison for different operations
performanceComparison();
}
private static void performanceComparison() throws Exception {
final int OPERATIONS = 10000;
CopyOnWriteArrayList<Integer> cowList = new CopyOnWriteArrayList<>();
Vector<Integer> vector = new Vector<>();
List<Integer> syncList = Collections.synchronizedList(new ArrayList<>());
System.out.println("\n=== Read Performance (10,000 iterations) ===");
// Initialize lists
for (int i = 0; i < 1000; i++) {
cowList.add(i);
vector.add(i);
syncList.add(i);
}
// Read performance
long cowReadTime = measureReadPerformance(cowList, OPERATIONS);
long vectorReadTime = measureReadPerformance(vector, OPERATIONS);
long syncListReadTime = measureReadPerformance(syncList, OPERATIONS);
System.out.printf("CopyOnWriteArrayList read time: %d ms%n", cowReadTime);
System.out.printf("Vector read time: %d ms%n", vectorReadTime);
System.out.printf("SynchronizedList read time: %d ms%n", syncListReadTime);
System.out.println("\n=== Write Performance (1,000 additions) ===");
// Write performance
long cowWriteTime = measureWritePerformance(new CopyOnWriteArrayList<>(), 1000);
long vectorWriteTime = measureWritePerformance(new Vector<>(), 1000);
long syncListWriteTime = measureWritePerformance(
Collections.synchronizedList(new ArrayList<>()), 1000);
System.out.printf("CopyOnWriteArrayList write time: %d ms%n", cowWriteTime);
System.out.printf("Vector write time: %d ms%n", vectorWriteTime);
System.out.printf("SynchronizedList write time: %d ms%n", syncListWriteTime);
}
private static long measureReadPerformance(List<Integer> list, int operations) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < operations; i++) {
for (int j = 0; j < Math.min(100, list.size()); j++) {
list.get(j); // Read operation
}
}
return System.currentTimeMillis() - startTime;
}
private static long measureWritePerformance(List<Integer> list, int operations) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < operations; i++) {
list.add(i);
}
return System.currentTimeMillis() - startTime;
}
}
2. Read-Heavy Use Cases
Configuration Management System
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
public class ConfigurationManager {
private final CopyOnWriteArrayList<ConfigurationListener> listeners =
new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<ConfigEntry> configEntries =
new CopyOnWriteArrayList<>();
private final AtomicLong version = new AtomicLong(0);
public static class ConfigEntry {
private final String key;
private volatile String value;
private final long lastModified;
public ConfigEntry(String key, String value) {
this.key = key;
this.value = value;
this.lastModified = System.currentTimeMillis();
}
// Getters
public String getKey() { return key; }
public String getValue() { return value; }
public long getLastModified() { return lastModified; }
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return key + "=" + value;
}
}
public interface ConfigurationListener {
void onConfigChanged(String key, String oldValue, String newValue);
}
// Read-heavy operations
public String getConfigValue(String key) {
// Fast read without locking
for (ConfigEntry entry : configEntries) {
if (entry.getKey().equals(key)) {
return entry.getValue();
}
}
return null;
}
public List<ConfigEntry> getAllConfigs() {
// Returns a snapshot - safe for iteration
return new ArrayList<>(configEntries);
}
public boolean configExists(String key) {
// Fast existence check
for (ConfigEntry entry : configEntries) {
if (entry.getKey().equals(key)) {
return true;
}
}
return false;
}
public void forEachConfig(Consumer<ConfigEntry> action) {
// Safe iteration even if modifications occur
for (ConfigEntry entry : configEntries) {
action.accept(entry);
}
}
// Write operations (infrequent)
public void setConfigValue(String key, String value) {
// Find existing entry
ConfigEntry existingEntry = null;
for (ConfigEntry entry : configEntries) {
if (entry.getKey().equals(key)) {
existingEntry = entry;
break;
}
}
if (existingEntry != null) {
// Update existing - requires creating new array
String oldValue = existingEntry.getValue();
CopyOnWriteArrayList<ConfigEntry> newEntries =
new CopyOnWriteArrayList<>(configEntries);
for (ConfigEntry entry : newEntries) {
if (entry.getKey().equals(key)) {
entry.setValue(value);
break;
}
}
configEntries = newEntries;
notifyListeners(key, oldValue, value);
} else {
// Add new entry
ConfigEntry newEntry = new ConfigEntry(key, value);
configEntries.add(newEntry); // Creates new array internally
notifyListeners(key, null, value);
}
version.incrementAndGet();
}
public void addListener(ConfigurationListener listener) {
listeners.add(listener);
}
public void removeListener(ConfigurationListener listener) {
listeners.remove(listener);
}
private void notifyListeners(String key, String oldValue, String newValue) {
// Safe iteration over listeners
for (ConfigurationListener listener : listeners) {
try {
listener.onConfigChanged(key, oldValue, newValue);
} catch (Exception e) {
System.err.println("Error notifying listener: " + e.getMessage());
}
}
}
// Bulk read operations
public Map<String, String> getConfigAsMap() {
Map<String, String> configMap = new HashMap<>();
for (ConfigEntry entry : configEntries) {
configMap.put(entry.getKey(), entry.getValue());
}
return configMap;
}
public long getVersion() {
return version.get();
}
// Usage example
public static void main(String[] args) throws Exception {
ConfigurationManager configManager = new ConfigurationManager();
// Add some initial configuration
configManager.setConfigValue("database.url", "jdbc:mysql://localhost:3306/mydb");
configManager.setConfigValue("cache.enabled", "true");
configManager.setConfigValue("thread.pool.size", "10");
// Add listener
configManager.addListener((key, oldValue, newValue) -> {
System.out.printf("Config changed: %s from %s to %s%n",
key, oldValue, newValue);
});
// Simulate multiple readers
ExecutorService readerPool = Executors.newFixedThreadPool(10);
AtomicInteger readCount = new AtomicInteger(0);
for (int i = 0; i < 1000; i++) {
readerPool.submit(() -> {
// Various read operations
String dbUrl = configManager.getConfigValue("database.url");
boolean exists = configManager.configExists("cache.enabled");
Map<String, String> allConfigs = configManager.getConfigAsMap();
readCount.incrementAndGet();
// Simulate some processing
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// Simulate occasional writers
ExecutorService writerPool = Executors.newSingleThreadExecutor();
writerPool.submit(() -> {
try {
Thread.sleep(100);
configManager.setConfigValue("cache.enabled", "false");
Thread.sleep(200);
configManager.setConfigValue("new.setting", "value");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// Wait for completion
readerPool.shutdown();
writerPool.shutdown();
readerPool.awaitTermination(5, TimeUnit.SECONDS);
writerPool.awaitTermination(5, TimeUnit.SECONDS);
System.out.printf("Completed %d read operations%n", readCount.get());
System.out.println("Final config version: " + configManager.getVersion());
System.out.println("All configs: " + configManager.getConfigAsMap());
}
// Volatile field for thread-safe publication
private volatile CopyOnWriteArrayList<ConfigEntry> configEntries =
new CopyOnWriteArrayList<>();
}
Event Bus System
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
public class EventBus {
private final CopyOnWriteArrayList<EventHandler<?>> handlers =
new CopyOnWriteArrayList<>();
private final ExecutorService executor;
public EventBus() {
this.executor = Executors.newCachedThreadPool();
}
public EventBus(ExecutorService executor) {
this.executor = executor;
}
@SuppressWarnings("unchecked")
private static class EventHandler<T> {
private final Class<T> eventType;
private final Consumer<T> handler;
private final boolean async;
public EventHandler(Class<T> eventType, Consumer<T> handler, boolean async) {
this.eventType = eventType;
this.handler = handler;
this.async = async;
}
public boolean handles(Class<?> eventClass) {
return eventType.isAssignableFrom(eventClass);
}
public void handle(Object event) {
if (async) {
executor.submit(() -> handler.accept((T) event));
} else {
handler.accept((T) event);
}
}
}
// Read-heavy: checking if there are handlers for an event type
public <T> boolean hasHandlersFor(Class<T> eventType) {
for (EventHandler<?> handler : handlers) {
if (handler.handles(eventType)) {
return true;
}
}
return false;
}
// Read-heavy: get all handler types
public Set<Class<?>> getRegisteredEventTypes() {
Set<Class<?>> types = new HashSet<>();
for (EventHandler<?> handler : handlers) {
types.add(handler.eventType);
}
return types;
}
// Read-heavy: count handlers for specific event type
public <T> int getHandlerCount(Class<T> eventType) {
int count = 0;
for (EventHandler<?> handler : handlers) {
if (handler.handles(eventType)) {
count++;
}
}
return count;
}
// Write operation (infrequent) - register handler
public <T> void registerHandler(Class<T> eventType, Consumer<T> handler) {
registerHandler(eventType, handler, false);
}
public <T> void registerAsyncHandler(Class<T> eventType, Consumer<T> handler) {
registerHandler(eventType, handler, true);
}
private <T> void registerHandler(Class<T> eventType, Consumer<T> handler, boolean async) {
EventHandler<T> eventHandler = new EventHandler<>(eventType, handler, async);
handlers.add(eventHandler);
}
// Write operation (infrequent) - unregister handler
public <T> void unregisterHandler(Class<T> eventType, Consumer<T> handler) {
handlers.removeIf(h ->
h.eventType.equals(eventType) && h.handler == handler
);
}
// Read-heavy: publish event to all interested handlers
public void publish(Object event) {
Class<?> eventClass = event.getClass();
int handlerCount = 0;
// Iteration uses snapshot - safe even if handlers are added/removed during publish
for (EventHandler<?> handler : handlers) {
if (handler.handles(eventClass)) {
handler.handle(event);
handlerCount++;
}
}
if (handlerCount == 0) {
System.out.println("No handlers for event: " + event);
}
}
// Bulk operation: publish to multiple events
public void publishAll(Collection<?> events) {
for (Object event : events) {
publish(event);
}
}
public void shutdown() {
executor.shutdown();
}
// Demo events
static class UserLoginEvent {
final String username;
final long timestamp;
public UserLoginEvent(String username) {
this.username = username;
this.timestamp = System.currentTimeMillis();
}
@Override
public String toString() {
return "UserLoginEvent{username='" + username + "', timestamp=" + timestamp + "}";
}
}
static class OrderCreatedEvent {
final String orderId;
final double amount;
public OrderCreatedEvent(String orderId, double amount) {
this.orderId = orderId;
this.amount = amount;
}
@Override
public String toString() {
return "OrderCreatedEvent{orderId='" + orderId + "', amount=" + amount + "}";
}
}
static class SystemAlertEvent {
final String message;
final String level;
public SystemAlertEvent(String message, String level) {
this.message = message;
this.level = level;
}
@Override
public String toString() {
return "SystemAlertEvent{message='" + message + "', level='" + level + "'}";
}
}
public static void main(String[] args) throws Exception {
EventBus eventBus = new EventBus();
// Register handlers (infrequent writes)
eventBus.registerHandler(UserLoginEvent.class, event -> {
System.out.println("[LoginLogger] " + event);
});
eventBus.registerAsyncHandler(UserLoginEvent.class, event -> {
// Simulate slow processing
try {
Thread.sleep(100);
System.out.println("[LoginAnalytics] Processing: " + event);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
eventBus.registerHandler(OrderCreatedEvent.class, event -> {
System.out.println("[OrderProcessor] " + event);
});
eventBus.registerHandler(SystemAlertEvent.class, event -> {
System.out.println("[AlertSystem] " + event);
});
// Simulate read-heavy usage pattern
ExecutorService publisherPool = Executors.newFixedThreadPool(5);
AtomicInteger eventsPublished = new AtomicInteger(0);
// Multiple threads publishing events (reading handler list frequently)
for (int i = 0; i < 10; i++) {
final int threadId = i;
publisherPool.submit(() -> {
for (int j = 0; j < 100; j++) {
// Check if there are handlers before publishing (read operation)
if (eventBus.hasHandlersFor(UserLoginEvent.class)) {
eventBus.publish(new UserLoginEvent("user-" + threadId + "-" + j));
eventsPublished.incrementAndGet();
}
if (j % 10 == 0 && eventBus.hasHandlersFor(OrderCreatedEvent.class)) {
eventBus.publish(new OrderCreatedEvent("order-" + j, j * 10.0));
eventsPublished.incrementAndGet();
}
// Simulate some work
try {
Thread.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
// Occasionally add new handlers (write operations)
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.schedule(() -> {
System.out.println("Adding new alert handler dynamically");
eventBus.registerHandler(SystemAlertEvent.class, event -> {
System.out.println("[NewAlertHandler] " + event);
});
// Publish some alerts
eventBus.publish(new SystemAlertEvent("System load high", "WARNING"));
}, 1, TimeUnit.SECONDS);
// Let it run for a while
Thread.sleep(3000);
// Statistics
System.out.println("\n=== Event Bus Statistics ===");
System.out.println("Total events published: " + eventsPublished.get());
System.out.println("Registered event types: " + eventBus.getRegisteredEventTypes());
System.out.println("UserLoginEvent handlers: " +
eventBus.getHandlerCount(UserLoginEvent.class));
publisherPool.shutdown();
scheduler.shutdown();
eventBus.shutdown();
publisherPool.awaitTermination(5, TimeUnit.SECONDS);
System.out.println("Event bus demo completed");
}
}
3. Advanced Patterns and Best Practices
Snapshot-Based Reporting System
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.stream.*;
public class SnapshotReportingSystem {
private final CopyOnWriteArrayList<Transaction> transactions =
new CopyOnWriteArrayList<>();
private final AtomicLong totalProcessed = new AtomicLong(0);
public static class Transaction {
private final String id;
private final double amount;
private final long timestamp;
private final String type;
public Transaction(String id, double amount, String type) {
this.id = id;
this.amount = amount;
this.type = type;
this.timestamp = System.currentTimeMillis();
}
// Getters
public String getId() { return id; }
public double getAmount() { return amount; }
public long getTimestamp() { return timestamp; }
public String getType() { return type; }
@Override
public String toString() {
return String.format("Transaction{id=%s, amount=%.2f, type=%s}",
id, amount, type);
}
}
// Write operation - add transaction
public void addTransaction(Transaction transaction) {
transactions.add(transaction);
totalProcessed.incrementAndGet();
}
// Batch write operation
public void addTransactions(Collection<Transaction> newTransactions) {
transactions.addAll(newTransactions);
totalProcessed.addAndGet(newTransactions.size());
}
// Read operations - all use snapshot semantics
public Report generateCurrentReport() {
// Snapshot is taken at the beginning of iteration
List<Transaction> snapshot = new ArrayList<>(transactions);
double totalAmount = snapshot.stream()
.mapToDouble(Transaction::getAmount)
.sum();
double averageAmount = snapshot.stream()
.mapToDouble(Transaction::getAmount)
.average()
.orElse(0.0);
Map<String, Long> countByType = snapshot.stream()
.collect(Collectors.groupingBy(
Transaction::getType,
Collectors.counting()
));
Map<String, Double> amountByType = snapshot.stream()
.collect(Collectors.groupingBy(
Transaction::getType,
Collectors.summingDouble(Transaction::getAmount)
));
return new Report(snapshot.size(), totalAmount, averageAmount,
countByType, amountByType, System.currentTimeMillis());
}
public List<Transaction> getTransactionsByType(String type) {
return transactions.stream()
.filter(t -> t.getType().equals(type))
.collect(Collectors.toList());
}
public List<Transaction> getRecentTransactions(long sinceTimestamp) {
return transactions.stream()
.filter(t -> t.getTimestamp() >= sinceTimestamp)
.collect(Collectors.toList());
}
public Optional<Transaction> findTransaction(String id) {
return transactions.stream()
.filter(t -> t.getId().equals(id))
.findFirst();
}
public boolean containsTransaction(String id) {
return transactions.stream()
.anyMatch(t -> t.getId().equals(id));
}
public long getTotalProcessedCount() {
return totalProcessed.get();
}
public int getCurrentTransactionCount() {
return transactions.size();
}
public static class Report {
private final int transactionCount;
private final double totalAmount;
private final double averageAmount;
private final Map<String, Long> countByType;
private final Map<String, Double> amountByType;
private final long generatedAt;
public Report(int transactionCount, double totalAmount, double averageAmount,
Map<String, Long> countByType, Map<String, Double> amountByType,
long generatedAt) {
this.transactionCount = transactionCount;
this.totalAmount = totalAmount;
this.averageAmount = averageAmount;
this.countByType = countByType;
this.amountByType = amountByType;
this.generatedAt = generatedAt;
}
@Override
public String toString() {
return String.format(
"Report{count=%d, total=%.2f, avg=%.2f, types=%s, generated=%d}",
transactionCount, totalAmount, averageAmount, countByType, generatedAt
);
}
}
// Demo with concurrent access pattern
public static void main(String[] args) throws Exception {
SnapshotReportingSystem system = new SnapshotReportingSystem();
// Simulate transaction producers (writers)
ExecutorService producerPool = Executors.newFixedThreadPool(3);
ScheduledExecutorService producerScheduler = Executors.newScheduledThreadPool(1);
// Generate transactions continuously but slowly
producerScheduler.scheduleAtFixedRate(() -> {
String[] types = {"PAYMENT", "REFUND", "TRANSFER", "FEE"};
Random random = new Random();
for (int i = 0; i < 5; i++) {
String type = types[random.nextInt(types.length)];
double amount = 10 + random.nextDouble() * 1000;
String id = "txn-" + System.currentTimeMillis() + "-" + i;
system.addTransaction(new Transaction(id, amount, type));
}
}, 0, 100, TimeUnit.MILLISECONDS); // 10 transactions per 100ms
// Simulate report consumers (readers) - much more frequent
ExecutorService consumerPool = Executors.newFixedThreadPool(10);
AtomicInteger reportsGenerated = new AtomicInteger(0);
for (int i = 0; i < 10; i++) {
consumerPool.submit(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
// Generate various types of reports (read operations)
Report currentReport = system.generateCurrentReport();
reportsGenerated.incrementAndGet();
// Occasionally query specific data
if (Math.random() < 0.1) {
List<Transaction> payments = system.getTransactionsByType("PAYMENT");
System.out.printf("Found %d PAYMENT transactions%n", payments.size());
}
if (Math.random() < 0.05) {
long recentTime = System.currentTimeMillis() - 5000;
List<Transaction> recent = system.getRecentTransactions(recentTime);
System.out.printf("Found %d recent transactions%n", recent.size());
}
Thread.sleep(10); // Readers are very fast
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
// Run for a while
Thread.sleep(5000);
// Shutdown and statistics
producerScheduler.shutdown();
consumerPool.shutdownNow();
producerScheduler.awaitTermination(1, TimeUnit.SECONDS);
consumerPool.awaitTermination(1, TimeUnit.SECONDS);
System.out.println("\n=== Snapshot Reporting System Statistics ===");
System.out.println("Total transactions processed: " + system.getTotalProcessedCount());
System.out.println("Current transactions in system: " + system.getCurrentTransactionCount());
System.out.println("Reports generated: " + reportsGenerated.get());
// Final report
Report finalReport = system.generateCurrentReport();
System.out.println("Final report: " + finalReport);
// Demonstrate snapshot consistency
System.out.println("\n=== Snapshot Consistency Demo ===");
List<Transaction> snapshot1 = new ArrayList<>(system.transactions);
System.out.println("Snapshot 1 size: " + snapshot1.size());
// Add some transactions
system.addTransaction(new Transaction("new-1", 100, "PAYMENT"));
system.addTransaction(new Transaction("new-2", 200, "REFUND"));
List<Transaction> snapshot2 = new ArrayList<>(system.transactions);
System.out.println("Snapshot 2 size: " + snapshot2.size());
System.out.println("Snapshot 1 unchanged: " + snapshot1.size());
}
}
Memory-Efficient Patterns
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
public class MemoryEfficientCOWPatterns {
// Pattern 1: Lazy snapshot creation
public static class LazySnapshotList<T> {
private final CopyOnWriteArrayList<T> delegate = new CopyOnWriteArrayList<>();
private volatile List<T> lastSnapshot;
private volatile long lastSnapshotTime;
private final long snapshotTTL;
public LazySnapshotList(long snapshotTTL) {
this.snapshotTTL = snapshotTTL;
}
public List<T> getSnapshot() {
long currentTime = System.currentTimeMillis();
List<T> snapshot = lastSnapshot;
// Return cached snapshot if it's still fresh
if (snapshot != null && (currentTime - lastSnapshotTime) < snapshotTTL) {
return snapshot;
}
// Create new snapshot
snapshot = new ArrayList<>(delegate);
lastSnapshot = snapshot;
lastSnapshotTime = currentTime;
return snapshot;
}
public void add(T element) {
delegate.add(element);
// Invalidate cached snapshot on write
lastSnapshot = null;
}
public boolean remove(T element) {
boolean removed = delegate.remove(element);
if (removed) {
lastSnapshot = null;
}
return removed;
}
}
// Pattern 2: Batched writes to reduce copy overhead
public static class BatchedWriteList<T> {
private final CopyOnWriteArrayList<T> delegate = new CopyOnWriteArrayList<>();
private final List<T> writeBuffer = new ArrayList<>();
private final int batchSize;
private final Object writeLock = new Object();
public BatchedWriteList(int batchSize) {
this.batchSize = batchSize;
}
public void add(T element) {
synchronized (writeLock) {
writeBuffer.add(element);
if (writeBuffer.size() >= batchSize) {
flushBuffer();
}
}
}
public void flush() {
synchronized (writeLock) {
if (!writeBuffer.isEmpty()) {
flushBuffer();
}
}
}
private void flushBuffer() {
delegate.addAll(writeBuffer);
writeBuffer.clear();
}
// Read operations delegate directly
public boolean contains(T element) {
return delegate.contains(element) || writeBuffer.contains(element);
}
public int size() {
return delegate.size() + writeBuffer.size();
}
public List<T> getSnapshot() {
flush(); // Ensure all writes are included
return new ArrayList<>(delegate);
}
}
// Pattern 3: Size-limited COW list with eviction
public static class SizeBoundedCOWList<T> {
private final CopyOnWriteArrayList<T> delegate = new CopyOnWriteArrayList<>();
private final int maxSize;
private final Consumer<T> evictionHandler;
public SizeBoundedCOWList(int maxSize) {
this(maxSize, null);
}
public SizeBoundedCOWList(int maxSize, Consumer<T> evictionHandler) {
this.maxSize = maxSize;
this.evictionHandler = evictionHandler;
}
public void add(T element) {
delegate.add(element);
// Check if we need to evict
while (delegate.size() > maxSize) {
T removed = delegate.remove(0);
if (evictionHandler != null) {
evictionHandler.accept(removed);
}
}
}
// Delegate read operations
public boolean contains(T element) {
return delegate.contains(element);
}
public int size() {
return delegate.size();
}
public List<T> getSnapshot() {
return new ArrayList<>(delegate);
}
}
// Demo and benchmarking
public static void main(String[] args) throws Exception {
System.out.println("=== Memory-Efficient Patterns Demo ===\n");
// Test LazySnapshotList
LazySnapshotList<String> lazyList = new LazySnapshotList<>(1000); // 1 second TTL
testLazySnapshotPattern(lazyList);
// Test BatchedWriteList
BatchedWriteList<Integer> batchedList = new BatchedWriteList<>(10);
testBatchedWritePattern(batchedList);
// Test SizeBoundedCOWList
SizeBoundedCOWList<String> boundedList = new SizeBoundedCOWList<>(5,
item -> System.out.println("Evicted: " + item));
testBoundedPattern(boundedList);
// Memory usage comparison
compareMemoryUsage();
}
private static void testLazySnapshotPattern(LazySnapshotList<String> list) throws Exception {
System.out.println("=== Testing LazySnapshotList ===");
// Add some data
for (int i = 0; i < 5; i++) {
list.add("item-" + i);
}
// First snapshot - will create
List<String> snapshot1 = list.getSnapshot();
System.out.println("Snapshot 1: " + snapshot1);
// Immediate second snapshot - should reuse cached
List<String> snapshot2 = list.getSnapshot();
System.out.println("Snapshot 2 (cached): " + snapshot2);
System.out.println("Same instance: " + (snapshot1 == snapshot2));
// Wait for TTL to expire
Thread.sleep(1100);
// Third snapshot - should create new
List<String> snapshot3 = list.getSnapshot();
System.out.println("Snapshot 3 (new): " + snapshot3);
System.out.println("Different instance: " + (snapshot1 != snapshot3));
}
private static void testBatchedWritePattern(BatchedWriteList<Integer> list) throws Exception {
System.out.println("\n=== Testing BatchedWriteList ===");
ExecutorService executor = Executors.newFixedThreadPool(3);
// Multiple writers adding data
for (int i = 0; i < 3; i++) {
final int writerId = i;
executor.submit(() -> {
for (int j = 0; j < 15; j++) {
list.add(writerId * 100 + j);
try {
Thread.sleep(10);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.SECONDS);
// Force flush and get snapshot
list.flush();
List<Integer> snapshot = list.getSnapshot();
System.out.println("Final snapshot size: " + snapshot.size());
System.out.println("Sample data: " + snapshot.subList(0, Math.min(10, snapshot.size())));
}
private static void testBoundedPattern(SizeBoundedCOWList<String> list) {
System.out.println("\n=== Testing SizeBoundedCOWList ===");
for (int i = 0; i < 10; i++) {
list.add("data-" + i);
System.out.println("After adding data-" + i + ", size: " + list.size());
}
List<String> snapshot = list.getSnapshot();
System.out.println("Final snapshot: " + snapshot);
}
private static void compareMemoryUsage() {
System.out.println("\n=== Memory Usage Comparison ===");
int elementCount = 10000;
// Regular CopyOnWriteArrayList
long startMemory = getUsedMemory();
CopyOnWriteArrayList<Integer> regularList = new CopyOnWriteArrayList<>();
for (int i = 0; i < elementCount; i++) {
regularList.add(i);
}
long regularMemory = getUsedMemory() - startMemory;
// Batched CopyOnWriteArrayList
startMemory = getUsedMemory();
BatchedWriteList<Integer> batchedList = new BatchedWriteList<>(100);
for (int i = 0; i < elementCount; i++) {
batchedList.add(i);
}
batchedList.flush();
long batchedMemory = getUsedMemory() - startMemory;
System.out.printf("Regular COW list memory: %d bytes%n", regularMemory);
System.out.printf("Batched COW list memory: %d bytes%n", batchedMemory);
System.out.printf("Memory saving: %.1f%%%n",
(1 - (double)batchedMemory / regularMemory) * 100);
}
private static long getUsedMemory() {
Runtime runtime = Runtime.getRuntime();
return runtime.totalMemory() - runtime.freeMemory();
}
}
Summary
Key Benefits of CopyOnWriteArrayList:
- Thread-Safe Reads: No locking required for read operations
- Snapshot Consistency: Iterators see a consistent snapshot
- No ConcurrentModificationException: Safe for concurrent modification during iteration
- Simple Usage: No explicit synchronization needed
Ideal Use Cases:
- Configuration systems with infrequent updates
- Event listener lists in observer patterns
- Read-mostly caches
- Snapshot-based reporting systems
- Immutable data views
Performance Characteristics:
- Read Operations: O(1) - very fast, no locking
- Write Operations: O(n) - slow, involves array copying
- Memory Usage: Higher due to copy-on-write semantics
- Iteration: Fast and consistent
Best Practices:
- Use only for read-heavy workloads (reads >> writes)
- Prefer batch writes to reduce copy overhead
- Consider memory implications for large collections
- Use appropriate initial capacity if known
- Monitor write performance in production
When to Avoid:
- Write-heavy scenarios
- Memory-constrained environments
- Large collections with frequent modifications
- Real-time systems where write latency matters
CopyOnWriteArrayList is a specialized collection that excels in specific read-heavy, write-rarely scenarios, providing excellent read performance and thread safety at the cost of write performance and memory usage.