前几年在分析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> {        /** 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

更多相关文章

  1. Android(安卓)SQLite使用入门
  2. Android中执行java命令的方法及java代码执行并解析shell命令
  3. JOIM:Android通过IPCamera通过互联网实时监控功能的实现
  4. Android(安卓)MP4取得播放时长的方法
  5. Android(安卓)AOP(二):AspectJ在Android中实现Aop
  6. 第10章 Android的消息机制
  7. 浅谈Java中Collections.sort对List排序的两种方法
  8. 类和 Json对象
  9. Python list sort方法的具体使用

随机推荐

  1. 【51CTO学员故事】6年拿下8个软考证书
  2. 微服务调用链追踪中心搭建
  3. 4.dockerfile
  4. 数据结构之集合和映射
  5. 打印机不断打印出一张张空白纸——好像跟
  6. 进阶 · 那些你必须搞懂的网络基础
  7. Spring Boot应用监控实战
  8. android两个子线程间通信
  9. 如何一句话激怒程序员
  10. 利用Zipkin追踪Mysql数据库调用链