0
点赞
收藏
分享

微信扫一扫

Java多线程编程-使用ThreadLocal存储线程专有对象


原理: Current Thread当前线程中有一个ThreadLocalMap对象,它的key是ThreadLocal的弱引用,Value是ThreadLocal调用set方法设置的对象值。​​每一个线程维护一个各自的ThreadLocalMap​​,所以多个线程之间变量相互隔离,互不干扰。

缺点: 存在内存泄漏问题,因为当ThreadLocal设置为null后,ThreadLocalMap的key的弱引用指向了null,又没有任何的强引用指向threadlocal,所以threadlocal会被GC回收掉。但是,ThreadLocalMap的Value不会被回收,CurrentThread当前线程的强引用指向了ThreadLocalMap,进而指向了这个Entry<key,value>,所以只有当currentThread结束强引用断开后,currentThread、ThreadLocalMap、Entry将全部被GC回收。

结论: 只要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 AnswerTest {
// 线程安全用法
private static final ThreadLocal<SimpleDateFormat> SIMPLE_DATE_FORMAT_THREAD_LOCAL =
ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")
);

@Test
public void doJob() throws Exception {
ExecutorService executorService = Executors.newCachedThreadPool();
List<String> dateStrList = Lists.newArrayList(
"2018-04-01 10:00:01",
"2018-04-02 11:00:02",
"2018-04-03 12:00:03",
"2018-04-04 13:00:04",
"2018-04-05 14:00:05"
);
// 非线程安全用法
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
for (String str : dateStrList) {
executorService.execute(() -> {
try {
// SIMPLE_DATE_FORMAT_THREAD_LOCAL.get().parse(str);
simpleDateFormat.parse(str);
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
} finally {
SIMPLE_DATE_FORMAT_THREAD_LOCAL.remove();
}
});
}

}

}

​​浅谈SimpleDateFormat的线程安全问题​​

 

使用演示

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);

 

Reference

  • ​​ Java 编程技巧之数据结构 ​​


举报

相关推荐

0 条评论