目录
1. 使用场景一:线程隔离
2. 使用场景二:使用ThreadLocal进行跨函数数据传递
3. ThreadLocal导致的内存泄漏问题
4. ThreadLocal在Spring框架中的应用
5. 扩展:InheritableThreadLocal
转载:【Java】ThreadLocal使用场景介绍以及关于内存泄漏的探讨 - 掘金
1. 使用场景一:线程隔离
【需求】假设我们有个UserService,[方法birthDate]中:
- 通过用户id,拿到用户的生日。
- 新建一个SimpleDateFormat对象df。
- df.format(用户生日)。
【方式1】 如果我们新建10个线程,每个线程都在运行[方法birthDate],那么相当于每个线程都会新建一个df:
方式1导致的问题
:但如果我们有1000个task需要执行,那么直接创建1000个线程,显然不太合理,通常情况下,我们会用线程池的方式执行任务。
【方式2】 如果我们新建一个核心线程数为10的线程池,往里面提交1000个任务:
方式2导致的问题
:虽然我们使用了线程池的方式,线程数为10,但会创建1000个df对象。
【方式3】 那么我们将SimpleDateFormat提取到方法a的外面,然后用参数的形式传入。这样解决了每次都会创建df对象的开销,但是SimpleDateFormat是线程不安全的,即需要对这个对象加锁以保证线程安全。
方式3导致的问题
:给全局的SimpleDateFormat加锁,会使得同一时间只有一个线程能拿到这个对象,导致效率低下。
那么有没有一种更折中的方案,即既不需要在方法内创建df以致于极端情况下要多达1000次的创建,也不要只有1个df对象,以至于每个线程用到它的时候都要排队拿?
答案是有的,即使用ThreadLocal
(【方式4】 ):
由图可知,我们希望每个线程有自己的df对象,这样既不需要每个task都创建一次(节省了开销),也不需要每个thread相互抢一个df(提高了效率):
什么是
ThreadLocal
:如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。“线程本地变量”可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作“线程本地变量”的时候,线程各自操作的是自己的本地值,从而规避了线程安全问题。
方式4的代码如下: 首先是新建一个Utils类,用来存放ThreadLocal,主要是重写了initialValue()方法,使得在新生成value的时候会自动生成一个SimpleDateFormat。
public class DateFormatThreadLocalUtils {
public static final ThreadLocal<SimpleDateFormat> df = new ThreadLocal<>() {
@Override
protected SimpleDateFormat initialValue() {
System.out.println("new SimpleDateFormat.....");
return new SimpleDateFormat("yyyy-MM-dd");
}
};
}
ps. 如果是JDK8+,可以用lmbda表达式写:
public class DateFormatThreadLocalUtils {
public static ThreadLocal<SimpleDateFormat> df = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
}
UserService类:
- 可以看到birthDate中使用到的sdf是从ThreadLocal中拿到的。
- 如果上述的ThreadLocal没有重写initialValue方法,那么在使用的时候可以先判断从ThreadLocal get出来的SimpleDateFormat是否为空,如果为空,再new,再set回ThreadLocal中也是可以的。
public class UserService {
public String birthDate(int userId) {
Date birthDate = getBirthDay(userId);
SimpleDateFormat sdf = DateFormatThreadLocalUtils.df.get();
return sdf.format(birthDate);
}
public Date getBirthDay(int userId) {
// todo, return a Date
}
}
测试:Task有1000个,核心线程数为10,那么上述的SimpleDateFormat只会new 10次,因为它是每个线程独有的。
public class UserServiceMain {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i ++) {
executorService.submit(() -> {
String birthDate = (new UserService()).birthDate(id);
System.out.println(Thread.currentThread().getName() + ": " + birthDate);
});
}
}
}
【总结】Use Case-1其实就是用了空间换取时间,给每个thread都分配一个SimpleDateFormat实例,避免了线程间相互换资源造成的效率问题。
在上面的使用场景中,除了以上的例子,还可以为每个线程绑定一个数据库连接等。
2. 使用场景二:使用ThreadLocal进行跨函数数据传递
假设我们有个API,从前端接收到request,然后经过一系列个service,但每个service都需要user这个参数:
那么可以有几种实现:
- 每个service的方法都带上user这个参数,以此来传递。
- 可以新建一个ThreadLocal,然后在第1个service中将user值set到ThreadLocal中,往后的service就可以直接从ThreadLocal中获取。
代码示例:
public class UserThreadLocalUtils {
public static final ThreadLocal<String> USER_ID_HOLDER = new ThreadLocal<>();
}
比如我们写一个Filter,将userId存放到ThreadLocal中:
@Component
public class UserFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
HttpServletRequest request = (HttpServletRequest)servletRequest;
String userId = request.getHeader("userId");
UserThreadLocalUtils.USER_ID_HOLDER.set(userId);
filterChain.doFilter(servletRequest, servletResponse);
} finally {
UserThreadLocalUtils.USER_ID_HOLDER.remove();
}
}
}
那么我们在Controller或是Service中都可以从ThreadLocal中拿:
@Slf4j
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("user")
public boolean get() {
log.info("Get userId = {}", UserThreadLocalUtils.USER_ID_HOLDER.get());
userService.get();
return true;
}
}
@Slf4j
@Service
public class UserService {
public void get() {
log.info("Get userId = {}", UserThreadLocalUtils.USER_ID_HOLDER.get());
}
}
3. ThreadLocal导致的内存泄漏问题
关于ThreadLocal的内存泄漏问题,可以参考以下,写的都非常好:
- 博文:ThreadLocal内存泄露原因分析
- 书《Java高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计模式》,第#1.8.7章
首先:
- 什么是
内存泄漏
?不再用到的内存没有及时释放(归还给系统),就叫作内存泄漏。 - 强引用和弱引用(WeakReference),参考:blog.csdn.net/CSDN_DK317/…
强引用
即类似“Object obj=new Object()”这类的引用,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。弱引用(WeakReference)
,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
其次,书中,使用了一个小例子来解释了ThreadLocal中的引用关系:
当线程tn尝试跑上述方法时,会有两个引用:
- 【引用1】 线程tn
-->
funcA()-->
local-- (强引用) -->
ThreadLocal实例。 - 【引用2】 local.set(100)
-->
线程tn-->
ThreadLocalMap-->
Entry实例-- 其Key以(弱引用)包装的方式指向 -->
ThreadLocal实例。
思考一个问题,【引用2】中为什么是弱引用?即为什么ThreadLocal中的实现,ThreadLocalMap中的key需要指向的ThreadLocal为弱引用?
- 当方法funcA()执行完毕后,强引用local的值也就没有了。即【引用1】没有了。
如果【引用2】的方式是强引用的话,那么就会造成ThreadLocal的实例回收,需要依赖线程tn的生命周期。
因此,【引用2】的Entry引用关系为弱引用,即ThreadLocal的实例回收不应该依赖tn线程的结束而回收。-->
即【引用1】的结束,就意味着【引用2】中Entry实例中的key指向ThreadLocal,会在下一次GC发生的时候,就回收掉了!
再思考一个问题,假设GC发生了,ThreadLocal对象被回收了(【引用1】的关系没有了,而【引用2】为弱引用),那么Entry中的key指向了null,那么这个Entry也就没有用了,它会在什么时候被释放?
- 后续当ThreadLocal的get()、set()或remove()被调用时,ThreadLocalMap的内部代码会清除这些Key为null的Entry,从而完成相应的内存释放。
-->
这也就是为什么我们需要调用remove()的原因。
最佳实践:使用static + final修饰,并且调用remove()进行显示的释放操作
在《Java高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计模式》一书中关于ThreadLocal,建义使用static final修饰ThreadLocal对象:
使用static、final修饰ThreadLocal实例也会带来副作用,使得Thread实例内部的ThreadLocalMap中Entry的Key在Thread实例的生命期内将始终保持为非null,从而导致Key所在的Entry不会被自动清空,这就会让Entry中的Value指向的对象一直存在强引用,于是Value指向的对象在线程生命期内不会被释放,最终导致内存泄漏。所以,在使用完static、final修饰的ThreadLocal实例之后,必须调用remove()来进行显式的释放操作。
此外,除了添加static final修饰外,还常常添加private,主要目的是缩小使用的范围,尽可能不让他人引用。
即如何在web项目中安全的使用ThreadLocal,可以考虑使用Filter,在finally的时候remove掉:
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
//set ThreadLocal variable
filterChain.doFilter(servletRequest, servletResponse);
} finally {
//remove threadLocal variable.
}
}
4. ThreadLocal在Spring框架中的应用
Spring框架中也使用了很多ThreadLocal来hold一些context,如:
- LocaleContextHolder
- TransactionContextHolder
- RequestContextHolder
- SecurityContextHolder
- DateTimeContextHolder
5. 扩展:InheritableThreadLocal
在JDK 1.2后,新引入了一个类,叫InheritableThreadLocal,这个类继承了ThreadLocal,从名字可以看出,Inheritable是可继承的意思。
ThreadLocal中,每一个线程在获取本地值时,都会将ThreadLocal实例作为Key从自己拥有的ThreadLocalMap中获取值,别的线程无法访问自己的ThreadLocalMap实例,自己也无法访问别人的ThreadLocalMap实例,达到相互隔离,互不干扰。
那么InheritableThreadLocal是线程中生成的子线程,也会共享该value。即在父线程中set的值,在子线程中通过get方法也可以获取到。
比如slf4j中的MDC类,有个变量叫MDCAdapter
,它有个实现类叫BasicMDCAdapter
,实质上就是一个InheritableThreadLocal
:
public class BasicMDCAdapter implements MDCAdapter {
private InheritableThreadLocal<Map<String, String>> inheritableThreadLocal = new InheritableThreadLocal<Map<String, String>>() {
protected Map<String, String> childValue(Map<String, String> parentValue) {
return parentValue == null ? null : new HashMap(parentValue);
}
};
...
}
而MDC,在log中是很重要的,比如像sleuth中的trace_id的打印,用到的就是MDC,即我们可以往MDC中set trace_id,然后在日志appender中打印出来。
但是,InheritableThreadLocal会在某种情况下失效,即子线程并不能在所有场景下都能拿到父线程set的值。但也有解决方法,具体参考文章:
- InheritableThreadLocal线程池下失效问题解决
- 遇到线程池InheritableThreadLocal就废了,该怎么办?
作者:伊丽莎白2015
链接:https://juejin.cn/post/7132068313449889805
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。