写给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类中
    写给Android开发者的ThreadLocal介绍_第1张图片
  • 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之间的关系:

写给Android开发者的ThreadLocal介绍_第2张图片
有了前面的基础,我们再来看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;
    }
}

写给Android开发者的ThreadLocal介绍_第3张图片

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

实际上在ThreadLocalMap类的set方法中:
写给Android开发者的ThreadLocal介绍_第4张图片

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

写给Android开发者的ThreadLocal介绍_第5张图片

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

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

写给Android开发者的ThreadLocal介绍_第6张图片

在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

你可能感兴趣的:(Java,java,ThreadLocal,Java,android)