From 23f22a37df119270dc7cca52493ee82a1c8d4dad Mon Sep 17 00:00:00 2001 From: CMD137 <2992456841@qq.com> Date: Thu, 22 Jan 2026 23:34:31 +0800 Subject: [PATCH 1/7] Add Thread-Specific Storage design pattern --- pom.xml | 1 + thread-specific-storage/README.md | 222 ++++++++++++++++++ .../etc/ThreadSpecificStorageUML.png | Bin 0 -> 28087 bytes .../etc/thread-specific-storage.urm.puml | 37 +++ thread-specific-storage/pom.xml | 83 +++++++ .../iluwatar/threadspecificstorage/App.java | 40 ++++ .../threadspecificstorage/RequestHandler.java | 58 +++++ .../threadspecificstorage/UserContext.java | 15 ++ .../UserContextProxy.java | 35 +++ .../threadspecificstorage/AppTest.java | 38 +++ .../RequestHandlerTest.java | 34 +++ .../UserContextProxyTest.java | 68 ++++++ .../UserContextTest.java | 51 ++++ 13 files changed, 682 insertions(+) create mode 100644 thread-specific-storage/README.md create mode 100644 thread-specific-storage/etc/ThreadSpecificStorageUML.png create mode 100644 thread-specific-storage/etc/thread-specific-storage.urm.puml create mode 100644 thread-specific-storage/pom.xml create mode 100644 thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java create mode 100644 thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java create mode 100644 thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContext.java create mode 100644 thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java create mode 100644 thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java create mode 100644 thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java create mode 100644 thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextProxyTest.java create mode 100644 thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextTest.java diff --git a/pom.xml b/pom.xml index 8337c97966da..05884a780537 100644 --- a/pom.xml +++ b/pom.xml @@ -232,6 +232,7 @@ template-method templateview thread-pool-executor + thread-specific-storage throttling tolerant-reader trampoline diff --git a/thread-specific-storage/README.md b/thread-specific-storage/README.md new file mode 100644 index 000000000000..a08b1f6af199 --- /dev/null +++ b/thread-specific-storage/README.md @@ -0,0 +1,222 @@ +--- +title: "Thread-Specific Storage Pattern in Java: Isolated Thread-Local Data Management" +shortTitle: Thread-Specific Storage +description: "Learn the Thread-Specific Storage pattern in Java with practical examples, class +diagrams, and implementation details. Understand how to manage thread-local data efficiently, +improving concurrency and avoiding synchronization issues." +category: Concurrency +language: en +tag: + +- Concurrency +- Multithreading +- Thread Safety +- Data Isolation +- Memory Management + +--- + +## Intent of Thread-Specific Storage Design Pattern + +The Thread-Specific Storage pattern ensures that each thread has its own isolated instance of shared data, +preventing concurrency issues by eliminating the need for synchronization. It achieves this by using +ThreadLocal variables to store data that is specific to each thread. + +## Detailed Explanation of Thread-Specific Storage Pattern with Real-World Examples + +### Real-world example + +> Think of a customer service center where each agent has their own notepad to record information +> about the customer they're currently helping. Even if multiple agents are helping customers +> simultaneously, each agent's notes are completely separate from the others'. When an agent finishes +> with one customer and moves to the next, they start with a fresh notepad. This approach eliminates +> the need for agents to coordinate their note-taking, as each agent's notes are private to their +> current customer interaction. + +### In plain words + +> Thread-Specific Storage provides each thread with its own private copy of data, isolating thread +> interactions and avoiding the need for synchronization mechanisms. + +### Wikipedia says + +> Thread-local storage (TLS) is a computer programming method that uses static or global memory +> local to a thread. TLS is a common technique for avoiding race conditions when multiple threads +> need to access the same data. Each thread has its own copy of the data, so there is no need for +> synchronization. + +### Class diagram + +![Thread-Specific Storage diagram](./etc/ThreadSpecificStorageUML.png) + +## Programmatic Example of Thread-Specific Storage Pattern in Java + +Imagine a web application that needs to track the current user's context across different stages of request processing. + +Each request is handled by a separate thread, and we need to maintain user-specific information without +synchronization overhead. + +The Thread-Specific Storage pattern efficiently handles this by providing each thread with its own copy +of the user context data. + +```java +@Slf4j +public class APP { + public static void main(String[] args) { + // Initialize components + UserContextProxy proxy = new UserContextProxy(); + + // Simulate concurrent requests from multiple users + for (int i = 1; i <= 5; i++) { + // Simulate tokens for different users + String token = "token::" + (i % 3 + 1); // 3 distinct users + + new Thread(() -> { + // Simulate request processing flow + RequestHandler handler = new RequestHandler(proxy, token); + handler.process(); + }).start(); + + // Slightly stagger request times + try { + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} +``` + +Here's how the request handler processes each request: + +```java +@Slf4j +public class RequestHandler { + private final UserContextProxy contextProxy; + private final String token; + + public void process() { + LOGGER.info("Start handling request with token: {}", token); + + try { + // Step 1: Parse token to get userId + Long userId = parseToken(token); + + // Step 2: Save userId in ThreadLocal storage + contextProxy.set(new UserContext(userId)); + + // Simulate delay between stages of request handling + Thread.sleep(200); + + // Step 3: Retrieve userId later in the request flow + Long retrievedId = contextProxy.get().getUserId(); + Random random = new Random(); + String accountInfo = retrievedId + "'s account: " + random.nextInt(400); + LOGGER.info(accountInfo); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + // Step 4: Clear ThreadLocal to avoid potential memory leaks + contextProxy.clear(); + } + } + + // ... parseToken method implementation +} +``` + +The UserContextProxy acts as a thread-safe accessor to the ThreadLocal storage: + +```java +public class UserContextProxy { + // Underlying TSObjectCollection (ThreadLocalMap) managed by JVM. + // This ThreadLocal acts as the Key for the map. + private static final ThreadLocal userContextHolder = new ThreadLocal(); + + /** Set UserContext for the current thread */ + public static void set(UserContext context) { + userContextHolder.set(context); + } + + /** Get UserContext for the current thread */ + public static UserContext get() { + return userContextHolder.get(); + } + + /** Clear UserContext to prevent potential memory leaks */ + public static void clear() { + userContextHolder.remove(); + } +} +``` + +Here's a sample console output: + +``` +Start handling request with token: token::1 +Start handling request with token: token::2 +Start handling request with token: token::3 +Start handling request with token: token::1 +1's account: 234 +Start handling request with token: token::2 +2's account: 157 +3's account: 89 +1's account: 342 +2's account: 76 +``` + +**Note:** Since this example demonstrates concurrent thread execution, **the actual output may vary between runs**. The order of execution and timing can differ due to thread scheduling, system load, and other factors that affect concurrent processing. However, each thread will correctly maintain its own user context without interference from other threads. + +## When to Use the Thread-Specific Storage Pattern in Java + +* When you need to maintain per-thread state without synchronization overhead +* For applications that process requests in multiple stages and need to share data across those stages +* To avoid concurrency issues when working with non-thread-safe objects like SimpleDateFormat +* When implementing logging or security context that needs to be accessible throughout a request processing +* To maintain thread-specific caches or counters without risk of data corruption + +## Thread-Specific Storage Pattern Java Tutorial + +* [Thread-Specific Storage Pattern Tutorial (Baeldung)](https://www.baeldung.com/java-threadlocal) + +## Real-World Applications of Thread-Specific Storage Pattern in Java + +* Servlet containers use ThreadLocal to maintain the current request and response objects +* Spring Framework uses ThreadLocal for managing transaction contexts and security contexts +* Logging frameworks use ThreadLocal to associate log messages with the current thread's context +* Database connection management where each thread needs its own connection or maintains a connection pool per thread +* User session management in web applications where session data is accessed across multiple layers + +## Benefits and Trade-offs of Thread-Specific Storage Pattern + +### Benefits + +* Eliminates the need for synchronization mechanisms, improving performance +* Provides complete isolation of data between threads, preventing concurrency issues +* Simplifies code by removing the need to pass context objects through method parameters +* Enables safe use of non-thread-safe classes in multi-threaded environments +* Reduces object creation overhead by reusing thread-local instances + +### Trade-offs + +* Can lead to increased memory consumption as each thread maintains its own copy of data +* Requires careful cleanup to prevent memory leaks, especially in long-running applications +* May complicate debugging as data is not visible across threads +* Can cause issues in environments with thread pooling where threads are reused (data from previous tasks may persist) + +## Related Java Design Patterns + +* [Context Object Pattern](https://java-design-patterns.com/patterns/context-object/): Encapsulates + request-specific information into a context object that can be passed between components. +* [Thread Pool Pattern](https://java-design-patterns.com/patterns/thread-pool-executor/): + Maintains a pool of worker threads to execute tasks concurrently, optimizing resource usage. +* [Singleton Pattern](https://java-design-patterns.com/patterns/singleton/): Ensures a class has only + one instance and provides global access to it, similar to how ThreadLocal provides per-thread access. + +## References and Credits + +* [Pattern-Oriented Software Architecture, Volume 2: Patterns for Concurrent and Networked Objects](https://www.amazon.com/Pattern-Oriented-Software-Architecture-Concurrent-Networked/dp/0471606952) +* [Java Documentation for ThreadLocal](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ThreadLocal.html) +* [Java Concurrency in Practice](https://jcip.net/) by Brian Goetz diff --git a/thread-specific-storage/etc/ThreadSpecificStorageUML.png b/thread-specific-storage/etc/ThreadSpecificStorageUML.png new file mode 100644 index 0000000000000000000000000000000000000000..1d2e0d27d7702005daeb4607bcec352f8feddb5e GIT binary patch literal 28087 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 miidQK2C{%#cZrJhJMovMp^)n?|%TN#?Fxd literal 0 HcmV?d00001 diff --git a/thread-specific-storage/etc/thread-specific-storage.urm.puml b/thread-specific-storage/etc/thread-specific-storage.urm.puml new file mode 100644 index 000000000000..8b95fce260de --- /dev/null +++ b/thread-specific-storage/etc/thread-specific-storage.urm.puml @@ -0,0 +1,37 @@ +@startuml +!theme plain +top to bottom direction +class UserContext { + - userId : Long + + UserContext(userId : Long) + + getUserId() : Long + + setUserId(userId : Long) : void +} +class UserContextProxy { + - {static} userContextHolder : ThreadLocal + + {static} set(context : UserContext) : void + + {static} get() : UserContext + + {static} clear() : void +} +class RequestHandler { + - contextProxy : UserContextProxy + - token : String + + RequestHandler(contextProxy : UserContextProxy, token : String) + + process() : void + - parseToken(token : String) : Long +} +class APP { + + {static} main(args : String[]) : void +} +class Thread { + + start() : void +} + +UserContextProxy ..> UserContext : manages +RequestHandler ..> UserContextProxy : uses +RequestHandler ..> UserContext : creates +APP ..> UserContextProxy : creates +APP ..> RequestHandler : creates +APP ..> Thread : starts +Thread ..> RequestHandler : executes +@enduml \ No newline at end of file diff --git a/thread-specific-storage/pom.xml b/thread-specific-storage/pom.xml new file mode 100644 index 000000000000..6e54c3fc406f --- /dev/null +++ b/thread-specific-storage/pom.xml @@ -0,0 +1,83 @@ + + + + + 4.0.0 + + + com.iluwatar + java-design-patterns + 1.26.0-SNAPSHOT + + + thread-specific-storage + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.junit.jupiter + junit-jupiter-engine + test + + + org.mockito + mockito-core + test + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + + + + + + com.iluwatar.threadspecificstorage.App + + + + + + + + + diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java new file mode 100644 index 000000000000..882852ef4c49 --- /dev/null +++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java @@ -0,0 +1,40 @@ +package com.iluwatar.threadspecificstorage; + +/** + * Application entry point demonstrating the Thread-Specific Storage pattern. + * + *

This example simulates concurrent request processing for multiple users. + * Each request carries a user token, and user-specific context is managed + * transparently using thread-specific storage.

+ */ +public class App { + + /** + * Runs the Thread-Specific Storage pattern demonstration. + * + * @param args command-line arguments (not used) + */ + public static void main(String[] args) { + // Initialize components + UserContextProxy proxy = new UserContextProxy(); + + // Simulate concurrent requests from multiple users + for (int i = 1; i <= 5; i++) { + // Simulate tokens for different users + String token = "token::" + (i % 3 + 1); // 3 distinct users + + new Thread(() -> { + // Simulate request processing flow + RequestHandler handler = new RequestHandler(proxy, token); + handler.process(); + }).start(); + + // Slightly stagger request times + try { + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java new file mode 100644 index 000000000000..6be42db5f62a --- /dev/null +++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java @@ -0,0 +1,58 @@ +package com.iluwatar.threadspecificstorage; + +import java.util.Random; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * Application Thread + * + *

Each instance simulates a request-processing thread that uses + * the Thread-Specific Object Proxy to access Thread-Specific Object. + */ +@AllArgsConstructor +@Slf4j +public class RequestHandler { + private final UserContextProxy contextProxy; + private final String token; + + /** + * Simulated business process: + * 1. Parse userId from token ("Token::userId"). + * 2. Store userId in thread-local storage. + * 3. Later, retrieve userId and use it for business logic. + * 4. Finally, clear thread-local to prevent memory leak. + */ + public void process() { + LOGGER.info("Start handling request with token: {}", token); + + try { + // Step 1: Parse token to get userId + Long userId = parseToken(token); + + // Step 2: Save userId in ThreadLocal storage + contextProxy.set(new UserContext(userId)); + + // Simulate delay between stages of request handling + Thread.sleep(200); + + // Step 3: Retrieve userId later in the request flow + Long retrievedId = contextProxy.get().getUserId(); + Random random = new Random(); + String accountInfo = retrievedId + "'s account: " + random.nextInt(400); + LOGGER.info(accountInfo); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + // Step 4: Clear ThreadLocal to avoid potential memory leaks + contextProxy.clear(); + } + } + + private Long parseToken(String token) { + // token format: "Token::1234" + String[] parts = token.split("::"); + return (parts.length == 2) ? Long.parseLong(parts[1]) : -1L; + } +} diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContext.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContext.java new file mode 100644 index 000000000000..51399a1a0c72 --- /dev/null +++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContext.java @@ -0,0 +1,15 @@ +package com.iluwatar.threadspecificstorage; + +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * Thread-Specific Object + * + *

Provides a service or data that is only accessible via a particular thread. + */ +@Data +@AllArgsConstructor +public class UserContext { + private Long userId; +} diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java new file mode 100644 index 000000000000..ca5e15e93919 --- /dev/null +++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java @@ -0,0 +1,35 @@ +package com.iluwatar.threadspecificstorage; + +/** + * Thread-Specific Object Proxy + * + *

The Thread-Specific Object Proxy acts as an intermediary, + * enabling application thread to access and manipulate thread-specific objects simply and securely. + */ +public class UserContextProxy { + /** + * Underlying TSObjectCollection (ThreadLocalMap) managed by JVM.This ThreadLocal acts as the Key for the map.So That there is also no key factory. + */ + private static final ThreadLocal userContextHolder = new ThreadLocal(); + + /** + * Set UserContext for the current thread. + */ + public static void set(UserContext context) { + userContextHolder.set(context); + } + + /** + * Get UserContext for the current thread. + */ + public static UserContext get() { + return userContextHolder.get(); + } + + /** + * Clear UserContext to prevent potential memory leaks. + */ + public static void clear() { + userContextHolder.remove(); + } +} diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java new file mode 100644 index 000000000000..0a01ee3be2ba --- /dev/null +++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java @@ -0,0 +1,38 @@ +package com.iluwatar.threadspecificstorage; + +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Tests for APP class + */ +class AppTest { + @Test + void testMainMethod() { + // Capture system output + ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + System.setOut(new PrintStream(outContent)); + + // Run the main method + App.main(new String[]{}); + + // Give some time for threads to execute + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // Verify output contains expected log messages + String output = outContent.toString(); + assertTrue(output.contains("Start handling request with token"), + "Should contain request handling start messages"); + + // Restore system output + System.setOut(System.out); + } +} \ No newline at end of file diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java new file mode 100644 index 000000000000..6afbaf7f0178 --- /dev/null +++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java @@ -0,0 +1,34 @@ +package com.iluwatar.threadspecificstorage; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +class RequestHandlerTest { + + @Test + void process_shouldStoreAndClearUserContext() { + // Given - a real UserContextProxy + UserContextProxy proxy = new UserContextProxy(); + RequestHandler handler = new RequestHandler(proxy, "token::123"); + + // When - process the request + handler.process(); + + // Then - after processing, ThreadLocal should be cleared + assertNull(proxy.get(), "ThreadLocal should be cleared after process()"); + } + + @Test + void process_withInvalidToken_shouldSetUserIdToMinusOne() { + // Given - a real UserContextProxy + UserContextProxy proxy = new UserContextProxy(); + RequestHandler handler = new RequestHandler(proxy, "invalid-token"); + + // When - process the request + handler.process(); + + // Then - after processing, ThreadLocal should be cleared + assertNull(proxy.get(), "ThreadLocal should be cleared even for invalid token"); + } +} diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextProxyTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextProxyTest.java new file mode 100644 index 000000000000..56eb44ad4d15 --- /dev/null +++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextProxyTest.java @@ -0,0 +1,68 @@ +package com.iluwatar.threadspecificstorage; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for UserContextProxy class + */ +class UserContextProxyTest { + + private UserContext userContext; + + @BeforeEach + void setUp() { + userContext = new UserContext(123L); + } + + @AfterEach + void tearDown() { + UserContextProxy.clear(); + } + + @Test + void testSetAndGetContext() { + UserContextProxy.set(userContext); + UserContext retrievedContext = UserContextProxy.get(); + assertNotNull(retrievedContext, "Retrieved context should not be null"); + assertEquals(userContext.getUserId(), retrievedContext.getUserId(), + "Retrieved context should have the same userId"); + } + + @Test + void testGetContextWhenNotSet() { + UserContext retrievedContext = UserContextProxy.get(); + assertNull(retrievedContext, "Context should be null when not set"); + } + + @Test + void testClearContext() { + UserContextProxy.set(userContext); + UserContextProxy.clear(); + UserContext retrievedContext = UserContextProxy.get(); + assertNull(retrievedContext, "Context should be null after clearing"); + } + + @Test + void testThreadIsolation() throws InterruptedException { + UserContext context1 = new UserContext(123L); + UserContext context2 = new UserContext(456L); + UserContextProxy.set(context1); + // Create another thread to set different context + Thread thread = new Thread(() -> { + UserContextProxy.set(context2); + UserContext threadContext = UserContextProxy.get(); + assertNotNull(threadContext); + assertEquals(456L, threadContext.getUserId()); + }); + thread.start(); + thread.join(); + // Main thread context should remain unchanged + UserContext mainThreadContext = UserContextProxy.get(); + assertNotNull(mainThreadContext); + assertEquals(123L, mainThreadContext.getUserId()); + } +} \ No newline at end of file diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextTest.java new file mode 100644 index 000000000000..636e8bfcdc2a --- /dev/null +++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextTest.java @@ -0,0 +1,51 @@ +package com.iluwatar.threadspecificstorage; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for UserContext class + */ +class UserContextTest { + + @Test + void testConstructorAndGetUserId() { + Long userId = 123L; + UserContext context = new UserContext(userId); + + assertEquals(userId, context.getUserId(), "UserId should match the one provided in constructor"); + } + + @Test + void testSetUserId() { + UserContext context = new UserContext(123L); + Long newUserId = 456L; + + context.setUserId(newUserId); + + assertEquals(newUserId, context.getUserId(), "UserId should be updated"); + } + + @Test + void testToString() { + Long userId = 123L; + UserContext context = new UserContext(userId); + + String expected = "UserContext(userId=" + userId + ")"; + assertEquals(expected, context.toString(), "toString should return expected format"); + } + + @Test + void testEqualsAndHashCode() { + Long userId = 123L; + UserContext context1 = new UserContext(userId); + UserContext context2 = new UserContext(userId); + UserContext context3 = new UserContext(456L); + + assertEquals(context1, context2, "Objects with same userId should be equal"); + assertEquals(context1.hashCode(), context2.hashCode(), "Objects with same userId should have same hashCode"); + + assertNotEquals(context1, context3, "Objects with different userId should not be equal"); + assertNotEquals(context1.hashCode(), context3.hashCode(), "Objects with different userId should have different hashCode"); + } +} \ No newline at end of file From 5d8581ac319bd95311148b25aa53ab62fa88c1cd Mon Sep 17 00:00:00 2001 From: CMD137 <2992456841@qq.com> Date: Fri, 23 Jan 2026 00:42:25 +0800 Subject: [PATCH 2/7] fixed:pom.xml Run 'mvn spotless:apply' to fix these violations. --- pom.xml | 2 +- .../iluwatar/threadspecificstorage/App.java | 17 +++++------ .../threadspecificstorage/RequestHandler.java | 12 ++++---- .../UserContextProxy.java | 19 +++++-------- .../threadspecificstorage/AppTest.java | 16 +++++------ .../RequestHandlerTest.java | 4 +-- .../UserContextProxyTest.java | 28 ++++++++++--------- .../UserContextTest.java | 22 +++++++++------ 8 files changed, 60 insertions(+), 60 deletions(-) diff --git a/pom.xml b/pom.xml index 05884a780537..92b5da5460ce 100644 --- a/pom.xml +++ b/pom.xml @@ -231,7 +231,7 @@ table-module template-method templateview - thread-pool-executor + thread-pool-executor thread-specific-storage throttling tolerant-reader diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java index 882852ef4c49..8d784787eede 100644 --- a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java +++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java @@ -3,9 +3,8 @@ /** * Application entry point demonstrating the Thread-Specific Storage pattern. * - *

This example simulates concurrent request processing for multiple users. - * Each request carries a user token, and user-specific context is managed - * transparently using thread-specific storage.

+ *

This example simulates concurrent request processing for multiple users. Each request carries + * a user token, and user-specific context is managed transparently using thread-specific storage. */ public class App { @@ -23,11 +22,13 @@ public static void main(String[] args) { // Simulate tokens for different users String token = "token::" + (i % 3 + 1); // 3 distinct users - new Thread(() -> { - // Simulate request processing flow - RequestHandler handler = new RequestHandler(proxy, token); - handler.process(); - }).start(); + new Thread( + () -> { + // Simulate request processing flow + RequestHandler handler = new RequestHandler(proxy, token); + handler.process(); + }) + .start(); // Slightly stagger request times try { diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java index 6be42db5f62a..eca0001a0e51 100644 --- a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java +++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java @@ -7,8 +7,8 @@ /** * Application Thread * - *

Each instance simulates a request-processing thread that uses - * the Thread-Specific Object Proxy to access Thread-Specific Object. + *

Each instance simulates a request-processing thread that uses the Thread-Specific Object Proxy + * to access Thread-Specific Object. */ @AllArgsConstructor @Slf4j @@ -17,11 +17,9 @@ public class RequestHandler { private final String token; /** - * Simulated business process: - * 1. Parse userId from token ("Token::userId"). - * 2. Store userId in thread-local storage. - * 3. Later, retrieve userId and use it for business logic. - * 4. Finally, clear thread-local to prevent memory leak. + * Simulated business process: 1. Parse userId from token ("Token::userId"). 2. Store userId in + * thread-local storage. 3. Later, retrieve userId and use it for business logic. 4. Finally, + * clear thread-local to prevent memory leak. */ public void process() { LOGGER.info("Start handling request with token: {}", token); diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java index ca5e15e93919..998fc514bbf4 100644 --- a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java +++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java @@ -3,32 +3,27 @@ /** * Thread-Specific Object Proxy * - *

The Thread-Specific Object Proxy acts as an intermediary, - * enabling application thread to access and manipulate thread-specific objects simply and securely. + *

The Thread-Specific Object Proxy acts as an intermediary, enabling application thread to + * access and manipulate thread-specific objects simply and securely. */ public class UserContextProxy { /** - * Underlying TSObjectCollection (ThreadLocalMap) managed by JVM.This ThreadLocal acts as the Key for the map.So That there is also no key factory. + * Underlying TSObjectCollection (ThreadLocalMap) managed by JVM.This ThreadLocal acts as the Key + * for the map.So That there is also no key factory. */ private static final ThreadLocal userContextHolder = new ThreadLocal(); - /** - * Set UserContext for the current thread. - */ + /** Set UserContext for the current thread. */ public static void set(UserContext context) { userContextHolder.set(context); } - /** - * Get UserContext for the current thread. - */ + /** Get UserContext for the current thread. */ public static UserContext get() { return userContextHolder.get(); } - /** - * Clear UserContext to prevent potential memory leaks. - */ + /** Clear UserContext to prevent potential memory leaks. */ public static void clear() { userContextHolder.remove(); } diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java index 0a01ee3be2ba..143c57541b49 100644 --- a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java +++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java @@ -1,15 +1,12 @@ package com.iluwatar.threadspecificstorage; -import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; import java.io.PrintStream; +import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertTrue; - -/** - * Tests for APP class - */ +/** Tests for APP class */ class AppTest { @Test void testMainMethod() { @@ -18,7 +15,7 @@ void testMainMethod() { System.setOut(new PrintStream(outContent)); // Run the main method - App.main(new String[]{}); + App.main(new String[] {}); // Give some time for threads to execute try { @@ -29,10 +26,11 @@ void testMainMethod() { // Verify output contains expected log messages String output = outContent.toString(); - assertTrue(output.contains("Start handling request with token"), + assertTrue( + output.contains("Start handling request with token"), "Should contain request handling start messages"); // Restore system output System.setOut(System.out); } -} \ No newline at end of file +} diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java index 6afbaf7f0178..e58cbb4a5661 100644 --- a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java +++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java @@ -1,9 +1,9 @@ package com.iluwatar.threadspecificstorage; -import org.junit.jupiter.api.Test; - import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + class RequestHandlerTest { @Test diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextProxyTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextProxyTest.java index 56eb44ad4d15..fd8cfbd6b7aa 100644 --- a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextProxyTest.java +++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextProxyTest.java @@ -1,14 +1,12 @@ package com.iluwatar.threadspecificstorage; +import static org.junit.jupiter.api.Assertions.*; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; - -/** - * Tests for UserContextProxy class - */ +/** Tests for UserContextProxy class */ class UserContextProxyTest { private UserContext userContext; @@ -28,7 +26,9 @@ void testSetAndGetContext() { UserContextProxy.set(userContext); UserContext retrievedContext = UserContextProxy.get(); assertNotNull(retrievedContext, "Retrieved context should not be null"); - assertEquals(userContext.getUserId(), retrievedContext.getUserId(), + assertEquals( + userContext.getUserId(), + retrievedContext.getUserId(), "Retrieved context should have the same userId"); } @@ -52,12 +52,14 @@ void testThreadIsolation() throws InterruptedException { UserContext context2 = new UserContext(456L); UserContextProxy.set(context1); // Create another thread to set different context - Thread thread = new Thread(() -> { - UserContextProxy.set(context2); - UserContext threadContext = UserContextProxy.get(); - assertNotNull(threadContext); - assertEquals(456L, threadContext.getUserId()); - }); + Thread thread = + new Thread( + () -> { + UserContextProxy.set(context2); + UserContext threadContext = UserContextProxy.get(); + assertNotNull(threadContext); + assertEquals(456L, threadContext.getUserId()); + }); thread.start(); thread.join(); // Main thread context should remain unchanged @@ -65,4 +67,4 @@ void testThreadIsolation() throws InterruptedException { assertNotNull(mainThreadContext); assertEquals(123L, mainThreadContext.getUserId()); } -} \ No newline at end of file +} diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextTest.java index 636e8bfcdc2a..3a7864c9e92e 100644 --- a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextTest.java +++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/UserContextTest.java @@ -1,11 +1,10 @@ package com.iluwatar.threadspecificstorage; -import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; -/** - * Tests for UserContext class - */ +import org.junit.jupiter.api.Test; + +/** Tests for UserContext class */ class UserContextTest { @Test @@ -13,7 +12,8 @@ void testConstructorAndGetUserId() { Long userId = 123L; UserContext context = new UserContext(userId); - assertEquals(userId, context.getUserId(), "UserId should match the one provided in constructor"); + assertEquals( + userId, context.getUserId(), "UserId should match the one provided in constructor"); } @Test @@ -43,9 +43,15 @@ void testEqualsAndHashCode() { UserContext context3 = new UserContext(456L); assertEquals(context1, context2, "Objects with same userId should be equal"); - assertEquals(context1.hashCode(), context2.hashCode(), "Objects with same userId should have same hashCode"); + assertEquals( + context1.hashCode(), + context2.hashCode(), + "Objects with same userId should have same hashCode"); assertNotEquals(context1, context3, "Objects with different userId should not be equal"); - assertNotEquals(context1.hashCode(), context3.hashCode(), "Objects with different userId should have different hashCode"); + assertNotEquals( + context1.hashCode(), + context3.hashCode(), + "Objects with different userId should have different hashCode"); } -} \ No newline at end of file +} From 34cc03f6bb576f4cd466743f05c0f94417d122bb Mon Sep 17 00:00:00 2001 From: CMD137 <2992456841@qq.com> Date: Fri, 23 Jan 2026 01:30:20 +0800 Subject: [PATCH 3/7] fix: address Sonar security hotspots for Random usage and e.printStackTrace() --- .../main/java/com/iluwatar/threadspecificstorage/App.java | 5 ++++- .../com/iluwatar/threadspecificstorage/RequestHandler.java | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java index 8d784787eede..2de8c837dd90 100644 --- a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java +++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java @@ -1,11 +1,14 @@ package com.iluwatar.threadspecificstorage; +import lombok.extern.slf4j.Slf4j; + /** * Application entry point demonstrating the Thread-Specific Storage pattern. * *

This example simulates concurrent request processing for multiple users. Each request carries * a user token, and user-specific context is managed transparently using thread-specific storage. */ +@Slf4j public class App { /** @@ -34,7 +37,7 @@ public static void main(String[] args) { try { Thread.sleep(50); } catch (InterruptedException e) { - e.printStackTrace(); + LOGGER.warn("Sleep interrupted", e); } } } diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java index eca0001a0e51..3aa2aa75b913 100644 --- a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java +++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java @@ -1,5 +1,6 @@ package com.iluwatar.threadspecificstorage; +import java.security.SecureRandom; import java.util.Random; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -36,7 +37,7 @@ public void process() { // Step 3: Retrieve userId later in the request flow Long retrievedId = contextProxy.get().getUserId(); - Random random = new Random(); + SecureRandom random = new SecureRandom(); String accountInfo = retrievedId + "'s account: " + random.nextInt(400); LOGGER.info(accountInfo); From 106f06d8080aba527063a48c19a894b96d2f6dd4 Mon Sep 17 00:00:00 2001 From: CMD137 <2992456841@qq.com> Date: Fri, 23 Jan 2026 01:52:19 +0800 Subject: [PATCH 4/7] fixed: Run 'mvn spotless:apply'(remove unused import) --- .../java/com/iluwatar/threadspecificstorage/RequestHandler.java | 1 - 1 file changed, 1 deletion(-) diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java index 3aa2aa75b913..719c663c0641 100644 --- a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java +++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java @@ -1,7 +1,6 @@ package com.iluwatar.threadspecificstorage; import java.security.SecureRandom; -import java.util.Random; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; From d3a8041057948fc91ee5eb339b1defc5babd3b32 Mon Sep 17 00:00:00 2001 From: CMD137 <2992456841@qq.com> Date: Fri, 23 Jan 2026 11:27:24 +0800 Subject: [PATCH 5/7] fixed: Refactor Thread-Specific Storage pattern implementation to address Sonar issues: remove unnecessary instantiation, add private constructor, use diamond operator, and update tests. --- thread-specific-storage/pom.xml | 6 ++++++ .../com/iluwatar/threadspecificstorage/App.java | 6 ++---- .../threadspecificstorage/RequestHandler.java | 13 +++++++------ .../threadspecificstorage/UserContextProxy.java | 5 ++++- .../iluwatar/threadspecificstorage/AppTest.java | 12 +++++++----- .../threadspecificstorage/RequestHandlerTest.java | 14 ++++++-------- 6 files changed, 32 insertions(+), 24 deletions(-) diff --git a/thread-specific-storage/pom.xml b/thread-specific-storage/pom.xml index 6e54c3fc406f..1af91514369b 100644 --- a/thread-specific-storage/pom.xml +++ b/thread-specific-storage/pom.xml @@ -59,6 +59,12 @@ mockito-core test + + org.awaitility + awaitility + 4.2.1 + test + diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java index 2de8c837dd90..a84388acfe08 100644 --- a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java +++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/App.java @@ -17,9 +17,6 @@ public class App { * @param args command-line arguments (not used) */ public static void main(String[] args) { - // Initialize components - UserContextProxy proxy = new UserContextProxy(); - // Simulate concurrent requests from multiple users for (int i = 1; i <= 5; i++) { // Simulate tokens for different users @@ -28,7 +25,7 @@ public static void main(String[] args) { new Thread( () -> { // Simulate request processing flow - RequestHandler handler = new RequestHandler(proxy, token); + RequestHandler handler = new RequestHandler(token); handler.process(); }) .start(); @@ -37,6 +34,7 @@ public static void main(String[] args) { try { Thread.sleep(50); } catch (InterruptedException e) { + Thread.currentThread().interrupt(); LOGGER.warn("Sleep interrupted", e); } } diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java index 719c663c0641..d5f2cca2830d 100644 --- a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java +++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/RequestHandler.java @@ -1,7 +1,6 @@ package com.iluwatar.threadspecificstorage; import java.security.SecureRandom; -import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; /** @@ -10,12 +9,14 @@ *

Each instance simulates a request-processing thread that uses the Thread-Specific Object Proxy * to access Thread-Specific Object. */ -@AllArgsConstructor @Slf4j public class RequestHandler { - private final UserContextProxy contextProxy; private final String token; + public RequestHandler(String token) { + this.token = token; + } + /** * Simulated business process: 1. Parse userId from token ("Token::userId"). 2. Store userId in * thread-local storage. 3. Later, retrieve userId and use it for business logic. 4. Finally, @@ -29,13 +30,13 @@ public void process() { Long userId = parseToken(token); // Step 2: Save userId in ThreadLocal storage - contextProxy.set(new UserContext(userId)); + UserContextProxy.set(new UserContext(userId)); // Simulate delay between stages of request handling Thread.sleep(200); // Step 3: Retrieve userId later in the request flow - Long retrievedId = contextProxy.get().getUserId(); + Long retrievedId = UserContextProxy.get().getUserId(); SecureRandom random = new SecureRandom(); String accountInfo = retrievedId + "'s account: " + random.nextInt(400); LOGGER.info(accountInfo); @@ -44,7 +45,7 @@ public void process() { Thread.currentThread().interrupt(); } finally { // Step 4: Clear ThreadLocal to avoid potential memory leaks - contextProxy.clear(); + UserContextProxy.clear(); } } diff --git a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java index 998fc514bbf4..468e554b0bf6 100644 --- a/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java +++ b/thread-specific-storage/src/main/java/com/iluwatar/threadspecificstorage/UserContextProxy.java @@ -11,7 +11,10 @@ public class UserContextProxy { * Underlying TSObjectCollection (ThreadLocalMap) managed by JVM.This ThreadLocal acts as the Key * for the map.So That there is also no key factory. */ - private static final ThreadLocal userContextHolder = new ThreadLocal(); + private static final ThreadLocal userContextHolder = new ThreadLocal<>(); + + /** Private constructor to prevent instantiation of this utility class. */ + private UserContextProxy() {} /** Set UserContext for the current thread. */ public static void set(UserContext context) { diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java index 143c57541b49..775ded85b8ea 100644 --- a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java +++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/AppTest.java @@ -1,5 +1,8 @@ package com.iluwatar.threadspecificstorage; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayOutputStream; @@ -18,11 +21,10 @@ void testMainMethod() { App.main(new String[] {}); // Give some time for threads to execute - try { - Thread.sleep(2000); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } + await() + .atMost(5, SECONDS) + .pollInterval(100, MILLISECONDS) + .until(() -> outContent.toString().contains("Start handling request with token")); // Verify output contains expected log messages String output = outContent.toString(); diff --git a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java index e58cbb4a5661..e5f5968e7794 100644 --- a/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java +++ b/thread-specific-storage/src/test/java/com/iluwatar/threadspecificstorage/RequestHandlerTest.java @@ -8,27 +8,25 @@ class RequestHandlerTest { @Test void process_shouldStoreAndClearUserContext() { - // Given - a real UserContextProxy - UserContextProxy proxy = new UserContextProxy(); - RequestHandler handler = new RequestHandler(proxy, "token::123"); + // Given - a request handler without proxy parameter + RequestHandler handler = new RequestHandler("token::123"); // When - process the request handler.process(); // Then - after processing, ThreadLocal should be cleared - assertNull(proxy.get(), "ThreadLocal should be cleared after process()"); + assertNull(UserContextProxy.get(), "ThreadLocal should be cleared after process()"); } @Test void process_withInvalidToken_shouldSetUserIdToMinusOne() { - // Given - a real UserContextProxy - UserContextProxy proxy = new UserContextProxy(); - RequestHandler handler = new RequestHandler(proxy, "invalid-token"); + // Given - a request handler without proxy parameter + RequestHandler handler = new RequestHandler("invalid-token"); // When - process the request handler.process(); // Then - after processing, ThreadLocal should be cleared - assertNull(proxy.get(), "ThreadLocal should be cleared even for invalid token"); + assertNull(UserContextProxy.get(), "ThreadLocal should be cleared even for invalid token"); } } From 1bc4eddd836b9b814429af738d9bbe555bdee954 Mon Sep 17 00:00:00 2001 From: CMD137 <2992456841@qq.com> Date: Fri, 23 Jan 2026 12:20:07 +0800 Subject: [PATCH 6/7] fixed: update uml and README.md --- thread-specific-storage/README.md | 25 +++--- .../etc/ThreadSpecificStorageUML.png | Bin 28087 -> 24939 bytes .../etc/thread-specific-storage.urm.puml | 72 +++++++++--------- 3 files changed, 53 insertions(+), 44 deletions(-) diff --git a/thread-specific-storage/README.md b/thread-specific-storage/README.md index a08b1f6af199..e3109595fd84 100644 --- a/thread-specific-storage/README.md +++ b/thread-specific-storage/README.md @@ -63,9 +63,6 @@ of the user context data. @Slf4j public class APP { public static void main(String[] args) { - // Initialize components - UserContextProxy proxy = new UserContextProxy(); - // Simulate concurrent requests from multiple users for (int i = 1; i <= 5; i++) { // Simulate tokens for different users @@ -73,7 +70,7 @@ public class APP { new Thread(() -> { // Simulate request processing flow - RequestHandler handler = new RequestHandler(proxy, token); + RequestHandler handler = new RequestHandler(token); handler.process(); }).start(); @@ -93,9 +90,12 @@ Here's how the request handler processes each request: ```java @Slf4j public class RequestHandler { - private final UserContextProxy contextProxy; private final String token; + public RequestHandler(String token) { + this.token = token; + } + public void process() { LOGGER.info("Start handling request with token: {}", token); @@ -104,13 +104,13 @@ public class RequestHandler { Long userId = parseToken(token); // Step 2: Save userId in ThreadLocal storage - contextProxy.set(new UserContext(userId)); + UserContextProxy.set(new UserContext(userId)); // Simulate delay between stages of request handling Thread.sleep(200); // Step 3: Retrieve userId later in the request flow - Long retrievedId = contextProxy.get().getUserId(); + Long retrievedId = UserContextProxy.get().getUserId(); Random random = new Random(); String accountInfo = retrievedId + "'s account: " + random.nextInt(400); LOGGER.info(accountInfo); @@ -119,7 +119,7 @@ public class RequestHandler { Thread.currentThread().interrupt(); } finally { // Step 4: Clear ThreadLocal to avoid potential memory leaks - contextProxy.clear(); + UserContextProxy.clear(); } } @@ -133,7 +133,12 @@ The UserContextProxy acts as a thread-safe accessor to the ThreadLocal storage: public class UserContextProxy { // Underlying TSObjectCollection (ThreadLocalMap) managed by JVM. // This ThreadLocal acts as the Key for the map. - private static final ThreadLocal userContextHolder = new ThreadLocal(); + private static final ThreadLocal userContextHolder = new ThreadLocal<>(); + + /** + * Private constructor to prevent instantiation of this utility class. + */ + private UserContextProxy() {} /** Set UserContext for the current thread */ public static void set(UserContext context) { @@ -219,4 +224,4 @@ Start handling request with token: token::2 * [Pattern-Oriented Software Architecture, Volume 2: Patterns for Concurrent and Networked Objects](https://www.amazon.com/Pattern-Oriented-Software-Architecture-Concurrent-Networked/dp/0471606952) * [Java Documentation for ThreadLocal](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/ThreadLocal.html) -* [Java Concurrency in Practice](https://jcip.net/) by Brian Goetz +* [Java Concurrency in Practice](https://jcip.net/) by Brian Goetz \ No newline at end of file diff --git a/thread-specific-storage/etc/ThreadSpecificStorageUML.png b/thread-specific-storage/etc/ThreadSpecificStorageUML.png index 1d2e0d27d7702005daeb4607bcec352f8feddb5e..d4957dd1cc2a3a488979328616ec1671a2050675 100644 GIT binary patch literal 24939 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 miidQK2C{%#cZrJhJMovMp^)n?|%TN#?Fxd diff --git a/thread-specific-storage/etc/thread-specific-storage.urm.puml b/thread-specific-storage/etc/thread-specific-storage.urm.puml index 8b95fce260de..b38ac8755f72 100644 --- a/thread-specific-storage/etc/thread-specific-storage.urm.puml +++ b/thread-specific-storage/etc/thread-specific-storage.urm.puml @@ -1,37 +1,41 @@ @startuml -!theme plain -top to bottom direction -class UserContext { - - userId : Long - + UserContext(userId : Long) - + getUserId() : Long - + setUserId(userId : Long) : void -} -class UserContextProxy { - - {static} userContextHolder : ThreadLocal - + {static} set(context : UserContext) : void - + {static} get() : UserContext - + {static} clear() : void -} -class RequestHandler { - - contextProxy : UserContextProxy - - token : String - + RequestHandler(contextProxy : UserContextProxy, token : String) - + process() : void - - parseToken(token : String) : Long -} -class APP { - + {static} main(args : String[]) : void -} -class Thread { - + start() : void -} +package thread-specific-storage { + + class APP { + + {static} main(args : String[]) : void + } -UserContextProxy ..> UserContext : manages -RequestHandler ..> UserContextProxy : uses -RequestHandler ..> UserContext : creates -APP ..> UserContextProxy : creates -APP ..> RequestHandler : creates -APP ..> Thread : starts -Thread ..> RequestHandler : executes + class Thread { + + start() : void + } + + class UserContext { + - userId : Long + + UserContext(userId : Long) + + getUserId() : Long + + setUserId(userId : Long) : void + } + + class UserContextProxy { + - {static} userContextHolder : ThreadLocal + + {static} set(context : UserContext) : void + + {static} get() : UserContext + + {static} clear() : void + } + + class RequestHandler { + - token : String + + RequestHandler(token : String) + + process() : void + - parseToken(token : String) : Long + } + + UserContextProxy --> UserContext : manages + RequestHandler --> UserContextProxy : uses static + RequestHandler --> UserContext : creates + APP --> RequestHandler : creates + APP --> Thread : starts + Thread --> RequestHandler : executes + +} @enduml \ No newline at end of file From c11025818420a2020eef4d5e8e893eddbd38ace2 Mon Sep 17 00:00:00 2001 From: CMD137 <2992456841@qq.com> Date: Fri, 23 Jan 2026 12:35:39 +0800 Subject: [PATCH 7/7] Trigger CI rerun