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

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

缺点: 存在内存泄漏问题,因为当ThreadLocal设置为null后,ThreadLocalMap的key的弱引用指向了null,又没有任何的强引用指向threadlocal,所以threadlocal会被GC回收掉。但是,ThreadLocalMap的Value不会被回收,CurrentThread当前线程的强引用指向了ThreadLocalMap,进而指向了这个Entry,所以只有当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 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 编程技巧之数据结构

你可能感兴趣的:(并发编程)