写给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> { /** 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
更多相关文章
- Android(安卓)SQLite使用入门
- Android中执行java命令的方法及java代码执行并解析shell命令
- JOIM:Android通过IPCamera通过互联网实时监控功能的实现
- Android(安卓)MP4取得播放时长的方法
- Android(安卓)AOP(二):AspectJ在Android中实现Aop
- 第10章 Android的消息机制
- 浅谈Java中Collections.sort对List排序的两种方法
- 类和 Json对象
- Python list sort方法的具体使用