目录

1. 使用场景一:线程隔离

2. 使用场景二:使用ThreadLocal进行跨函数数据传递

3. ThreadLocal导致的内存泄漏问题

4. ThreadLocal在Spring框架中的应用

5. 扩展:InheritableThreadLocal


1. 使用场景一:线程隔离

【需求】假设我们有个UserService,[方法birthDate]中:

  • 通过用户id,拿到用户的生日。
  • 新建一个SimpleDateFormat对象df。
  • df.format(用户生日)。

【方式1】 如果我们新建10个线程,每个线程都在运行[方法birthDate],那么相当于每个线程都会新建一个df:

ThreadLocal使用场景介绍以及关于内存泄漏的探讨-LMLPHP

方式1导致的问题但如果我们有1000个task需要执行,那么直接创建1000个线程,显然不太合理,通常情况下,我们会用线程池的方式执行任务。

【方式2】 如果我们新建一个核心线程数为10的线程池,往里面提交1000个任务:

ThreadLocal使用场景介绍以及关于内存泄漏的探讨-LMLPHP

方式2导致的问题虽然我们使用了线程池的方式,线程数为10,但会创建1000个df对象。

【方式3】 那么我们将SimpleDateFormat提取到方法a的外面,然后用参数的形式传入。这样解决了每次都会创建df对象的开销,但是SimpleDateFormat是线程不安全的,即需要对这个对象加锁以保证线程安全。

方式3导致的问题给全局的SimpleDateFormat加锁,会使得同一时间只有一个线程能拿到这个对象,导致效率低下。

答案是有的,即使用ThreadLocal【方式4】 ):
由图可知,我们希望每个线程有自己的df对象,这样既不需要每个task都创建一次(节省了开销),也不需要每个thread相互抢一个df(提高了效率):

ThreadLocal使用场景介绍以及关于内存泄漏的探讨-LMLPHP

方式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这个参数:

ThreadLocal使用场景介绍以及关于内存泄漏的探讨-LMLPHP

那么可以有几种实现:

  • 每个service的方法都带上user这个参数,以此来传递。
  • 可以新建一个ThreadLocal,然后在第1个service中将user值set到ThreadLocal中,往后的service就可以直接从ThreadLocal中获取。

ThreadLocal使用场景介绍以及关于内存泄漏的探讨-LMLPHP

代码示例:

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的内存泄漏问题,可以参考以下,写的都非常好:

首先:

  • 什么是内存泄漏不再用到的内存没有及时释放(归还给系统),就叫作内存泄漏。
  • 强引用和弱引用(WeakReference),参考:blog.csdn.net/CSDN_DK317/…
    • 强引用即类似“Object obj=new Object()”这类的引用,垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
    • 弱引用(WeakReference),在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

其次,书中,使用了一个小例子来解释了ThreadLocal中的引用关系:

ThreadLocal使用场景介绍以及关于内存泄漏的探讨-LMLPHP

当线程tn尝试跑上述方法时,会有两个引用:

  • 【引用1】 线程tn --> funcA() --> local -- (强引用) --> ThreadLocal实例。
  • 【引用2】 local.set(100) --> 线程tn --> ThreadLocalMap --> Entry实例 -- 其Key以(弱引用)包装的方式指向 --> ThreadLocal实例。

ThreadLocal使用场景介绍以及关于内存泄漏的探讨-LMLPHP

思考一个问题,【引用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对象:

即如何在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的值。但也有解决方法,具体参考文章:

作者:伊丽莎白2015
链接:https://juejin.cn/post/7132068313449889805
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

07-07 14:32