定义
线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。ThreadLocal可以让每个线程拥有一个属于自己的变量的副本,不会和其他线程的变量副本冲突,实现了线程的数据隔离。
使用示例
public class ThreadLocalTest {
private static ThreadLocal threadLocal = new ThreadLocal<>();
static class MyThread extends Thread{
@Override
public void run() {
super.run();
threadLocal.set("son");
System.out.println(threadLocal.get()); //son
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
threadLocal.set("main");
System.out.println(threadLocal.get()); //main
}
}
通过上面的代码我们发现,当我我们使用同一个ThreadLocal对象在不同线程中set不同的值,会打印出不同的值,这是怎么做到的呢?上面定义中提到每个线程拥有一个属于自己的变量的副本,这就能解通为什么我们可以获取到不同的值。
既然每个线程有一个自己的变量副本,那我们自己是不是也可以实现呢?答案是肯定的,下面我们自己实现一个类似的功能。
//自定义实现的ThreadLocal
public class MyThreadLocal {
private Map hashMap = new HashMap();
//多线程下保证原子性
public synchronized void set(T t) {
synchronized (MyThreadLocal.this) {
hashMap.put(Thread.currentThread(), t);
}
}
public synchronized T get() {
return hashMap.get(Thread.currentThread());
}
}
public class ThreadLocalTest {
//使用我们自己定义的ThreadLocal
private static MyThreadLocal threadLocal = new MyThreadLocal<>();
static class MyThread extends Thread{
@Override
public void run() {
super.run();
threadLocal.set("son");
System.out.println(threadLocal.get()); //son
}
}
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();
threadLocal.set("main");
System.out.println(threadLocal.get()); //main
}
}
我们自己实现了一个MyThreadLocal对象,创建了一个Key是Thread的Map对象存储对应线程的数据,也实现了ThreadLocal的功能,下面我们看看系统的ThreadLocal是怎么做的。
源码分析
我们从set方法开始分析。
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
//获取ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) //不为空直接调用ThreadLocalMap的set方法
map.set(this, value);
else
createMap(t, value); //为空创建ThreadLocalMap 并将数据存储起来
}
然后跟进getMap方法
ThreadLocalMap getMap(Thread t) {
//这里调用到了Thread中,也就是说ThreadLocalMap对象是从Thread中获取的
return t.threadLocals;
}
继续看看Thread的threadLocals对象
//Thread中定义的threadLocals属性
ThreadLocal.ThreadLocalMap threadLocals = null;
继续看看ThreadLocalMap的set方法
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();
//如果存在key则更新值
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//没有对应的key则添加到数组中
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
上面我们看到,存值的时候将ThreadLocal和Object(我们存储的数据)封装到了一个Entry对象中,下面我们看下这个Entry
//ThreadLocalMap中的内部类 存储了ThreadLocal和我们保存的值
static class Entry extends WeakReference> {
Object value;
//ThreadLocal作为key
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
这里我们可以总结下大致的流程
1、ThreadLocal调用set方法,然后获取当前调用的Thread
2、根据Thread获取threadLocals属性(ThreadLocalMap)
3、调用ThreadLocalMap的set方法
4、将ThreadLocal和我们保存的值封装成Entry对象,添加到数组(table)中
我们知道了存储的大致流程,获取其实也是一样的,都是对应的对象调用流程,我们就不介绍了。
我们上面也实现了自己的MyThreadLocal对象,也完成了线程副本数据的存储,我们思考下,这两种方式有什么区别呢?系统为什么要做这么多操作步骤去实现线程副本数据呢?
我们自己的实现方式是创建了一个Map去存储,当多线程情况下,为了保证原子性,我们加了锁。
如果当前有很多线程要获取数据,那么这些线程都会去争夺这个map,没有拿到的就会阻塞,这样性能上肯定是会有影响的。
系统的实现方式则是,没个线程都有自己的map,不存在并发的问题,自己用自己的,效率无疑是要高的,所以系统的实现还是有一定道理的。
这就跟打篮球是一样的,大家都抢一个球,和每个人都有一个球,效果肯定是不一样的。