写给Android开发者的ThreadLocal介绍

前几年在分析Android消息机制源码时,就碰到了ThreadLocal,但是当时就只引用了《Android开发艺术探索》中结论,没有深入细致地研究它的使用和细节。作为Android开发者而言,日常开发中应该很少使用到ThreadLocal类本身,应该是Java后台开发兄弟会用的多一点。但是,理解了ThreadLocal,可以加深对于Looper的理解。

对于ThreadLocal,日常开发中一般有两种使用场景:

  • 每个线程需要一个独享的对象:比如Android中的Looper,后端中常用的工具类(如SimpleDateFormat)
  • 每个线程内需要保存全局变量:都知道Java服务端Controller作为接口响应入口,Service处理业务逻辑,Repository提供数据库CRUD数据接口,类似在拦截器中获取的用户信息这类共享数据,就可以放置到ThreadLocal中,就不用一层一层的通过参数传递下去。

下面我们就针对这两种使用场景举例说明ThreadLocal的使用:

1. 每个线程需要一个独享的对象

对于拿到时间戳,我们通常需要通过SimpleDateFormat类来将其转换成相应的日期格式,假设我们有如下一个工具类:

public class DateUtils {

    public static String format(long milliSeconds) {
      	SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(new Date(milliSeconds));
    }
}

现在我们通过线程池来模拟多线程环境:

public class ThreadLocalTest2 {

    private static ExecutorService threadPool = Executors.newFixedThreadPool(5);

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            int finalI = i;
            threadPool.submit(() -> {
                String result = DateUtils.format(finalI * 1000);
                System.out.println(result);
            });
        }

        threadPool.shutdown();
    }

}

运行后的输出结果如下:

1970-01-01 08:00:03
1970-01-01 08:00:00
1970-01-01 08:00:02
1970-01-01 08:00:04
1970-01-01 08:00:01
1970-01-01 08:00:05
1970-01-01 08:00:08
1970-01-01 08:00:06
1970-01-01 08:00:09
1970-01-01 08:00:07

现在一切都是正常的,但是由于每次调用format方法都是创建一个新的SimpleDateFormat对象,这样是没有必要的。我们可以有如下修改:

public class DateUtils {

    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");

    public static String format(long milliSeconds) {
        return dateFormat.format(new Date(milliSeconds));
    }
}

现在再运行代码:

1970-01-01 08:00:02
1970-01-01 08:00:02
1970-01-01 08:00:02
1970-01-01 08:00:02
1970-01-01 08:00:07
1970-01-01 08:00:02
1970-01-01 08:00:09
1970-01-01 08:00:09
1970-01-01 08:00:07
1970-01-01 08:00:07

从结果来看,明显这种写法已经出问题了。那么该怎么去解决这个问题呢?接下来,就轮到我们今天的主人公ThreadLocal登场啦!

class DateUtils {

    private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));

    public static String format(long milliSeconds) {
        return threadLocal.get().format(new Date(milliSeconds));
    }
}

现在再运行:

1970-01-01 08:00:00
1970-01-01 08:00:01
1970-01-01 08:00:03
1970-01-01 08:00:05
1970-01-01 08:00:06
1970-01-01 08:00:04
1970-01-01 08:00:09
1970-01-01 08:00:02
1970-01-01 08:00:07
1970-01-01 08:00:08

这样,每个线程之间就互不干扰啦,因为每个进入format()方法的线程所使用的的SimpleDateFormat对象都是线程独享的,相互之间互不干扰的。

2. 线程内保存全局变量

假定我们有一个UserInfo类,用来表示用户的信息:

class UserInfo {
    int id;

    public UserInfo(int id) {
        this.id = id;
    }
}

再有一个UseInfoHolder类,持有ThreadLocal对象:

class UserInfoHolder {

    static final ThreadLocal<UserInfo> holder = new ThreadLocal<>();
}

构造三个Service,分别表示处理逻辑:

class Service1 {


    public void process() {
        UserInfo userInfo = new UserInfo(1);
        UserInfoHolder.holder.set(userInfo);
        new Service2().process();
    }
}

class Service2 {

    public void process() {
        System.out.println("in Service2 : " + UserInfoHolder.holder.get().id);
        new Service3().process();
    }
}

class Service3 {

    public void process() {
        System.out.println("in Service3 : " + UserInfoHolder.holder.get().id);
    }
}

在Service1中,我们为UserInfoHolder中的ThreadLocal设置了值;在Service2、Service3中,我们可以直接通过UserInfoHolder中的ThreadLocal获取设置的UserInfo对象,从而做到共享。

最后写上main测试方法:

public class ThreadLocalTest3 {
    public static void main(String[] args) {
        new Service1().process();
    } 
}

运行结果如下:

in Service2 : 1
in Service3 : 1

3. 比较两种用法在写法层面上的不同

对于第一种,我们一般会在创建ThreadLocal对象时,直接给定了线程间独享的对象;对于第二种,我们仅仅创建了ThreadLocal对象,是后面通过set方法设置进去的。或者说,我们可以通过这两种方式来给ThreadLocal设置值。

4. ThreadLocal类源码分析(基于JDK1.8.0_231)

我们就第二种使用方式为例,入手ThreadLocal类的分析。先看ThreadLocal类的构造器:

/**
 * Creates a thread local variable.
 * @see #withInitial(java.util.function.Supplier)
 */
public ThreadLocal() {
}

啥都没做,那就看set()方法:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

总结一下这里面的逻辑:

  • ThreadLocalMap,并且存放在Thread类中
    在这里插入图片描述
  • set方法的逻辑很简单,如果当前Thread中的threadLocals不为空,则直接将set进来的value放入到ThreadLocalMap中去;如果为空,则创建ThreadLocalMap对象,最后再将set进来的value放入到新创建的ThreadLocalMap中去。

那么理所当然,我们接下来的分析重点就落到了ThreadLocalMap类。我们从ThreadLocalMap的set方法开始:

private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

我们首先注意一点,ThreadLocalMap的set方法传入的两个参数分别是谁:key是ThreadLocal,value是往ThreadLocal中set的值。也就是说,形成了ThreadLocal对象到往ThreadLocal中set的值两者之间的映射。这个地方的检索我们会发现和我们常见的HashMap有所不同。总之,我们目前可以得到的信息是:ThreadLocalMap中存储着ThreadLocal到放入其中value的映射,并且ThreadLocalMap是存放在Thread类中

我们可以用下面的图来表示Thread、ThreadLocal以及ThreadLocalMap之间的关系:

在这里插入图片描述
有了前面的基础,我们再来看get()方法的实现就很轻松:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
  • 当前线程的ThreadLocalMap是否为null,如果不为null,则在ThreadLocalMap中进行查找,查找成功直接返回;否则进入下一步。
  • 调用setInitialValue()方法。
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

  protected T initialValue() {
        return null;
  }

可以看出,setInitialValue的实现几乎和set()方法一模一样。ThreadLocal类中的initialValue()方法的默认实现是直接返回null。这个时候我们可以看下第一种使用方式的ThreadLocal.withInitial()的实现:

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

 static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

        private final Supplier<? extends T> supplier;

        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }

        @Override
        protected T initialValue() {
            return supplier.get();
        }
    }

这样,对于实现就很清晰了。两种使用方式也都联系起来了。

5. 防止ThreadLocal中的内存泄漏

我们再来看ThreadLocalMap的结构:

static class ThreadLocalMap {

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    
    private Entry[] table;
    private static final int INITIAL_CAPACITY = 16;
    private int size = 0;
}

也就是说,ThreadLocalMap底层会维护一个Entry数组,而Entry本身却是WeakReference的子类,并且在构造器中将ThreadLocal传给了父类WeakReference。也就是说,Entry对于ThreadLocal持有的引用是弱引用,它并不会影响GC对于ThreadLocal对象的回收。但是对于value,依旧是强应用,如果不及时清理释放,是会导致内存泄漏的。所以,我们在不使用时,最好调用ThreadLocal的remove方法:

public class ThreadLocal{
    
  	public void remove() {
       ThreadLocalMap m = getMap(Thread.currentThread());
       if (m != null)
          m.remove(this);
    }

		static class ThreadLocalMap {
      
      private void remove(ThreadLocal<?> key) {
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        // 查找以key为键Entry对象
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
              	// 这里的clear()方法实际上Reference中提供的方法
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
   }  
      
    
}
  
public abstract class Reference<T>{
  	public void clear() {
        this.referent = null;
    }
}

在这里插入图片描述

然后在expungeStaleEntry()方法里:进行了各种置null操作。

实际上在ThreadLocalMap类的set方法中:
在这里插入图片描述

而replaceStaleEntry方法里会有这样一行代码:

在这里插入图片描述

也就是说,在每次调用set方法的时候也会去做相应防止内存泄漏的检查。

最后,分享一下Spring源码中一处对于ThreadLocal的规范使用实例:

在这里插入图片描述

在finally代码块中进行了remove操作。

6. Android消息机制的Looper类中ThreadLocal使用
public static void prepare() {
    prepare(true);
}

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

 /**
     * Return the Looper object associated with the current thread.  Returns
     * null if the calling thread is not associated with a Looper.
     */
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

可以看到,Looper对象实际上是通过ThreadLocal来进行存取的,其真实存放在Thread对象中ThreadLocalMap中,这样再回过头来理解消息机制,印象会更加深刻。

7. ThreadLocalMap的实现算法

这里给大家推荐一篇大佬的文章,对于ThreadLocalMap底层的实现算法做了很详细的注释:https://www.cnblogs.com/micrari/p/6790229.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值