一、定义
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。
二、基本使用
示例
public static void main(String[] args) { ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); threadLocal.set(999); // 模拟3个线程 for (int i=0; i<3; i++) { new Thread(()-> { for (int j = 0; j<2000; j++) { if (null==threadLocal.get()) { threadLocal.set(1); } else { threadLocal.set(threadLocal.get()+1); } } System.out.println(Thread.currentThread().getName() + " " + threadLocal.get()); }).start(); } System.out.println(Thread.currentThread().getName() + " " + threadLocal.get()); }
输出结果:
main 999 Thread-0 2000 Thread-1 2000 Thread-2 2000
上述示例说明:线程与线程之间相互隔离,且线程安全。ThreadLocal作用域为当前线程
三、源码分析
ThreadLocal为什么可以做到每一个线程都有一份变量的副本,其实原理很简单
ThreadLocal原理同HashMap原理是一样的,核心为ThreadLocalMap,只不过是Map的key为当前线程,通过Thread.currentThread()获取。只要是在同一个线程内,只要你执行set方法,这个key永远保持不变,即每一个线程对应一个value。
我们先通过ThreadLocal的set方法来分析
// set方法一般都是set(key, value),因为ThreadLocalMap使用了当前线程作为key,所以省略了,get()方法也一样。 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; // 这个变量在Thread类中,ThreadLocal.ThreadLocalMap threadLocals = null; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
首次在调用 set方法时,会执行createMap,在createMap方法中又会创建一个ThreadLocalMap对象,我们再来看一下ThreadLocalMap这个构造方法
构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); }
通过构造方法,我们可以看出ThreadLocal底层使用的就是HashMap结构
ThreadLocalMap继承了WeakReference
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } private static final int INITIAL_CAPACITY = 16; private Entry[] table; }
ThreadLocal为什么要使用弱引用?
首先我们看一个WeakHashMap的示例,WeakHashMap也是它的 Entry类继承了WeakReference
public static void main(String[] args) throws InterruptedException { WeakHashMap<String, String> weakMap = new WeakHashMap<String, String>(); String key = new String("1"); String key2 = "2"; weakMap.put(key, "test"); weakMap.put(key2, "test2"); System.out.println(weakMap); // {1=test, 2=test2} key = null; key2 = null; // 不会被回收 System.gc(); System.out.println(weakMap); // {2=test2} }
我们可以看出WeakHashMap针对key作了回收,而在整个map中并没有真正的回收此对象。在ThreadLocal中,它使用当前线程作为key的,如果线程生命周期结束后,即这个key以及对就的value都应该被GC掉
内存泄露问题
虽然上述的弱引用解决了key,也就是线程的ThreadLocal能及时被回收,但是value却依然存在内存泄漏的问题。当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。map里面的value却没有被回收.而这块value永远不会被访问到了。所以存在着内存泄露,因为存在一条从current thread连接过来的强引用。只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.
ThreadLocal和Synchonized的比较:
- ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
- ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比Synchronized要简单得多。
- ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与Synchronized有本质的区 别。Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本, 使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
- Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。