关于Java线程问题,在博客上看到一篇文章挺好的:

https://blog.csdn.net/w172087242/article/details/83375022#23_ThreadLocal_175

自己动手实验了一下。

1、maven设置

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
<scope>provided</scope>
</dependency>

<!-- https://mvnrepository.com/artifact/com.alibaba/transmittable-thread-local -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.10.2</version>
</dependency>

 

2、目录设置

3、公共服务类

①:用户实体类

package cn.demo.entity;

import java.time.LocalDate;

import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class User {

private Integer userId;
private String name;
private LocalDate birthday;

public Integer getUserId() {
return userId;
}

public void setUserId(Integer userId) {
this.userId = userId;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public LocalDate getBirthday() {
return birthday;
}

public void setBirthday(LocalDate birthday) {
this.birthday = birthday;
}

@Override
public String toString() {
return "User [userId=" + userId + ", name=" + name + ", birthday=" + birthday + "]";
}
}

②:用户信息管理上下文类

package cn.demo.context;

import cn.demo.entity.User;

/**
* 基于线程上下文的用户信息管理
*/
public class BaseUserContext {

//存储线程变量
public ThreadLocal<User> context = null;

/**
* 设置用户信息
*
* @param user -- 用户信息
*/
public void set(User user) {
context.set(user);
}

/**
* 获取用户信息
*
* @return -- 用户信息
*/
public User get() {
return context.get();
}

/**
* 移除用户信息
*/
public void remove() {
context.remove();
}
}

③:基本调用服务类(子类继承)

package cn.demo.context;

import cn.demo.entity.User;

/**
* 基于线程上下文的用户信息管理
*/
public class BaseUserContext {

//存储线程变量
public ThreadLocal<User> context = null;

/**
* 设置用户信息
*
* @param user -- 用户信息
*/
public void set(User user) {
context.set(user);
}

/**
* 获取用户信息
*
* @return -- 用户信息
*/
public User get() {
return context.get();
}

/**
* 移除用户信息
*/
public void remove() {
context.remove();
}
}

④:接口服务

package cn.demo.service;

import cn.demo.context.BaseUserContext;

public class UserService {

private BaseUserContext userContext;

public UserService(BaseUserContext userContext) {
this.userContext = userContext;
}

/**
* 执行添加用户
*/
public void addUser() {
System.out.println(Thread.currentThread().getName() + "添加用户信息:" + userContext.get());
}
}

4、ThreadLocal,线程变量

优点:多线程环境中存储线程级别变量,单线程没有必要使用。

代码-上下文:

package cn.demo.context;

import cn.demo.entity.User;

public class UserContext1 extends BaseUserContext {

public UserContext1() {
//1、线程开启新线程有缺陷
this.context = new ThreadLocal<User>();
}
}

代码-调用:

package cn.demo.call;

import cn.demo.context.BaseUserContext;
import cn.demo.context.UserContext1;
import cn.demo.service.UserService;

public class CallService1 extends BaseCall {
public static void main(String[] args) {
BaseUserContext userContext = new UserContext1();
UserService userService = new UserService(userContext);
//同时10个调用
for (int i = 0; i < 10; i++) {
new Thread(() -> {
userContext.set(initUser(Thread.currentThread().getName()));
//进行调用
userService.addUser();
}, "CallService1-" + i).start();
}
}

}

控制台输出结果:(正确)

CallService1-3添加用户信息:User [userId=3, name=CallService1-3, birthday=1995-07-26]
CallService1-8添加用户信息:User [userId=4, name=CallService1-8, birthday=2000-10-01]
CallService1-2添加用户信息:User [userId=8, name=CallService1-2, birthday=1995-07-26]
CallService1-5添加用户信息:User [userId=9, name=CallService1-5, birthday=2000-10-01]
CallService1-7添加用户信息:User [userId=10, name=CallService1-7, birthday=1988-09-11]
CallService1-1添加用户信息:User [userId=6, name=CallService1-1, birthday=1989-11-10]
CallService1-4添加用户信息:User [userId=7, name=CallService1-4, birthday=1990-03-07]
CallService1-9添加用户信息:User [userId=5, name=CallService1-9, birthday=1988-09-11]
CallService1-0添加用户信息:User [userId=1, name=CallService1-0, birthday=1989-11-10]
CallService1-6添加用户信息:User [userId=2, name=CallService1-6, birthday=1990-03-07]

缺点:它仅仅能获取自己当前线程设置的变量,开启新的线程后获取到初始线程设置的变量值。

代码-调用:

package cn.demo.call;

import cn.demo.context.BaseUserContext;
import cn.demo.context.UserContext1;
import cn.demo.service.UserService;

public class CallService2 extends BaseCall {

public static void main(String[] args) {
//main作为当前调用线程
BaseUserContext userContext = new UserContext1();
userContext.set(initUser(Thread.currentThread().getName()));
UserService userService = new UserService(userContext);
//开启新线程来进行调用
new Thread(() -> userService.addUser(), "CallService2").start();
}

}

控制台输出结果:(错误)

CallService2添加用户信息:null

5、InheritableThreadLocal

解决开启新的线程后,ThreadLocal无法获取到线程变量问题。

但是在应用线程池的场景中,线程复用导致读取线程变量数据混乱问题(真实项目中线程池应用很广泛)

代码-上下文:

package cn.demo.context;

import cn.demo.entity.User;

public class UserContext3 extends BaseUserContext {
public UserContext3() {
//2、线程复用导致数据混乱
this.context = new InheritableThreadLocal<User>();
}
}

代码-调用:

package cn.demo.call;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

import cn.demo.context.BaseUserContext;
import cn.demo.context.UserContext3;
import cn.demo.service.UserService;

public class CallService3 extends BaseCall {

//申明一个简单的线程池,3个核心线程
private static final AtomicInteger threadIdCreator = new AtomicInteger(1);
private static ExecutorService pool = Executors.newFixedThreadPool(3, (runnable) ->
new Thread(runnable, "ThreadName-" + threadIdCreator.getAndIncrement())
);

public static void main(String[] args) {
BaseUserContext userContext = new UserContext3();
UserService userService = new UserService(userContext);
//同时10个调用
for (int i = 0; i < 10; i++) {
new Thread(() -> {
userContext.set(initUser(Thread.currentThread().getName()));
//使用线程池进行调用
pool.execute(userService::addUser);
}, "CallService3-" + i).start();
}
}

}

控制台输出结果:(错误:复用线程导致线程变量混乱,只有用户1,2,3)

ThreadName-2添加用户信息:User [userId=3, name=CallService3-4, birthday=1995-07-26]
ThreadName-3添加用户信息:User [userId=2, name=CallService3-1, birthday=1990-03-07]
ThreadName-3添加用户信息:User [userId=2, name=CallService3-1, birthday=1990-03-07]
ThreadName-1添加用户信息:User [userId=1, name=CallService3-0, birthday=1989-11-10]
ThreadName-3添加用户信息:User [userId=2, name=CallService3-1, birthday=1990-03-07]
ThreadName-3添加用户信息:User [userId=2, name=CallService3-1, birthday=1990-03-07]
ThreadName-2添加用户信息:User [userId=3, name=CallService3-4, birthday=1995-07-26]
ThreadName-3添加用户信息:User [userId=2, name=CallService3-1, birthday=1990-03-07]
ThreadName-1添加用户信息:User [userId=1, name=CallService3-0, birthday=1989-11-10]
ThreadName-2添加用户信息:User [userId=3, name=CallService3-4, birthday=1995-07-26]

6、TransmittableThreadLocal

必须配合如TtlRunnable/TtlCallable等一起使用,也可以配合ExecutorServiceTtlWrapper的线程池使用

代码-上下文:

package cn.demo.context;

import com.alibaba.ttl.TransmittableThreadLocal;

import cn.demo.entity.User;

public class UserContext4 extends BaseUserContext {

public UserContext4() {
//3、提供的无侵入式实现
this.context = new TransmittableThreadLocal<User>();
}
}

代码-调用:

package cn.demo.call;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

import com.alibaba.ttl.TtlRunnable;

import cn.demo.context.BaseUserContext;
import cn.demo.context.UserContext4;
import cn.demo.service.UserService;

public class CallService4 extends BaseCall {

//申明一个简单的线程池,3个核心线程
private static final AtomicInteger threadIdCreator = new AtomicInteger(1);
private static ExecutorService pool = Executors.newFixedThreadPool(3, (runnable) ->
new Thread(runnable, "ThreadName-" + threadIdCreator.getAndIncrement())
);

public static void main(String[] args) {
BaseUserContext userContext = new UserContext4();
UserService userService = new UserService(userContext);
//同时10个调用
for (int i = 0; i < 10; i++) {
new Thread(() -> {
userContext.set(initUser(Thread.currentThread().getName()));
//使用线程池进行调用
//pool.execute(userService::addUser);
pool.execute(TtlRunnable.get(userService::addUser));
}, "CallService4-" + i).start();
}
}

}

控制台输出结果:(正确)

ThreadName-2添加用户信息:User [userId=7, name=CallService4-6, birthday=1990-03-07]
ThreadName-1添加用户信息:User [userId=4, name=CallService4-2, birthday=2000-10-01]
ThreadName-2添加用户信息:User [userId=10, name=CallService4-9, birthday=1988-09-11]
ThreadName-1添加用户信息:User [userId=3, name=CallService4-5, birthday=1995-07-26]
ThreadName-2添加用户信息:User [userId=6, name=CallService4-3, birthday=1989-11-10]
ThreadName-1添加用户信息:User [userId=1, name=CallService4-0, birthday=1989-11-10]
ThreadName-2添加用户信息:User [userId=9, name=CallService4-8, birthday=2000-10-01]
ThreadName-3添加用户信息:User [userId=5, name=CallService4-1, birthday=1988-09-11]
ThreadName-1添加用户信息:User [userId=2, name=CallService4-4, birthday=1990-03-07]
ThreadName-3添加用户信息:User [userId=8, name=CallService4-7, birthday=1995-07-26]

项目地址:

https://github.com/wangymd/ThreadTest.git

Java线程变量问题-ThreadLocal的更多相关文章

  1. Java线程本地存储ThreadLocal

    前言 ThreadLocal 是一种 无同步 的线程安全实现 体现了 Thread-Specific Storage 模式:即使只有一个入口,内部也会为每个线程分配特有的存储空间,线程间 没有共享资源 ...

  2. 【Java线程安全】 — ThreadLocal

    [用法] 首先明确,ThreadLocal是用空间换时间来解决线程安全问题的,方法是各个线程拥有自己的变量副本. 既然如此,那么是涉及线程安全,必然有一个共享变量,我给大家声明一个: public c ...

  3. 线程变量(ThreadLocal)的使用和测试

    ThreadLocal可以定义线程范围的变量,也可以称之为线程局部变量.与一般的变量的区别在于,生命周期是在线程范围内的. 也就是说某个类的某个对象(为清晰描述,以下称A对象)里面有个ThreadLo ...

  4. java 多线程(0) Java线程

    线程 线程是系统调度的基本单元,每当创建一个进程时,会有许多的线程,也叫轻量级进程,在一个进程中拥有多个线程,各自都有自己的计数器,堆和局部变量属性,并且能够分享内存变量. 为什么要使用多线程  1. ...

  5. Java Concurrency - ThreadLocal, 本地线程变量

    共享数据是多线程应用最常见的问题之一,但有时我们需要为每个线程保存一份独立的变量.Java API 提供了 ThreadLocal 来解决这个问题. 一个 ThreadLocal 作用的例子: imp ...

  6. 【java】ThreadLocal线程变量的实现原理和使用场景

    一.ThreadLocal线程变量的实现原理 1.ThreadLocal核心方法有这个几个 get().set(value).remove() 2.实现原理 ThreadLocal在每个线程都会创建一 ...

  7. Java 类 ThreadLocal 本地线程变量

    前言:工作中将要使用ThreadLocal,先学习总结一波.有不对的地方欢迎评论指出. 定义 ThreadLocal并不是一个Thread,而是Thread的局部变量.这些变量不同于它们的普通对应物, ...

  8. 【java项目实战】ThreadLocal封装Connection,实现同一线程共享资源

    线程安全一直是程序员们关注的焦点.多线程也一直是比較让人头疼的话题,想必大家以前也遇到过各种各种的问题.我就不再累述了.当然,解决方案也有非常多,这篇博文给大家提供一种非常好的解决线程安全问题的思路. ...

  9. Java线程:线程栈模型与线程的变量

    Java线程:线程栈模型与线程的变量   要理解线程调度的原理,以及线程执行过程,必须理解线程栈模型. 线程栈是指某时刻时内存中线程调度的栈信息,当前调用的方法总是位于栈顶.线程栈的内容是随着程序的运 ...

随机推荐

  1. Rabbit的字符串 字符串最小表示法

    Rabbit的字符串 #include<bits/stdc++.h> using namespace std; ; char s[maxn]; int get_min_pos() { , ...

  2. 5.7 Go 捕获异常

    5.7 Go 捕获异常 Go语言处理异常不同于其他语言处理异常的方式. 传统语言处理异常: try catch finally go语言 引入了defer.panic.recover 1.Go程序抛出 ...

  3. SQL拦截器

    一.拦截SQL 1.slow log 2.general log 3.mycat sql拦截器 二.mycat sql拦截器 1.配置server.xml # 1.5可用 <system> ...

  4. MYSQL的DOUBLE WRITE双写

    期待未来超高速大容量的固态硬盘普及时,只需要CHECKPOINT,而不再需要各种各样的BUFFER,CACHE了 DOUBLE WRITE 在InnoDB将BP中的Dirty Page刷(flush) ...

  5. JS中的var、let、const三者的区别

    ES5:var          ES6:let.const ES5中的作用域有---全局作用域.函数作用域 ES6中新增了---块级作用域(块级作用域由{}包裹,if语句.for语句中的{}也属于块 ...

  6. Docker容器启动时初始化Mysql数据库

    1. 前言 Docker在开发中使用的越来越多了,最近搞了一个Spring Boot应用,为了方便部署将Mysql也放在Docker中运行.那么怎么初始化 SQL脚本以及数据呢? 我这里有两个传统方案 ...

  7. 剑指Offer之调整数组顺序使奇数位于偶数前面

    题目描述 输入一个整数数组,实现一个函数来调整该数组中数字的顺序,使得所有的奇数位于数组的前半部分,所有的偶数位于数组的后半部分,并保证奇数和奇数,偶数和偶数之间的相对位置不变.   思路:将奇数放进 ...

  8. nginx: [error] invalid PID number "" in ...

    1.查看进程 ps -ef|grep nginx 2.进入nginx安装目录sbin下,执行命令: ./nginx -t 如下显示: syntax is ok test is successful 3 ...

  9. 8.Hash集合类型操作使用

    数据类型Hash (1)介绍 hash数据类型存储的数据与mysql数据库中存储的一条记录极为相似 Redis本身就类似于Hash的存储结构,分为key-value键值对,实际上它的Hash数据就好像 ...

  10. 前端练手小项目——网页版qq音乐仿写

    qq音乐网页版仿写 一些步骤与注意事项 一开始肯定就是html+css布局和页面了,这段特别耗时间,耐心写完就好了 首先要说一下大致流程: 一定要先布局html!,所以一定要先分析页面布局情况,用不同 ...