什么是ThreadLocal以及三大应用场景
一、什么是ThreadLocal
ThreadLocal是Thread的局部变量,它是每个线程独享的本地变量,每个线程都有自己的ThreadLocal,它们是线程隔离的。
二、三大使用场景
-
场景1:每个线程需要一个独享的对象,通常是工具类,比如典型的SimpleDateFormat和Random等。
-
场景2:每个线程内需要保存线程内的全局变量,这样线程在执行多个方法的时候,可以在多个方法中获取这个线程内的全局变量,避免了过度参数传递的问题。
-
场景3:记录接口请求日志,(多个方法之间传参的应用)
场景一:多线程格式化时间
package com.demo.demo.threadLocal;
import java.text.SimpleDateFormat;
import java.util.Date;
public class test01 {
public static void main(String[] args) {
new Thread(() -> {
String date = new test01().date(10);
System.out.println(date);
}).start();
new Thread(() -> {
String date = new test01().date(100);
System.out.println(date);
}).start();
}
private String date(int seconds) {
// 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return dateFormat.format(date);
}
}
少数的线程使用上述方式是没问题的,但是多个线程,就会重复创建SimpleDateFormat对象,不管是对内存还是gc都不好。
下面使用线程池:1000个线程来共享一个SimpleDateFormat对象,这样SimpleDateFormat对象只需要创建一次即可
package com.demo.demo.threadLocal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class test02 {
public static ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10);
static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
THREAD_POOL.submit(() -> {
String date = new test02().date(finalI);
System.out.println(date);
});
}
// 关闭线程池,此种关闭方式不再接受新的任务提交,等待现有队列中的任务全部执行完毕之后关闭
THREAD_POOL.shutdown();
}
private String date(int seconds) {
Date date = new Date(1000 * seconds);
return DATE_FORMAT.format(date);
}
}
执行结果如下:己经出现了重复的数据。
1970-01-01 08:00:04
1970-01-01 08:00:02
1970-01-01 08:00:03
1970-01-01 08:00:04
1970-01-01 08:00:03
1970-01-01 08:00:06
1970-01-01 08:00:06
1970-01-01 08:00:07
解决方式一:使用synchronized关键字
private String date(int seconds) {
Date date = new Date(1000 * seconds);
String format;
synchronized (Object.class) {
format = DATE_FORMAT.format(date);
}
return format;
}
解决方式二:ThreadLocal
package com.demo.demo.threadLocal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class test03 {
public static ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
THREAD_POOL.submit(() -> {
String date = new test03().date(finalI);
System.out.println(date);
});
}
THREAD_POOL.shutdown();
}
private String date(int seconds) {
// 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat simpleDateFormat = ThreadSafeDateFormatter.dateFormatThreadLocal.get();
return simpleDateFormat.format(date);
}
}
class ThreadSafeDateFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}
以上介绍的是ThreadLocal的第一大场景的使用,也就是利用到了ThreadLocal的initialValue()
方法,使得每个线程内都具备了一个SimpleDateFormat副本。
场景二:参数传递的问题
假设有一个学生类,类成员变量包括姓名,性别,成绩,我们需要定义三个方法来分别获取学生的姓名、性别和成绩,那么我们传统的做法是:
package com.demo.demo.threadLocal;
public class test04 {
public static void main(String[] args) {
Student student = init();
new NameService().getName(student);
new SexService().getSex(student);
new ScoreService().getScore(student);
}
private static Student init() {
Student student = new Student();
student.name = "Lemon";
student.sex = "female";
student.score = "100";
return student;
}
}
class Student {
/**
* 姓名、性别、成绩
*/
String name;
String sex;
String score;
}
class NameService {
public void getName(Student student) {
System.out.println(student.name);
}
}
class SexService {
public void getSex(Student student) {
System.out.println(student.sex);
}
}
class ScoreService {
public void getScore(Student student) {
System.out.println(student.score);
}
}
从上面的代码中可以看出,每个类的方法都需要传递学生的信息才可以获取到正确的信息,这样做能达到目的。但是每个方法都需要学生信息作为入参,这样未免有点繁琐,且在实际使用中通常在每个方法里面还需要对每个学生信息进行判空,这样的代码显得十分冗余,不利于维护。也可以使用ConcurrentHashMap或者Collections.SynchronizedMap()
ThreadLocal 的解决方式:
package com.demo.demo.threadLocal;
public class test05 {
public static void main(String[] args) {
init();
new NameService().getName();
new SexService().getSex();
new ScoreService().getScore();
}
private static void init() {
Student student = new Student();
student.name = "Lemon";
student.sex = "female";
student.score = "100";
ThreadLocalProcessor.studentThreadLocal.set(student);
}
}
class ThreadLocalProcessor {
public static ThreadLocal<Student> studentThreadLocal = new ThreadLocal<>();
}
class Student {
/**
* 姓名、性别、成绩
*/
String name;
String sex;
String score;
}
class NameService {
public void getName() {
System.out.println(ThreadLocalProcessor.studentThreadLocal.get().name);
}
}
class SexService {
public void getSex() {
System.out.println(ThreadLocalProcessor.studentThreadLocal.get().sex);
}
}
class ScoreService {
public void getScore() {
System.out.println(ThreadLocalProcessor.studentThreadLocal.get().score);
}
}
总结前两个:
-
场景一:通常多线程之间需要拥有同一个对象的副本,那么通常就采用initialValue()方法进行初始化,直接将需要拥有的对象存储到ThreadLocal中。
-
场景二:如果多个线程中存储不同的信息,为了方便在其他方法里面获取到信息,那么这种场景适合使用set()方法。例如,在拦截器生成的用户信息,用ThreadLocal.set直接放入到ThreadLocal中去,以便在后续的方法中取出来使用。
场景三:拦截器里ThreadLocal的使用
package oa.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* Created by hongtu on 2018/8/11.
*/
@Aspect
@Component
public class RequestAspect {
private static ThreadLocal<Long> startTime = new ThreadLocal<Long>();
private static ThreadLocal<String> key = new ThreadLocal<String>();
/**
* 定义拦截规则:拦截有@GetMapping注解的方法
*/
//@Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
@Pointcut("execution(public * oa..*Ctrl.search*(..)) || execution(public * oa..*Ctrl.getByPage*(..)) || execution(public * oa..*Ctrl.getAllByPage*(..))")
public void controllerMethodPointcut() {
}
/**
* 请求方法前打印内容
*
* @param joinPoint
*/
@Before("controllerMethodPointcut()")
public void doBefore(JoinPoint joinPoint) {
// 请求开始时间
startTime.set(System.currentTimeMillis());
key.set(uri);
}
/**
* 在方法执行后打印返回内容
*
* @param obj
*/
@AfterReturning(returning = "obj", pointcut = "controllerMethodPointcut()")
public void doAfterReturing(Object obj) {
long costTime = System.currentTimeMillis() - startTime.get();
String uri = key.get();
startTime.remove();
key.remove();
}
}
本文地址:https://blog.****.net/qianhuan_/article/details/107699001
下一篇: Spring声明式事务原理解析