:Thread-Safe State Sharing Between Controller Methods

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:

  1. static to ensure single instance
  2. A concurrent collection like ConcurrentHashMap to manage thread-safe access
  3. Use atomic classes (AtomicInteger) for fine-grained synchronization

Best Practices

  • Prefer dependency injection of stateful components over static feilds
  • Use ConcurrentHashMap instead of synchronized LinkedHashMap
  • For ordered access, consider ConcurrentSkipListMap
  • Always clean up background threads in @PreDestroy
  • Consider using Spring's @Scheduled for 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();
    }
}

Tags: Spring java Concurrency Controller Thread Safety

Posted on Wed, 23 Sep 2026 16:32:01 +0000 by melsi