mirror of
https://github.com/tiennm99/java-design-patterns.git
synced 2026-08-17 00:25:54 +00:00
feature: Rewrite thread-local storage pattern (#2452)
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
---
|
||||
title: Thread-local storage
|
||||
category: Concurrency
|
||||
language: en
|
||||
tag:
|
||||
- Data access
|
||||
---
|
||||
|
||||
## Intent
|
||||
|
||||
Provide an ability to have a copy of a variable for each thread, making it thread-safe.
|
||||
|
||||
## Explanation
|
||||
|
||||
During code assembling compiler add _.tdata_ directive,
|
||||
which means that variables below will store in different places from thread to thread.
|
||||
This means changing variable in one thread won't affect the same variable in other thread.
|
||||
|
||||
**Real world example**
|
||||
|
||||
> On constructions each worker has its own shovel. When one of them broke his shovel,
|
||||
> other still can continue work due to they have own instrument.
|
||||
|
||||
**In plain words**
|
||||
|
||||
>Each thread will have its own copy of variable.
|
||||
|
||||
**Wikipedia says**
|
||||
|
||||
>Thread-local storage (TLS) is a computer programming method that uses static or global memory local to a thread.
|
||||
|
||||
**Programmatic Example**
|
||||
|
||||
To define variable in thread-local storage you just need to put it into Java's ThreadLocal, e.g:
|
||||
|
||||
```java
|
||||
public class Main {
|
||||
|
||||
private ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();
|
||||
|
||||
public void initialize(String value) {
|
||||
this.stringThreadLocal.set(value);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Below we will contact a variable from two threads at a time to see, why we need it.
|
||||
Main logic that show's current variable value is imposed in class AbstractThreadLocalExample, two implementations
|
||||
differs only in the **value store approach**.
|
||||
|
||||
Main class:
|
||||
|
||||
```java
|
||||
/**
|
||||
* Class with main per-thread logic.
|
||||
*/
|
||||
public abstract class AbstractThreadLocalExample implements Runnable {
|
||||
|
||||
private static final Random RND = new Random();
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
//Here we stop thread randomly
|
||||
LockSupport.parkNanos(RND.nextInt(1_000_000_000, 2_000_000_000));
|
||||
|
||||
System.out.println(getCurrentThreadName() + ", before value changing: " + getter().get());
|
||||
setter().accept(RND.nextInt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for our value.
|
||||
*
|
||||
* @return consumer
|
||||
*/
|
||||
protected abstract Consumer<Integer> setter();
|
||||
|
||||
/**
|
||||
* Getter for our value.
|
||||
*
|
||||
* @return supplier
|
||||
*/
|
||||
protected abstract Supplier<Integer> getter();
|
||||
|
||||
private String getCurrentThreadName() {
|
||||
return Thread.currentThread().getName();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
And two implementations. With ThreadLocal:
|
||||
```java
|
||||
/**
|
||||
* Example of runnable with use of {@link ThreadLocal}.
|
||||
*/
|
||||
public class WithThreadLocal extends AbstractThreadLocalExample {
|
||||
|
||||
private ThreadLocal<Integer> value;
|
||||
|
||||
public WithThreadLocal(ThreadLocal<Integer> value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Consumer<Integer> setter() {
|
||||
return value::set;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<Integer> getter() {
|
||||
return value::get;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
And the class that stores value in one shared for all threads place:
|
||||
```java
|
||||
/**
|
||||
* Example of runnable without usage of {@link ThreadLocal}.
|
||||
*/
|
||||
public class WithoutThreadLocal extends AbstractThreadLocalExample {
|
||||
|
||||
private Integer value;
|
||||
|
||||
public WithoutThreadLocal(Integer value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Consumer<Integer> setter() {
|
||||
return integer -> value = integer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<Integer> getter() {
|
||||
return () -> value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Guess we want to run these classes. We will construct both implementations and invoke _run_ method.
|
||||
So, we want to see initial value in console (thanks to System.out.println()). Let's take a look at tests.
|
||||
|
||||
```java
|
||||
public class ThreadLocalTest {
|
||||
|
||||
private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
|
||||
private final PrintStream originalOut = System.out;
|
||||
|
||||
@BeforeEach
|
||||
public void setUpStreams() {
|
||||
System.setOut(new PrintStream(outContent));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void restoreStreams() {
|
||||
System.setOut(originalOut);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withoutThreadLocal() throws InterruptedException {
|
||||
int initialValue = 1234567890;
|
||||
|
||||
int threadSize = 2;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threadSize);
|
||||
|
||||
WithoutThreadLocal threadLocal = new WithoutThreadLocal(initialValue);
|
||||
for (int i = 0; i < threadSize; i++) {
|
||||
executor.submit(threadLocal);
|
||||
}
|
||||
executor.awaitTermination(3, TimeUnit.SECONDS);
|
||||
|
||||
List<String> lines = outContent.toString().lines().toList();
|
||||
//Matches only first finished thread output, the second has changed by first thread value
|
||||
Assertions.assertFalse(lines.stream()
|
||||
.allMatch(line -> line.endsWith(String.valueOf(initialValue))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withThreadLocal() throws InterruptedException {
|
||||
int initialValue = 1234567890;
|
||||
|
||||
int threadSize = 2;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threadSize);
|
||||
|
||||
WithThreadLocal threadLocal = new WithThreadLocal(ThreadLocal.withInitial(() -> initialValue));
|
||||
for (int i = 0; i < threadSize; i++) {
|
||||
executor.submit(threadLocal);
|
||||
}
|
||||
|
||||
executor.awaitTermination(3, TimeUnit.SECONDS);
|
||||
|
||||
List<String> lines = outContent.toString().lines().toList();
|
||||
Assertions.assertTrue(lines.stream()
|
||||
.allMatch(line -> line.endsWith(String.valueOf(initialValue))));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The output of test named withThreadLocal:
|
||||
```
|
||||
pool-2-thread-2, before value changing: 1234567890
|
||||
pool-2-thread-1, before value changing: 1234567890
|
||||
|
||||
```
|
||||
|
||||
And the output of withoutThreadLocal:
|
||||
```
|
||||
pool-1-thread-2, before value changing: 1234567890
|
||||
pool-1-thread-1, before value changing: 848843054
|
||||
```
|
||||
Where 1234567890 - is our initial value. We see, that in test _withoutThreadLocal_
|
||||
thread 2 got out from LockSupport#parkNanos earlier than the first and
|
||||
change value in shared variable.
|
||||
|
||||
## Class diagram
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class ThreadLocal
|
||||
ThreadLocal : get()
|
||||
ThreadLocal : initialValue()
|
||||
ThreadLocal : remove()
|
||||
ThreadLocal : set(T value)
|
||||
```
|
||||
|
||||
## Applicability
|
||||
|
||||
Use ThreadLocal when:
|
||||
|
||||
- You need to run singleton with state in multiple threads
|
||||
- You need to use not thread-safe classes in concurrency program
|
||||
|
||||
## Tutorials
|
||||
- [Baeldung](https://www.baeldung.com/java-threadlocal)
|
||||
|
||||
## Known uses
|
||||
- In java.lang.Thread during thread initialization
|
||||
- In java.net.URL to prevent recursive provider lookups
|
||||
- In org.junit.runners.BlockJUnit4ClassRunner to contain current rule
|
||||
- In org.springframework:spring-web to store request context
|
||||
- In org.apache.util.net.Nio2Endpoint to allow detecting if a completion handler completes inline
|
||||
- In io.micrometer to avoid problems with not thread-safe NumberFormat
|
||||
|
||||
## Credits
|
||||
- [Usage cases](https://chao-tic.github.io/blog/2018/12/25/tls)
|
||||
- [Implementation in Linux](https://uclibc.org/docs/tls.pdf)
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
|
||||
|
||||
The MIT License
|
||||
Copyright © 2014-2022 Ilkka Seppälä
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
-->
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.iluwatar</groupId>
|
||||
<artifactId>java-design-patterns</artifactId>
|
||||
<version>1.26.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>thread-local-storage</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.iluwatar;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Class with main logic.
|
||||
*/
|
||||
public abstract class AbstractThreadLocalExample implements Runnable {
|
||||
|
||||
private static final SecureRandom RND = new SecureRandom();
|
||||
|
||||
private static final Integer RANDOM_THREAD_PARK_START = 1_000_000_000;
|
||||
private static final Integer RANDOM_THREAD_PARK_END = 2_000_000_000;
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long nanosToPark = RND.nextInt(RANDOM_THREAD_PARK_START, RANDOM_THREAD_PARK_END);
|
||||
LockSupport.parkNanos(nanosToPark);
|
||||
|
||||
System.out.println(getThreadName() + ", before value changing: " + getter().get());
|
||||
setter().accept(RND.nextInt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for our value.
|
||||
*
|
||||
* @return consumer
|
||||
*/
|
||||
protected abstract Consumer<Integer> setter();
|
||||
|
||||
/**
|
||||
* Getter for our value.
|
||||
*
|
||||
* @return supplier
|
||||
*/
|
||||
protected abstract Supplier<Integer> getter();
|
||||
|
||||
private String getThreadName() {
|
||||
return Thread.currentThread().getName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.iluwatar;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
/**
|
||||
* Example of runnable with use of {@link ThreadLocal}.
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class WithThreadLocal extends AbstractThreadLocalExample {
|
||||
|
||||
private final ThreadLocal<Integer> value;
|
||||
|
||||
/**
|
||||
* Removes the current thread's value for this thread-local variable.
|
||||
*/
|
||||
public void remove() {
|
||||
this.value.remove();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Consumer<Integer> setter() {
|
||||
return value::set;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<Integer> getter() {
|
||||
return value::get;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.iluwatar;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
/**
|
||||
* Example of runnable without usage of {@link ThreadLocal}.
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
public class WithoutThreadLocal extends AbstractThreadLocalExample {
|
||||
|
||||
private Integer value;
|
||||
|
||||
@Override
|
||||
protected Consumer<Integer> setter() {
|
||||
return integer -> value = integer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Supplier<Integer> getter() {
|
||||
return () -> value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import com.iluwatar.WithThreadLocal;
|
||||
import com.iluwatar.WithoutThreadLocal;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ThreadLocalTest {
|
||||
|
||||
private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
|
||||
private final PrintStream originalOut = System.out;
|
||||
|
||||
@BeforeEach
|
||||
public void setUpStreams() {
|
||||
System.setOut(new PrintStream(outContent));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void restoreStreams() {
|
||||
System.setOut(originalOut);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withoutThreadLocal() throws InterruptedException {
|
||||
int initialValue = 1234567890;
|
||||
|
||||
int threadSize = 2;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threadSize);
|
||||
|
||||
WithoutThreadLocal threadLocal = new WithoutThreadLocal(initialValue);
|
||||
for (int i = 0; i < threadSize; i++) {
|
||||
executor.submit(threadLocal);
|
||||
}
|
||||
executor.awaitTermination(3, TimeUnit.SECONDS);
|
||||
|
||||
List<String> lines = outContent.toString().lines().toList();
|
||||
//Matches only first thread, the second has changed by first thread value
|
||||
Assertions.assertFalse(lines.stream()
|
||||
.allMatch(line -> line.endsWith(String.valueOf(initialValue))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withThreadLocal() throws InterruptedException {
|
||||
int initialValue = 1234567890;
|
||||
|
||||
int threadSize = 2;
|
||||
ExecutorService executor = Executors.newFixedThreadPool(threadSize);
|
||||
|
||||
WithThreadLocal threadLocal = new WithThreadLocal(ThreadLocal.withInitial(() -> initialValue));
|
||||
for (int i = 0; i < threadSize; i++) {
|
||||
executor.submit(threadLocal);
|
||||
}
|
||||
|
||||
executor.awaitTermination(3, TimeUnit.SECONDS);
|
||||
threadLocal.remove();
|
||||
|
||||
List<String> lines = outContent.toString().lines().toList();
|
||||
Assertions.assertTrue(lines.stream()
|
||||
.allMatch(line -> line.endsWith(String.valueOf(initialValue))));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user