浅谈ThreaLocal类

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类用于提供线程局部变量,这些变量与普通的变量不同之处在于每个线程访问该变量的时候都有各自独立的变量副本,通常是用于希望将状态跟线程相关联的一些场景。

ThreadLocal怎么用?

如果单凭上述的解释还觉得比较抽象的话,那么我们就写一段代码看看具体是啥效果:

public class Client {
    private static ThreadLocal localStr = new ThreadLocal(){
        @Override
        protected String initialValue(){
            return "initial localStr value!";
        }
    };
//    也可以使用如下方法给ThreadLocal变量提供初始值
//    private static ThreadLocal localStr = ThreadLocal.withInitial(() -> "initial localStr value!");

    public static void main(String[] args) {
        Thread[] threads = new Thread[5];

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + " before set :" + localStr.get());
                    localStr.set(Thread.currentThread().getName());
                    System.out.println(Thread.currentThread().getName() + " after set :" + localStr.get());
                }
            }, "Thread-"+i);
        }

        for (Thread t : threads){
            t.start();
        }
    }
}

输出结果:

Thread-0 before set :initial localStr value!
Thread-0 after set :Thread-0
Thread-1 before set :initial localStr value!
Thread-1 after set :Thread-1
Thread-2 before set :initial localStr value!
Thread-2 after set :Thread-2
Thread-3 before set :initial localStr value!
Thread-3 after set :Thread-3
Thread-4 before set :initial localStr value!
Thread-4 after set :Thread-4

从代码和输出结果可以看出,localStr这个静态变量在每个thread当中都是互相独立的,也就是说对这个变量进行多线程操作是线程安全的,我们不需要对其进行锁操作。

ThreadLocal怎么实现的?

知道了ThreadLocal类怎么使用,那我们就会想它是怎么实现这种效果的,或者说是通过什么办法将变量副本跟Thread对象进行了关联,下面我们看下ThreadLocal类的几个主要方法和静态内部类(上传了下截图发现好大,不知道怎么调整大小,就此作罢):

  • 静态内部类 static class ThreadLocalMap{}
  • public T get()
  • public void set(T value)
  • public void remove()

对应方法实现如下:

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();
    }

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

我们可以看到三个方法里面都有一个getMap方法,传入的参数是当前线程对象,返回的是当前线程所关联的ThreadLocalMap对象,具体看getMap方法和Thread类的代码:

ThreadLocalMap getMap(Thread t) {
   return t.threadLocals;
}

简单到不能再简单的一段代码有木有,threadLocals这个玩意儿是Thread类的一个成员变量。所以getMap方法返回的就是Thread对象的一个成员变量,所以不同Thread都有各自的一份ThreadLocalMap。好,到目前为止,我们终于发现了,原来真正存储变量副本的地方是在ThreadLocal类里面的一个静态内部类ThreadLocalMap当中。
那我们再继续看ThreadLocalMap类是怎么把变量值存放起来的:

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

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

ThreadLocalMap当中又定义了一个静态内部类Entry来存放具体的数据,这是个key,value结构的,真正存储变量副本的地方是Entry里面的value。由于Entry继承了WeakReference>(思考一下这里为什么要用弱引用呢?),因此ThreadLocalMap当中的这个key是用的弱引用,这样设计的目的是当内存空间不足的时候,key会被GC回收掉。

那是不是说有了这个弱引用,ThreadLocalMap就不会发生内存泄漏了呢?不是的,下面让我们来理一理上面第二部分给出的那段代码的内存使用示意图(简单起见,这里假设代码里只创建了一个线程):


浅谈ThreaLocal类_第1张图片
这里借用参考文章1当中的图

这里假设我将ThreadLocalRef = null,也就是把ThreadLocalRef指向ThreadLocal的强引用(Strong Reference)打断,由于Key指向的的ThreadLocal是弱引用,因此当内存空间不足时,ThreadLocal就会被GC回收掉Key就指向了null,也就是说这个Entry里的value永远没人能够访问到,同时由于保持着Current Thread Ref -> Current Thread -> Map -> Entry的强引用链,因此value的数据不会被GC回收,这就造成了内存泄漏问题。

有人会说,当thread对象被GC回收时,Current Thread Ref -> Current Thread 的强引用链不就断了吗,这下Heap里面的那坨东西总会被回收了吧?
是的,没错。不过一般框架底层都会使用线程池来创建和管理线程,这时候线程用完是回到线程池当中进行复用的,并不会直接销毁,这时候就会内存泄漏。

好在,ThreadLocalMap的设计者已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。这些被动预防措施只是降低了内存泄漏的风险,但并不能够保证,因此需要我们在使用当中特别注意,在使用完ThreadLocal对象后,一定要调用remove()!。

讲到这里差不多讲完了,那么最后可以再看看下面的代码,想想输出结果是什么?

public class Client {
    private static final ClassA a = new ClassA();
    private static ThreadLocal local = new ThreadLocal();

    public static void main(String[] args) {
        Thread[] threads = new Thread[5];

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    local.set(a);
                    local.get().setNum(local.get().getNum() + 5);
                    System.out.println(Thread.currentThread().getName() + " after set :" + local.get().getNum());
                }
            }, "Thread-"+i);
        }

        for (Thread t : threads){
            t.start();
        }
    }
}

class ClassA{
    private int num;

    int getNum() {
        return num;
    }

    void setNum(int num) {
        this.num = num;
    }
}

参考文章:
http://www.importnew.com/22039.html
https://www.toutiao.com/a6586218943701058061

你可能感兴趣的:(浅谈ThreaLocal类)