ThreadLocal小解

196 阅读4分钟

ThreadLocal是java.lang包下面的一个类,与线程息息相关. 作为一个刚入门的小码农,平时代码开发基本没怎么用到过,偶然间接手了一个大牛的项目(你懂的)见到了这个类,发现了它的强大之处,相见恨晚啊!

ThreadLocal可以设置某个全局变量的当前线程局部值!

说起来很绕口,就是指:每个线程都可以看到这个变量,但是每个线程看到的这个变量的值是不一样的。这个变量对于所有线程来说是全局的,但这个变量的值对每一线程来说都是局部的。每个线程都可以对这个变量操作,但各个线程对这个变量的操作结果互不影响。(简单来说,就是每个线程对应这个变量都有不同的值)

一般的项目开发中,我们设置的接口、类、变量、方法(public 、private、protected、默认修饰符)都是对所有线程公开的,各个线程的操作结果会相互影响,(一个最简单的问题,多进程高并发情况下可能会导致属性的覆盖)而ThreadLoca恰恰就解决了这个问题。

下面先说一下用处:

ThreadLocal可以隐性传参

比如:用户登录,在登录时我就获取用户的信息,放进线程中,这样在调用其他方法时就不必携带用户信息作为方法参数了,可以直接从线程里面取。

ThreadLocal可以保证变量值在任何时候不被其他线程覆盖

比如:数据库的连接池,使用ThreadLocal管理Connection,保证事务的一致性。

下面简单讲解一下:

public class ThreadLocal<T> {

     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 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 remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
     
    其他内容暂时省略
    ....
}

T 即为你准备放进去的数据类型(最常见的就是放一些自定义类,比如放用户的登录信息UserInfo)

使用的时候可以

ThreadLocal<UserInfo> user=new ThreadLocal<>;
创建完ThreadLocal对象后就可以直接调用set方法放入用户信息;
使用时,直接调用get方法;
业务结束后可以再调用remove方法清除用户信息。

如果是使用spring框架,可以借助AOP来完成用户信息的放入与清除

注:如果使用了线程池,则必须要在线程复用之前清除上次放入的信息,否则会造成内存的泄露,数据混乱。

写一个小用法示例:

public class ThreadLocalUtil{
    private static ThreadLocal<UserInfo> user=new ThreadLocal<>();
    
    public static void set(UserInfo userInfo){
        user.set(userInfo);
    }
    
    public static void remove(){
        user.remove();
    }
    
    public static void get(){
        return user.get();
    }
    
}

在Spring Aop 动态放入用户信息后, 在调用时 UserInfo user=ThreadLocalUtil.get(); 即可获得存储在当前线程的用户信息

下面讲一下ThreadLocal的原理:

ThreadLocal类:
 public ThreadLocal() {}
    
 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是ThreadLocal的内部类
 static class ThreadLocalMap {
     其他内容暂时省略
     ...
 }

ThreadLocal类只有一个无参构造方法,且并没用任何操作。
创建完对象后就可以调用set方法放进对象了。
set 方法里面首先得到了当前线程,
有从当前线程中调用getMap方法得到ThreadLocalMap对象

-----------------------------------------------------------------------
ThreadLocalMap类:
   ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
这里又从线程里得到了当前线程的threadLocals;

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

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }


-----------------------------------------------------------------------

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

-----------------------------------------------------------------------
由此我们可以看出ThreadLocalMap是ThreadLocal的内部类,使用Entry进行存储
Thread类维护一个ThreadLocalMap对象(默认为null);
    

我们回到ThreadLocal类的set方法,代码拷贝过来

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

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

如果map为空就创建一个ThreadLocalMap(Thread类的ThreadLocalMap对象默认为null),并把值放进去; 不为空就map.set(this,value)放进去 方法其实是内部类ThreadLocalMap的set方法(使用Entry存储,Entry是ThreadLocalMap的内部类),代码拷贝过来

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

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

这便是ThreadLocal set方法的完整流程了,get方法同理就不做介绍了,建议参照源码深入理解下,加深理解。

这里放一张ThreadLoacal对象关系引用图

注:如果使用了线程池,则必须要在线程复用之前清除上次放入的信息,否则会造成内存的泄露,数据混乱。