ThreadLocal 源码分析

zcpHappy 2020-06-16

一、定义
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更加复杂。
 

相关推荐

源码交流区 / 0评论 2019-11-01