feature: Rewrite thread-local storage pattern (#2452)

This commit is contained in:
Eugene
2023-01-19 20:50:59 +02:00
committed by GitHub
parent 13d0416045
commit 196233d587
18 changed files with 432 additions and 933 deletions
+247
View File
@@ -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)
+46
View File
@@ -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))));
}
}