When building web applications, controllers often need to maintain shared state across multiple request handlers. A common pattern involves tracking metrics or caching temporary data that multiple methods must access.
The Static Field Approach
Using a static field creates a single instance shared by all controller instances and threads. While simple, this introduces concurrancy challenges:
// Shared state - all threads access the same map instance
private static final Map<Long, Integer> viewCountCache = new LinkedHashMap<>();
Thread Safety Considerations
The naive implementation suffers from race conditions. Consider this unsafe pattern:
// DANGEROUS: Not thread-safe
viewCountCache.putIfAbsent(newsId, baseCount);
Integer updated = viewCountCache.computeIfPresent(newsId, (k, v) -> {
viewCountCache.remove(k); // Potential race condition
return v + 1;
});
Multiple threads can corrupt the map state because LinkedHashMap isn't thread-safe. The remove() operation within computeIfPresent can cause ConcurrentModificationException or lost updates.
Robust Implementation with Concurrent Collections
A production-ready solution uses ConcurrentHashMap with atomic operations:
private static final ConcurrentHashMap<Long, AtomicInteger> newsViewTracker =
new ConcurrentHashMap<>();
private final ScheduledExecutorService persistenceScheduler =
Executors.newSingleThreadScheduledExecutor();
@PostConstruct
public void initializeMetricsCollector() {
persistenceScheduler.scheduleAtFixedRate(this::batchPersistViewCounts,
10, 10, TimeUnit.SECONDS);
}
private void batchPersistViewCounts() {
System.out.printf("[%s] Persisting metrics for %d news articles%n",
Thread.currentThread().getName(), newsViewTracker.size());
// Database batch update logic here
}
@GetMapping("/news/content")
public String displayNewsContent(@RequestParam long newsId, Model model) {
News story = newsService.fetchNewsById(newsId);
// Thread-safe increment using atomic operations
AtomicInteger counter = newsViewTracker.computeIfAbsent(newsId,
id -> new AtomicInteger(story.getBaselineViews()));
int realTimeViews = counter.incrementAndGet();
story.setViewCount(realTimeViews);
model.addAttribute("newsStory", story);
return "news/contentView";
}
Understanding Static vs Volatile
Static Modifier: Creates a class-level variable stored in the method area. All instances and threads share the same memory location. However, static provides no visibility or atomicity guarantees across threads.
Volatile Keyword: Ensures all threads read the latest value from main memory rather than CPU caches. Crucially, volatile does NOT provide atomicity for compound operations like check-then-act patterns.
For the use case above, neither static nor volatile alone suffices. The map must be:
staticto ensure single instance- A concurrent collection like
ConcurrentHashMapto manage thread-safe access - Use atomic classes (
AtomicInteger) for fine-grained synchronization
Best Practices
- Prefer dependency injection of stateful components over static feilds
- Use
ConcurrentHashMapinstead of synchronizedLinkedHashMap - For ordered access, consider
ConcurrentSkipListMap - Always clean up background threads in
@PreDestroy - Consider using Spring's
@Scheduledfor periodic tasks instead of manual executor management
@PreDestroy
public void cleanup() {
persistenceScheduler.shutdown();
try {
if (!persistenceScheduler.awaitTermination(5, TimeUnit.SECONDS)) {
persistenceScheduler.shutdownNow();
}
} catch (InterruptedException e) {
persistenceScheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}