Java線程變量問題-ThreadLocal


關於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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM