一文读懂系列-ThreadLocal
前言
ThreadLocal大家应该也经常听说,但是可能大家在日常工作中真正用到的比较少,其实ThreadLocal在日常工作中的使用中还是有挺多使用场景的,今天我们就介绍下ThreadLocal解决了什么问题?实现原理是怎么样的?它是如何做到静态变量线程多副本的?最后介绍下如何使用。
ThreadLocal解决了什么问题
在日常工作中我们经常会遇到这样的情况,某些类我们在每个线程的很多模块中都需要使用到,但是我们又不希望把这个类作为参数在各个模块中一直传递下去,这样会让代码比较丑陋,并且存在了很多重复的代码。
public String moduleA(Context context){
...
moduleB(Context context);
}
public String moduleB(Context context){
...
moduleC(Context context);
}
public String modulC(Context context){
...
}
上面的代码是不是很丑,那么我们换个写法,既然这个变量一直在各个模块中传递那么麻烦,干嘛不把Context里的变量都定义成静态变量,这样在每个模块中直接使用就行了,就像下面这样。
public class Context {
private static String tranId = new String();
public static void setTranId(String id){
tranId = id;
}
public static String getTranId(){
return tranId;
}
}
public String moduleA(){
...
Context.setTranId(...);
}
public String moduleB(Context context){
String id = Context.getTranId();
...
Context.setTranId(...);
}
public String modulC(Context context){
...
}
等等,明眼人一下就看出来这个代码有线程安全的问题,如果多线程访问一定出问题,
线程1的moduleA set完tranId = 1后
正好这时线程2的moduleA setTranId = 2
此时cpu时间分片给了线程1,线程1继续执行moduleB,那么线程1取出来的tranId就是刚才线程2设置的2而不是之前线程1设置的1了
这样就出了线程安全的问题,那么有没有好的办法既能解决多参数传递的问题,又能解决非线程安全场景下的静态变量共享问题?
答案当时有,那就是使用ThreadLocal定义静态变量。ThreadLocal定义的静态变量是会为每个线程都定义一个线程变量副本的,这样每个线程自己访问自己操作的ThreadLocal静态变量的时候就不会遇到多线程交叉取数和赋值覆盖的问题了。具体实例可以看下面的如何使用ThreaLocal章节。
ThreadLocal实现原理
ThreadLocal的核心原理其实就是为每个线程都单独存储了这个变量,在内部通过一个Map来保存这些局部变量,下面我们来看下ThreadLocal的数据结构。
从图中可以看到在ThreadLocal内部定义了一个ThreadLocalMap静态对象,在ThreadLocalMap中是一个Entry数组private Entry[] table
,在数组中保存每个线程的ThreadLocal变量值,并且在ThreadLocalMap类中还定义了类似HashMap一样的阈值private int threshold
,并有数组扩容等的控制,数组默认大小为16 private static final int INITIAL_CAPACITY = 16
。ThreadLocalMap还提供了Map的set和get方法。
了解了基本的数据结构下面我们看下ThreadLocal核心的get和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;
}
可以看到set方法逻辑很简单,就是获取当前进程对象的threadLocals对象,这里要插播一下Thread类中包含ThreadLocal中静态类ThreadLocalMap的引用,ThreadLocal.ThreadLocalMap threadLocals = null
,这样就可以通过线程找到,线程对应的ThreadLocalMap对象。
再回到ThreadLocal的set方法,从set方法中可以看到,通过当前线程取到ThreadLocalMap后,直接通过ThreadLocalMap的set(ThreadLocal> key, Object value)
方法进行赋值到map中,所以真正的核心逻辑在这里,将ThreadLocal实例和value封装成Entry,将Entry插入table,可以看到插入table的核心逻辑思路就是先获取到当前ThreadLocal的hashcode通过对该hashcode与table的长度得到数组的index位置int i = key.threadLocalHashCode & (len-1)
,table[i]的值为e,e的key值为k,如果k和当前的key代表的ThreadLocal一致,则修改value值并返回,如果k等于null,则说明这个元素已经陈旧了,就调用replaceStaleEntry(key, value, i)
方法删除table中所有陈旧的元素(entry为null)并插入新的元素,如果不满足这两个条件则继续for循环取下一个元素,直到e为null,就把e插入到table的i位置,最后再判断下是否需要扩容。这样整个set的过程就结束了,在其中可以发现ThreadLocalMap虽然也是Map,但是底层实现和HashMap还是不一样的,HashMap当插入值hash散列桶有冲突的时候,会转成链表或者红黑树,但是ThreadLocalMap中没有链表和红黑树结构,如果冲突了会通过nextIndex继续查找下一个空位,这就导致当并发处理的线程非常多并且散列桶冲突很多的时候会导致各个线程在ThreadLocal中查找值的时候非常慢,还会导致table数组的不断扩容。
private void set(ThreadLocal> key, Object value) {
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();
}
下面我们再来看下ThreadLocal.get方法,get()方法比较简单实际上就是获取当前线程的ThreadLocalMap引用,并返回该Entry对应的value值。
ThreadLocal.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.getEntry
private Entry getEntry(ThreadLocal> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
如何使用ThreadLocal
下面我们介绍下日常开发中如何使用ThreadLocal,从上面介绍的使用场景和实现原理,大家应该了解到了ThreadLocal的能够解决什么问题,用在哪些场景下,能够解决我们什么问题。我们通过下面这个例子来介绍下具体如何使用,其实使用起来非常简单我们使用ThreadLocal一般是用在可能被多个线程共同访问到的静态类对象上,那么我们先定一个交易上下文类,类中包含一个交易流水tranID字段,这个字段因为会被多个交易线程get和set,那么为了保证每个线程能够拿到自己set的tranId,而不会拿串,我们就需要使用到ThreadLocal,将tranId定义成ThreadLocal类型。
public class Context {
private static ThreadLocal tranId = new ThreadLocal();
public static void setTranId(String id){
tranId.set(id);
}
public static String getTranId(){
return tranId.get();
}
}
定义好了Context交易上下文类后,我们就可以写一个Main函数,在里面定义个线程池进行多线程调用,在线程中,其实就是先set Context的tranId字段,然后线程sleep1秒后再读取tranId字段,如果tranId没有定义为ThreadLocal那么这个值一定是不同线程会取串了,但是使用了ThreadLocal定义可能被多个线程共同访问的tranId后就不会出现串号的现象,ThreadLocal为每一个线程单独创建了这个静态变量的副本。
set thread id= 9 || tranId= -1193959466
set thread id= 10 || tranId= -1139614796
set thread id= 12 || tranId= -1220615319
set thread id= 11 || tranId= 837415749
get thread id= 9 || tranId= -1193959466
get thread id= 11 || tranId= 837415749
get thread id= 12 || tranId= -1220615319
get thread id= 10 || tranId= -1139614796
Process finished with exit code 0
public class Main {
public static void main(String[] args){
MyThread myThread = new MyThread();
ExecutorService es = Executors.newFixedThreadPool(4);
for(int i=0; i<4; i++){
es.submit(myThread);
}
es.shutdown();
}
public static class MyThread implements Runnable{
Random random = new Random(100);
@Override
public void run() {
String tranId = random.nextInt() + "";
Context.setTranId(tranId);
System.out.println(" set thread id= " + Thread.currentThread().getId() + " || tranId= " + Context.getTranId());
try {
Thread.sleep(1000);
System.out.println("get thread id= " + Thread.currentThread().getId() + " || tranId= " + Context.getTranId());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}