Java面试题必备知识之ThreadLocal
老套路,先列举下关于threadlocal常见的疑问,希望可以通过这篇学习笔记来解决这几个问题:
- threadlocal是用来解决什么问题的?
- 如何使用threadlocal?
- threadlocal的实现原理是什么?
- 可否举几个实际项目中使用threadlocal的案例?
基础知识
threadlocal是线程局部变量,和普通变量的不同在于:每个线程持有这个变量的一个副本,可以独立修改(set方法)和访问(get方法)这个变量,并且线程之间不会发生冲突。
类中定义的threadlocal实例一般会被private static
修饰,这样可以让threadlocal实例的状态和thread绑定在一起,业务上,一般用threadlocal包装一些业务id(user id或事务id)——不同的线程使用的id是不相同的。
如何使用
case1
从某个角度来看,threadlocal为java并发编程提供了额外的思路——避免并发,如果某个对象本身是非线程安全的,但是你想实现多线程同步访问的效果,例如simpledateformat,你可以使用threadlocal变量。
public class foo { // simpledateformat is not thread-safe, so give one to each thread private static final threadlocal<simpledateformat> formatter = new threadlocal<simpledateformat>(){ @override protected simpledateformat initialvalue() { return new simpledateformat("yyyymmdd hhmm"); } }; public string formatit(date date) { return formatter.get().format(date); } }
注意,这里针对每个线程只需要初始化一次simpledateformat对象,其实跟在自定义线程中定义一个simpledateformat成员变量,并在线程初始化的时候new这个对象,效果是一样的,只是这样看起来代码更规整。
case2
之前在yunos做酷盘项目的数据迁移时,我们需要按照用户维度去加锁,每个线程在处理迁移之前,都需要先获取当前用户的锁,每个锁的key是带着用户信息的,因此也可以使用threadlocal变量实现:
case3
下面这个例子,我们定义了一个myrunnable对象,这个myrunnable对象会被线程1和线程2使用,但是通过内部的threadlocal变量,每个线程访问到的整数都是自己单独的一份。
package org.java.learn.concurrent.threadlocal; /** * @author duqi * @createtime 2018-12-29 23:25 **/ public class threadlocalexample { public static class myrunnable implements runnable { private threadlocal<integer> threadlocal = new threadlocal<integer>(); @override public void run() { threadlocal.set((int) (math.random() * 100d)); try { thread.sleep(2000); } catch (interruptedexception e) { } system.out.println(threadlocal.get()); } } public static void main(string[] args) throws interruptedexception { myrunnable sharedrunnableinstance = new myrunnable(); thread thread1 = new thread(sharedrunnableinstance); thread thread2 = new thread(sharedrunnableinstance); thread1.start(); thread2.start(); thread1.join(); //wait for thread 1 to terminate thread2.join(); //wait for thread 2 to terminate } }
threadlocal关键知识点
源码分析
threadlocal是如何被线程使用的?原理如下图所示:thread引用和threadlocal引用都在栈上,thread引用会引用一个threadlocalmap对象,这个map中的key是threadlocal对象(使用weakreference包装),value是业务上变量的值。
首先看java.lang.thread
中的代码:
public class thread implements runnable { //......其他源码 /* threadlocal values pertaining to this thread. this map is maintained by the threadlocal class. */ threadlocal.threadlocalmap threadlocals = null; /* * inheritablethreadlocal values pertaining to this thread. this map is maintained by the inheritablethreadlocal class. */ threadlocal.threadlocalmap inheritablethreadlocals = null; //......其他源码
thread中的threadlocals变量指向的是一个map,这个map就是threadlocal.threadlocalmap,里面存放的是跟当前线程绑定的threadlocal变量;inheritablethreadlocals的作用相同,里面也是存放的threadlocal变量,但是存放的是从当前线程的父线程继承过来的threadlocal变量。
在看java.lang.threadlocal
类,主要的成员和接口如下:
-
withinitial方法,java 8以后用于初始化threadlocal的一种方法,在外部调用get()方法的时候,会通过supplier确定变量的初始值;
public static <s> threadlocal<s> withinitial(supplier<? extends s> supplier) { return new suppliedthreadlocal<>(supplier); }
-
get方法,获取当前线程的变量副本,如果当前线程还没有创建该变量的副本,则需要通过调用
initialvalue
方法来设置初始值;get方法的源代码如下,首先通过当前线程获取当前线程对应的map,如果map不为空,则从map中取出对应的entry,然后取出对应的值;如果map为空,则调用setinitialvalue设置初始值;如果map不为空,当前threadlocal实例对应的entry为空,则也需要设置初始值。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(); }
-
set方法,跟get方法一样,先获取当前线程对应的map,如果map为空,则调用createmap创建map,否则将变量的值放入map——key为当前这个threadlocal对象,value为变量的值。
public void set(t value) { thread t = thread.currentthread(); threadlocalmap map = getmap(t); if (map != null) map.set(this, value); else createmap(t, value); }
-
remove方法,删除当前线程绑定的这个副本
public void remove() { threadlocalmap m = getmap(thread.currentthread()); if (m != null) m.remove(this); }
数字0x61c88647,这个值是hash_increment的值,普通的hashmap是使用链表来处理冲突的,但是threadlocalmap是使用线性探测法来处理冲突的,hash_increment就是每次增加的步长,根据参考资料1所说,选择这个数字是为了让冲突概率最小。
/** * the difference between successively generated hash codes - turns * implicit sequential thread-local ids into near-optimally spread * multiplicative hash values for power-of-two-sized tables. */ private static final int hash_increment = 0x61c88647;
父子进程数据共享
inheritablethreadlocal主要用于子线程创建时,需要自动继承父线程的threadlocal变量,实现子线程访问父线程的threadlocal变量。inheritablethreadlocal继承了threadlocal,并重写了childvalue、getmap、createmap三个方法。
public class inheritablethreadlocal<t> extends threadlocal<t> { /** * 创建线程的时候,如果需要继承且父线程中thread-local变量,则需要将父线程中的threadlocal变量一次拷贝过来。 */ protected t childvalue(t parentvalue) { return parentvalue; } /** * 由于重写了getmap,所以在操作inheritablethreadlocal变量的时候,将只操作thread类中的inheritablethreadlocals变量,与threadlocals变量没有关系 **/ threadlocalmap getmap(thread t) { return t.inheritablethreadlocals; } /** * 跟getmap类似,set或getinheritablethreadlocal变量的时候,将只操作thread类中的inheritablethreadlocals变量 */ void createmap(thread t, t firstvalue) { t.inheritablethreadlocals = new threadlocalmap(this, firstvalue); } }
关于childvalue多说两句,拷贝是如何发生的?
首先看thread.init方法,
private void init(threadgroup g, runnable target, string name, long stacksize, accesscontrolcontext acc, boolean inheritthreadlocals) { //其他源码 if (inheritthreadlocals && parent.inheritablethreadlocals != null) this.inheritablethreadlocals = threadlocal.createinheritedmap(parent.inheritablethreadlocals); /* stash the specified stack size in case the vm cares */ this.stacksize = stacksize; /* set thread id */ tid = nextthreadid(); }
然后看threadlocal.createinheritedmap方法,最终会调用到newthreadlocalmap方法,这里inheritablethreadlocal对childvalue做了重写,可以看出,这里确实是将父线程关联的threadlocalmap中的内容依次拷贝到子线程的threadlocalmap中了。
private threadlocalmap(threadlocalmap parentmap) { entry[] parenttable = parentmap.table; int len = parenttable.length; setthreshold(len); table = new entry[len]; for (int j = 0; j < len; j++) { entry e = parenttable[j]; if (e != null) { @suppresswarnings("unchecked") threadlocal<object> key = (threadlocal<object>) e.get(); if (key != null) { object value = key.childvalue(e.value); entry c = new entry(key, value); int h = key.threadlocalhashcode & (len - 1); while (table[h] != null) h = nextindex(h, len); table[h] = c; size++; } } } }
threadlocal对象何时被回收?
threadlocalmap中的key是threadlocal对象,然后threadlocal对象时被weakreference包装的,这样当没有强引用指向该threadlocal对象之后,或者说map中的threadlocal对象被判定为弱引用可达时,就会在垃圾收集中被回收掉。看下entry的定义:
static class entry extends weakreference<threadlocal<?>> { /** the value associated with this threadlocal. */ object value; entry(threadlocal<?> k, object v) { super(k); value = v; } }
threadlocal和线程池一起使用?
threadlocal对象的生命周期跟线程的生命周期一样长,那么如果将threadlocal对象和线程池一起使用,就可能会遇到这种情况:一个线程的threadlocal对象会和其他线程的threadlocal对象串掉,一般不建议将两者一起使用。
案例学习
dubbo中对threadlocal的使用
我从dubbo中找到了threadlocal的例子,它主要是用在请求缓存的场景,具体代码如下:
@activate(group = {constants.consumer, constants.provider}, value = constants.cache_key) public class cachefilter implements filter { private cachefactory cachefactory; public void setcachefactory(cachefactory cachefactory) { this.cachefactory = cachefactory; } @override public result invoke(invoker<?> invoker, invocation invocation) throws rpcexception { if (cachefactory != null && configutils.isnotempty(invoker.geturl().getmethodparameter(invocation.getmethodname(), constants.cache_key))) { cache cache = cachefactory.getcache(invoker.geturl(), invocation); if (cache != null) { string key = stringutils.toargumentstring(invocation.getarguments()); object value = cache.get(key); if (value != null) { if (value instanceof valuewrapper) { return new rpcresult(((valuewrapper)value).get()); } else { return new rpcresult(value); } } result result = invoker.invoke(invocation); if (!result.hasexception()) { cache.put(key, new valuewrapper(result.getvalue())); } return result; } } return invoker.invoke(invocation); }
可以看出,在rpc调用(invoke)的链路上,会先使用请求参数判断当前线程是否刚刚发起过同样参数的调用——这个调用会使用threadlocalcache保存起来。具体的看,threadlocalcache的实现如下:
package org.apache.dubbo.cache.support.threadlocal; import org.apache.dubbo.cache.cache; import org.apache.dubbo.common.url; import java.util.hashmap; import java.util.map; /** * threadlocalcache */ public class threadlocalcache implements cache { //threadlocal里存放的是参数到结果的映射 private final threadlocal<map<object, object>> store; public threadlocalcache(url url) { this.store = new threadlocal<map<object, object>>() { @override protected map<object, object> initialvalue() { return new hashmap<object, object>(); } }; } @override public void put(object key, object value) { store.get().put(key, value); } @override public object get(object key) { return store.get().get(key); } }
rocketmq
在rocketmq中,我也找到了threadlocal的身影,它是用在消息发送的场景,mqclientapiimpl是rmq中负责将消息发送到服务端的实现,其中有一个步骤需要选择一个具体的队列,选择具体的队列的时候,不同的线程有自己负责的index值,这里使用了threadlocal的机制,可以看下threadlocalindex的实现:
package org.apache.rocketmq.client.common; import java.util.random; public class threadlocalindex { private final threadlocal<integer> threadlocalindex = new threadlocal<integer>(); private final random random = new random(); public int getandincrement() { integer index = this.threadlocalindex.get(); if (null == index) { index = math.abs(random.nextint()); if (index < 0) index = 0; this.threadlocalindex.set(index); } index = math.abs(index + 1); if (index < 0) index = 0; this.threadlocalindex.set(index); return index; } @override public string tostring() { return "threadlocalindex{" + "threadlocalindex=" + threadlocalindex.get() + '}'; } }
总结
这篇文章主要是解决了关于threadlocal的几个问题:(1)具体的概念是啥?(2)在java开发中的什么场景下使用?(3)threadlocal的实现原理是怎样的?(4)开源项目中有哪些案例可以参考?不知道你是否对这几个问题有了一定的了解呢?如果还有疑问,欢迎交流。
参考资料
- why 0x61c88647?
- java threadlocal
- when and how should i use a threadlocal variable?
- 技术小黑屋:理解java中的threadlocal
- 深入分析threadlocal的内存泄漏问题
- 《java并发编程实战》
- inheritablethreadlocal详解
- threadlocal详解
- threadlocal的使用场景
- 数据结构:哈希表
本号专注于后端技术、jvm问题排查和优化、java面试题、个人成长和自我管理等主题,为读者提供一线开发者的工作和成长经验,期待你能在这里有所收获。
上一篇: windows下如何使用Git
下一篇: Spark的lazy特性有什么意义呢?