用法
ThreadLocal是用空间换时间来解决线程安全问题,方法是各个线程拥有自己的变量副本。
既然是涉及线程安全,必然有一个共享变量,声明一个:
public class Singleton { private Connection connection = DataSourceUtil.getConnection(); }
多线程下,上面资源非线程安全就会报错,所以我们不如让每一个线程单独拥有一个:
public class Singleton { private ThreadLocal<Connection> connection = new ThreadLocal< Connection >(){ @Override protected Connection initialValue() { return DataSourceUtil.getConnection(); } }; }
以上就是TheadLocal的正确用法了,大家要知道,实现initialValue的时候,必须要返回一个新的对象;不然就没必要用ThreadLocal了。如何使用这个connection呢?
public class Singleton { private ThreadLocal<Connection> connectionSource = new ThreadLocal< Connection >(){ @Override protected Connection initialValue() { return DataSourceUtil.getConnection(); } }; public void use(){ connectionSource.get().doSomething(); } }
请注意使用代码的命名,它将会让你知道实质。
实现
如果让我们手把手来做,该如何实现呢?
简单,用一个Map来维护这个线程和变量的映射关系,thread1->con1 ; thread2->con2 ; thread3->con3 ; 但这样并不好
因为这个map也需要保证线程安全了,所以,这里又有点绕回来的感觉,当然你可以用concurrentHashMap,是我用了这个map我又何必用threadlocal呢,而且还存在垃圾难回收的问题?还好,还有更好的方案。
更好的方案:让线程自己维护自己的所有ThreadLocal实例,然后在线程需要用的时候在Thread实例里面取!如何把ThreadLocal示例放到Thread里面?毕竟Thead不会为你设置一个ThreadLocal属性,而且一个属性也不够,因为一个Thread可能涉及多个ThreadLocal实例,很自然,在Thread里面放一个Map就好了,key就是没一个ThreadLocal实例;
下面为Thread类的源代码,看到这个属性 threadLocals是一个ThreadLocalMap类,但该类是ThreadLocal的内部静态类。
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
先记住几个点:
1、ThreadLocal是一个类,所以使用ThreadLocal是创建一个ThreadLocal的类实例:threadLocal
2、每个Thread有个叫ThreadLocalMap的成员变量,ThreadLocalMap是ThreadLocal的静态内部类;
3、ThreadLocalMap里面是多个Entry,键是ThreadLocal实例,值是被保护的资源,因为每一个线程独特有的资源都是需要创建ThreadLocal实例的
4、每一个ThreadLocal都是弱引用实现,GC会被回收,所以不用担心内存泄露问题。
图示:
弱引用,使用弱引用是因为线程的执行实现可能很长,但其实资源是可以回收的,所以避免这类内存泄漏的问题。
/** * 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<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
ThreadLocalMap其实和HashMap的实现是差不多的,拓展还是寻址、求模等,所以没啥特别。重点是ThreadLocalMap其实是给ThreadLocal使用的,因为最终还是学习ThreadLocal的精粹最重要,贴下ThreadLocal是如何使用ThreadLocalMap的
Set方法,注意这个this是ThreadLocal实例:
/** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } /** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param t the current thread * @return the map */ ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
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(); }
关键点:
ThrealLocal的核心是在一个共享代码(特别指单实例中的成员变量)中实现线程安全,方法是使得每一个线程具有其独特的副本,当线程跑到该资源代码的时候,获取自己的副本,怎么获取呢?
- 首先是获取线程实例,也就是自己,然后通过线程实例获取资源,即使Thread类的使用并非是共享方法(上面的例子很好说明了是线程特有的),但实际语义是一样的,其语义核心代码是这句:Thread t = Thread.currentThread(); 没有这句代码,就没有共享线程安全。
- 不同点在于是把变量作为线程成员变量存放还是在堆中的共享变量存放,而后者还有更多问题无法解决,这里就不提了。
避坑:
说到线程安全,我相信大家接触的比较多的就是各种锁了,那么如何才能做到不使用锁而线程也安全呢?没办法,如果非要竞争资源那是真没办法,所以最好就是不需要竞争资源了,而ThreadLocal就是这种想法下诞生的,是的,ThreadLocal就是让你不需要竞争资源,每个线程都分配一个只有自己可访问的局部资源。
我们知道ThreadLocal有四个方法,initialValue(),get(),set(T),remove() ,前三个好理解,最后一个方法大家注意了,也是最不可忽视的。特别当你用到线程池的时候,因为线程执行一个任务,而任务完成后,如果不做清除操作,remove(),那么下次其他事务再用到该线程访问同样资源的时候,你就很可能掉坑里去。
多Remove:
即使你考虑到了remove,也记得捕捉下异常,避免出现异常抛出导致不执行remove的问题。举个例子:如果你用了动态数据源切换,当你的线程在用某个数据源查询数据异常而无法执行remove的时候,下次某个用默认数据源的再用该线程,线程就会被绑定旧的数据源,从而导致错误。
内存溢出:
如果不用Remove()还会导致内存溢出
JVM:
其实ThrealLocal在JVM中也有用到,它就是TLAB,下面Copy一段介绍:
JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
也就是说,Java中每个线程都会有自己的缓冲区称作TLAB(Thread-local allocation buffer),每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步(另一种同步方式就是强大的CAS自旋了),也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。
当然,以上的特性决定了分配内存首先是不被共享的,因此和在栈内分配内存一样,JVM要先做逃逸分析,如果是线程特有,那么就在TLAB中分配。
所以ThreadLocal其实就是一个比较简单的方法而已,没有CAS这么特别,你在工程中也能自己用上。