java线程本地变量ThreadLocal详解
介绍
threadlocal作为jdk1.2以来的一个java.lang包下的一个类,在面试和工程中都非常重要,这个类的主要目的是提供线程本地的变量,所以也有很多地方把这个类叫做线程本地变量
从字面理解,这个类为每个线程都创建了一个本地变量,实际上是threadlocal为变量在每个线程中都创建了一个副本,使得每个线程都可以访问自己内部的副本变量
通常提到多线程,都会考虑变量同步的问题,但是threadlocal并不是为了解决多线程共享变量同步的问题,而是为了让每个线程的变量不互相影响,相当于线程之间操纵的都是变量的副本,自然就不用考虑多线程竞争的问题,也自然没有性能损耗
使用方式
先来看常用的这几个方法
public t get() { } public void set(t value) { } public void remove() { } protected t initialvalue() { }
显而易见,get()方法获取线程拥有的副本值,set()方法进行设值,remove()方法移除,initialvalue()进行变量初始化,我们先来看下面这个实例,同时体会一下应用场景
public class demo { public static threadlocal<integer> threadlocal = null; public static void main(string[] args) { threadlocal = new threadlocal<integer>() { /** * 通过重写该方法来初始化threadlocal的值 */ @override protected integer initialvalue() { return 10; } }; mythread t1 = new mythread(20); mythread t2 = new mythread(30); t1.start(); // 这里为了描述清晰,省略了try-catch语句块 t1.join(); t2.start(); } }
在上述方法中,我们定义并初始化一个threadlocal类为10(通过重写initialvalue()方法实现),然后开启了两个线程,同时我们这里让t2线程等待t1线程执行完再执行
mythread类详细信息如下
class mythread extends thread { private int val = 0; mythread(int val) { this.val = val; } @override public void run() { system.out.println(thread.currentthread() + "-before-" + demo.threadlocal.get()); demo.threadlocal.set(val); system.out.println(thread.currentthread() + "-after-" + demo.threadlocal.get()); } }
我们通过调用threadlocal对象的get()方法来获取当前的值,然后通过set()方法设置一个新值(每个线程我们设置不同的值),然后再通过get()方法来获取设置后的值
运行结果如下
重点是图中标注的t2线程变量的初始值,虽然我们在t1线程中修改了变量的值,但是在t2线程中变量值并没有被改变,这样就实现了每个线程独有的变量
同时,如果一个threadlocal对象要在很多地方进行复用时,需要在使用前通过调用**remove()**方法来将本地变量恢复到默认值
也许有人会问了,我们给每个线程定义自己的私有变量不是也可以实现同样的操作吗,理论上当然是可行的,但是threadlocal远比私有变量的形式方便,不仅可以在线程外部进行统一的初始化,而且避免在线程内部额外设置变量
原理
点进threadlocal的源码中,发现并没有存储变量的字段值,那看来threadlocal并不负责保存变量,我们只能从方法下手
先看initial()方法,毕竟我们的变量默认初始值就是在这个方法中设置,如下
protected t initialvalue() { return null; }
我们在每次创建threadlocal都要重写这个方法,那么这个方法到底在哪调用呢,我们点进get()方法源码中,如下
public t get() { // 获取当前线程 thread t = thread.currentthread(); // 获取当前线程的map threadlocalmap map = getmap(t); if (map != null) { threadlocalmap.entry e = map.getentry(this); if (e != null) { t result = (t)e.value; return result; } } // 如果map为空则进行创建 return setinitialvalue(); }
有点眉头了,我们发现这里获取了一个threadlocalmap对象,所以会想到有可能是通过让线程与变量作一个kv表,来实现每个线程拥有自己独有的变量
我们点进getmap(t)方法中,发现返回了线程t的一个threadlocals属性,这是thread类的一个字段:
threadlocal.threadlocalmap threadlocals = null;
这是一个由threadlocal类维护的属性,thread的任何方法都没有对这个字段进行修改操作,而这个threadlocalmap本身又是threadlocal的一个内部类,可以把它理解成一个map(虽然这个类没有继承map接口)
同时要注意,在threadlocalmap对象中的entry对象(键值对),继承了一个threadlocamap的弱引用,如下
static class entry extends weakreference<threadlocal<?>> { /** the value associated with this threadlocal. */ object value; entry(threadlocal<?> k, object v) { super(k); value = v; } }
也就是说,当threadlocal被置为空时,entry中的key则会在下一次ygc中被回收
我们还是没有看到initialvalue()方法,别急,点进setinitialvalue()方法,也就是如果在get()方法中检测到map为空时调用的方法,如下
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; }
现在基本操作我们都清楚了,set()方法和initialvalue()几乎完全一致,remove()方法则是普通地移除了一个kv键值对(k为当前线程),这里均不再列出,如果感兴趣可以自行查看
注意事项
1.脏数据
从上面的分析可以看出,threadlocal是和thread绑定的,每一个thread对应一个value,如果没有在使用结束后调用remove()方法,就会在下一次重用时读到脏数据(针对同一个线程而言),尤其是使用线程池的场景(线程池中的线程经常会复用)
2.内存泄露
一般在使用时都会将threadlocal设置为静态字段,这时候当线程执行完成后,kv中的v是不会自动回收的,所以要在使用完后及时调用remove()方法清理
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。