ThreadLocal 没你想的那么难

ThreadLocal 是什么?

直接上 JDK 里的注释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).

简单的翻译一下,ThreadLocal 这个类提供了线程本地变量,这个变量很特别,因为访问这个变量(通过其 get()set() 方法)的每个线程都有自己的一个对应的局部变量。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

能看懂吗?如果你是第一次了解 ThreadLocal 的话,我相信你绝对看不懂,但别着急,咱们接着往下走。

首先,说在最前面,ThreadLocal 绝对不是为了线程同步问题服务的,也不是为了解决多线程共享变量的问题,它只是提供了一种很特殊的机制,使得每个线程都拥有了一个独立的局部变量,而且某个线程更改这个变量完全不会干涉到其它线程的局部变量。

干说没用,我们直接上一段代码来体会体会。

public class ThreadLocalDemo {
	
	// 声明两个ThreadLocal变量
	private static ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<>();
	private static ThreadLocal<String> threadLocalString = new ThreadLocal<>();
	
	public static void main(String[] args) {
		threadLocalInteger.set(1);// 赋值
		threadLocalString.set("abc");// 赋值
		System.out.println(threadLocalInteger.get());// 获取
		System.out.println(threadLocalString.get());// 获取
	}

}

上面这段就是一个非常简单的 ThreadLocal 的用法,但是这里并没有使用多个线程来演示效果,我们这里只有一个主线程在运行对吧?那我们现在来加一个线程来看看这主线程与另一个线程之间同时操作 ThreadLocal 对象是否会相互干扰。

我们在原来的代码中加入一段对新线程的定义,如下:

public class ThreadLocalDemo {
	
	private static ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<>();
	private static ThreadLocal<String> threadLocalString = new ThreadLocal<>();
	
	static class ThreadTest extends Thread{
		@Override
		public void run() {
			System.out.println(threadLocalInteger.get());
			System.out.println(threadLocalString.get());
		}
	}
	
	public static void main(String[] args){
		threadLocalInteger.set(1);
		threadLocalString.set("abc");
		ThreadTest t1 = new ThreadTest();
		t1.start();
	}

}

这里我们先让主线程去操作两个 ThreadLocal 对象,设置完值之后,去让新线程 t1 去打印结果。看一下控制台。
在这里插入图片描述
没结果,我们再试一试先在主线程,然后在线程 t1 里去设置 ThreadLocal 的值,最后让主线程去打印结果,查看线程 t1 是否会覆盖主线程的设置的结果。

public class ThreadLocalDemo {
	
	private static ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<>();
	private static ThreadLocal<String> threadLocalString = new ThreadLocal<>();
	
	static class ThreadTest extends Thread{
		@Override
		public void run() {
			threadLocalInteger.set(2);
			threadLocalString.set("def");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	
	public static void main(String[] args) throws InterruptedException {
		// 先在主线程里设置ThreadLocal值
		threadLocalInteger.set(1);
		threadLocalString.set("abc");
		
		// 之后在t1线程里再设置
		ThreadTest t1 = new ThreadTest();
		t1.start();
		
		// 查看是否被t1线程覆盖了
		System.out.println(threadLocalInteger.get());
		System.out.println(threadLocalString.get());
	}

}

控制台打印结果
在这里插入图片描述
至此,我们应该知道了,尽管不同线程操作的是同一个 ThreadLocal 对象,但是每个线程的操作之间相互是独立的,不会出现变量共享的情况。

 

ThreadLocal 实现原理

ThreadLocal 的实现主要是基于 ThreadLocalMap,它是 ThreadLocal 的一个内部类,那说到 Map 我们应该立即就能想到这是一种键值对的存储结构,我们去看一下这个键值对是如何定义的:

       static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

首先注意下,Entry 继承自 WeakReference,那么熟悉 Java 引用类型的朋友应该知道,这是弱引用,被弱引用关联的对象只会存活在下一次 GC 发生之前,即寿命只有一岁,这里为什么要说这个呢,因为 ThreadLocal 容易发生内存泄露的问题,所以这里简单的提一嘴,在文章的最后会更详细的介绍这个问题,这里只是让你留个印象。

OK,回归正题,ThreadLocalMap 其内部使用 Entry 来实现 key-value 的存储,而且,重点来啦,这个 key 是什么?没错!就是 ThreadLocal,而 value 就是线程设置的值。

那么,我们再回头看看我之前提供的 demo

private static ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<>();
private static ThreadLocal<String> threadLocalString = new ThreadLocal<>();

这里threadLocalIntegerthreadLocalString这两个变量就是两个 key!

主线程执行 :

threadLocalInteger.set(1);
threadLocalString.set("abc");

实际上就是以threadLocalInteger为 key,以整数1为 value,存入ThreadLocalMap里。
threadLocalString为 key,以字符串"abc"为 value,存入ThreadLocalMap里。

而线程 t1 执行:

threadLocalInteger.set(2);
threadLocalString.set("def");

实际上就是以threadLocalInteger为 key,以整数2为 value,存入ThreadLocalMap里。
threadLocalString为 key,以字符串"def"为 value,存入ThreadLocalMap里。

那既然 key 是一样的,为啥两个线程没发生覆盖呢?想一想,是不是很容易想到一种情况?那就是存入的ThreadLocalMap根本就不是同一个ThreadLocalMap

好,为了解释上面的那句话,我们直接来看 ThreadLocal 的set()的源码

    public void set(T value) {
        Thread t = Thread.currentThread(); // 获取当前线程
        ThreadLocalMap map = getMap(t);// 根据当前线程获取到它的ThreadLocalMap
        if (map != null)
            // this就是当前的ThreadLocal对象,value就是设置的值,而map是当前线程自己的map!
            map.set(this, value);
        else
            // 如果当前线程还没有创建ThreadLocalMap,那就给它创建一个,并设置值
            createMap(t, value);
    }

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals; // 返回t线程的ThreadLocalMap
    }

根据上面的源码我们可以看到,执行set()方法时会根据当前的线程获取到它的 ThreadLocalMap,不要感到讶异,我们先来看看 Thread 类的源码。

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

看到了吧,其实每一个 Thread 都持有一个 ThreadLocalMap 的引用,所以每一个线程都有一个自己的 ThreadLocalMap,这就印证了上面的说法,尽管多个线程使用都是同一个 ThreadLocal 对象来作为 key,但是插入的 Map,压根就不是同一个 ThreadLocalMap,那当然不会发生覆盖的情况啊!

本着精益求精的原则,我们继续深挖 ThreadLocal,来看一下 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;
                }

                // 该Entry的key为空, 则代表该Entry需要被清空
                if (k == null) {
                    // replaceStaleEntry()方法会继续寻找传入key的安放位置, 并清理掉key为空的Entry
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            // 寻找到可以放置的位置, 则直接new一个entry出来放置在该位置上
            tab[i] = new Entry(key, value);
            int sz = ++size;
            // cleanSomeSlots清理无用的entry(key == null)
            // 如果没有清理陈旧的entry并且数组中的元素大于了阈值,则rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

ThreadLocalMap 的set()方法很有意思,首先,它定位数组索引位置的方式,同 HashMap 一样,不是简单的使用哈希值对数组长度进行取模,而是通过将数组长度限制为 2 的 n 次方,从而使用hashcode & (size - 1)这种位运算的手段来代替取模操作,进而提高计算效率。

但是解决哈希冲突的方式跟 HashMap 的链地址法不同,这里 ThreadLocalMap 解决哈希冲突的方式使用的是开放地址法,又称为线性探测法,它的核心思想是如果发生了哈希碰撞,那就往后找,一直找到没有元素的位置,把待插入的元素放在这个位置。

在 ThreadLocalMap 的set()方法有两个方法也需要特别注意一下,那就是replaceStaleEntry()cleanSomeSlots(),这两个方法都是为了清理 Entry 中 key 为 null 的键值对,从而防止发生内存泄露的情况。但是,依然会有内存泄露的情况会出现,我们最后再谈这个问题。

不论是 ThreadLocal 的set()方法还是 ThreadLocalMap 的set()方法,我们现在都已经大致梳理完毕了。对于线程执行set()方法插入元素的流程相信你也已经明确了,接下来我们来看一下 ThreadLocal 的get()方法,啃下了刚才的set()方法,现在来看get()方法就会非常的简单了。

    public T get() {
        Thread t = Thread.currentThread();// 获取当前线程
        ThreadLocalMap map = getMap(t);// 根据当前线程获取到它的ThreadLocalMap
        if (map != null) {
            // 从当前线程的ThreadLocalMap获取相对应的Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取value
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

ThreadLocal 的get()方法很简单,这里不过多赘述了,自己看注释吧。

最后要聊的一个方法是 ThreadLocal 的remove()方法,它会清除掉两种 Entry:

  • 一种是以当前 ThreadLocal 为 key 的 Entry
  • 一种是 key 为 null 的 Entry

当然,清除的地点是当前线程的 ThreadLocalMap 里。。。

     public void remove() {
         // 获取当前线程的ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             // 以当前ThreadLocal为参数,调用remove方法
             m.remove(this);
     }
    
     //将当前线程的ThreadLocalMap中以当前ThreadLocal为key的Entry清除掉
     //同时也会清除掉key为null的Entry
     private void remove(ThreadLocal<?> key) {
            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)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
     }

我们可以看到顶层的这个remove()方法它是 public 的,说明我们可以手动调用进行清理来减少内存的占用,但是一般也不需要,因为 ThreadLocalMap 是线程私有的,线程挂掉了,ThreadLocalMap 也会跟着被回收。

注意我上面说的是一般情况下不需要进行主动的remove(),但是依然会有一些特殊的情况,这也是本文最后要谈的内容,就是 ThreadLocal 的内存泄露问题。
 

ThreadLocal 的内存泄漏问题

我们回顾一下之前聊过的内容,ThreadLocalMap 的键值对,也就是 Entry 的源码

       static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

之前有提到过,WeakReference>,可以看到 ThreadLocalMap 使用ThreadLocal 的弱引用作为 Entry 的 key,如果一个 ThreadLocal 没有外部强引用来引用它,下一次 GC 时,这个 ThreadLocal 必然会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,此时就发生了内存泄露。

而且这些 value 还很顽强,它是存在一条强引用链的,如下图所示(图片来自http://www.jianshu.com/p/ee8c9dccc953. )。
ThreadLocal 没你想的那么难_第1张图片
所以它不会被 GC 回收,而回顾我们上面介绍过的get()/set()/remove()等方法中,都会对 key 为 null 的 Entry 进行清除。这样看似解决了问题,但是真的解决了所有问题吗?

考虑这样一种情况,如果线程还活着(那么线程私有的部分不会被回收),并且一直不执行get()/set()/remove()等方法,那么这些 key 为 null 的 Entry 的 value 将永远无法回收。尤其是在使用线程池的情况下,因为线程池中的线程会常驻(比如FixedThreadPool,核心线程会一直存活,不会销毁),那么这种使用 ThreadLocal 而导致的内存泄露的问题就很容易发生。

所以,ThreadLocal 从 JDK1.5 之后为我们提供了 remove()方法来手动清除避免内存泄露的情况发生。

你可能感兴趣的:(Java并发)