原理: Current Thread当前线程中有一个ThreadLocalMap对象,它的key是ThreadLocal的弱引用,Value是ThreadLocal调用set方法设置的对象值。每一个线程维护一个各自的ThreadLocalMap
,所以多个线程之间变量相互隔离,互不干扰。
缺点: 存在内存泄漏问题,因为当ThreadLocal设置为null后,ThreadLocalMap的key的弱引用指向了null,又没有任何的强引用指向threadlocal,所以threadlocal会被GC回收掉。但是,ThreadLocalMap的Value不会被回收,CurrentThread当前线程的强引用指向了ThreadLocalMap,进而指向了这个Entry
结论: 只要currentThread被GC回收,就不会出现内存泄漏。
但是在currentThread被GC回收之前,threadlocal设置为null之后的这段时间里,Value不会被回收,比如当使用线程池的时候,线程结束不会被GC回收,会被继续复用,那这个Value肯定还会继续存在。如果这个Value很大的情况下,可能就会内存泄漏。
虽然threadlocal的set和get方法执行时会清除key为null的value,但是如果当前线程在使用中没有调用threadlocal的set或者get方法一样可能会内存泄漏。
跟线程池结合使用的注意事项: 因为线程池中线程复用的情况,本次的threadlocal中可能已经存在数据,所以上一次使用完threadlocal的变量后,要调用threadlocal的remove方法清除value
。而且要注意调用完remove后应该保证不会再调用get方法。
ThreadLocal提供了线程专有对象,可以在整个线程生命周期中随时取用,极大地方便了一些逻辑的实现。
常见的ThreadLocal用法主要有两种:
这里,以PageHelper插件的源代码中的分页参数设置与使用为例说明。
设置分页参数代码:
public abstract class PageMethod {
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal<Page>();
protected static void setLocalPage(Page page) {
LOCAL_PAGE.set(page);
}
public static <T> Page<T> getLocalPage() {
return LOCAL_PAGE.get();
}
public static <E> Page<E> startPage(Object params) {
Page<E> page = PageObjectUtil.getPageFromObject(params, true);
//当已经执行过orderBy的时候
Page<E> oldPage = getLocalPage();
if (oldPage != null && oldPage.isOrderByOnly()) {
page.setOrderBy(oldPage.getOrderBy());
}
setLocalPage(page);
return page;
}
}
使用分页参数代码:
public abstract class AbstractHelperDialect extends AbstractDialect implements Constant {
public <T> Page<T> getLocalPage() {
return PageHelper.getLocalPage();
}
@Override
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
String sql = boundSql.getSql();
Page page = getLocalPage();
//支持 order by
String orderBy = page.getOrderBy();
if (StringUtil.isNotEmpty(orderBy)) {
pageKey.update(orderBy);
sql = OrderByParser.converToOrderBySql(sql, orderBy);
}
if (page.isOrderByOnly()) {
return sql;
}
return getPageSql(sql, page, pageKey);
}
}
使用分页插件代码:
public PageInfo<UserVO> queryUser(UserReq userReq, int pageNum, int pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<UserVO> users = userDao.queryUser(userReq);
PageInfo<UserDO> pageInfo = new PageInfo<>(users);
return pageInfo;
}
如果要把分页参数通过函数参数逐级传给查询语句,除非修改MyBatis相关接口函数,否则是不可能实现的。
在写日期格式化工具函数时,首先想到的写法如下:
// 日期格式化器
private static final String FORMATTER = "yyyy-MM-dd";
// 格式化日期
public static String format(Date date) {
return new SimpleDateFormat(FORMATTER).format(date);
}
其中,每次调用都要初始化DateFormat导致性能较低,把DateFormat定义成常量后的写法如下:
private static final String FORMATTER = "yyyy-MM-dd";
private static final DateFormat FORMAT = new SimpleDateFormat(FORMATTER);
public static String format(Date date) {
return FORMAT.format(date);
}
由于SimpleDateFormat是非线程安全的,当多线程同时调用formatDate函数时,会导致返回结果与预期不一致。如果采用ThreadLocal定义线程专有对象,优化后的代码如下:
private static final String FORMATTER = "yyyy-MM-dd";
private static final ThreadLocal<DateFormat> LOCAL_DATE_FORMAT = ThreadLocal.withInitial(() -> new SimpleDateFormat(FORMATTER));
public static String format(Date date) {
return LOCAL_DATE_FORMAT.get().format(date);
}
这是在没有线程安全的日期格式化工具类之前的实现方法。在JDK8以后,建议使用DateTimeFormatter代替SimpleDateFormat,因为SimpleDateFormat是线程不安全的,而DateTimeFormatter是线程安全的。当然,也可以采用第三方提供的线程安全日期格式化函数,比如apache的DateFormatUtils工具类。
注意:
ThreadLocal有一定的内存泄露的风险,尽量在业务代码结束前调用remove函数进行数据清除。
public class ThreadLocalApp {
private final static ThreadLocal<Integer> LOCAL_VALUE = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
for (int i = 1; i <= 5; i++) {
Thread thread = new Thread(() -> {
System.out.println(MessageFormat.format("{0} init value:{1}", Thread.currentThread().getName(), LOCAL_VALUE.get()));
for (int k = 0; k < 10; k++) {
LOCAL_VALUE.set(LOCAL_VALUE.get() + k);
}
System.out.println(MessageFormat.format("{0} summation value:{1}", Thread.currentThread().getName(), LOCAL_VALUE.get()));
});
thread.setName("Thread-" + i);
thread.start();
LOCAL_VALUE.set(LOCAL_VALUE.get() + i);
}
System.out.println(MessageFormat.format("{0} summation value:{1}", Thread.currentThread().getName(), LOCAL_VALUE.get()));
}
}
程序运行结果输出
main summation value:15
Thread-5 init value:0
Thread-3 init value:0
Thread-1 init value:0
Thread-5 summation value:45
Thread-1 summation value:45
Thread-4 init value:0
Thread-2 init value:0
Thread-4 summation value:45
Thread-3 summation value:45
Thread-2 summation value:45
// java8 之前
private static final ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 100;
}
};
// java8中
private static final ThreadLocal<Integer> integerThreadLocal = ThreadLocal.withInitial(() -> 100);