ThreadLocal

介绍

该类提供线程局部变量。 这些变量与其正常对应变量的不同之处在于,访问一个变量(通过其 get 或 set 方法)的每个线程都有其自己的、独立初始化的变量副本。 ThreadLocal 实例通常是类中希望将状态与线程关联起来的私有静态字段(例如,用户 ID 或事务 ID)。

只要线程处于活动状态并且 ThreadLocal 实例可访问,每个线程就持有对其线程局部变量副本的隐式引用; 线程消失后,其线程本地实例的所有副本都将受到垃圾回收(除非存在对这些副本的其他引用)。

是一种将变量和线程绑定的方案。

使用

用户登录登出动作,用户信息保存。

  1. 静态变量保存

/**
 * 本地变量测试
 *
 * @author dcx
 * @since 2023-06-26 14:57
 **/
public class NoThreadLocalDemo {
    //类属性保存当前用户名
    private static String curName;

    public void login(String name) {
        System.out.println(name + " login");
        curName = name;
    }

    public void getCur() {
        System.out.println(curName+" get");
    }

    public void logout() {
        System.out.println(curName+" logout");
        curName = null;
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        NoThreadLocalDemo threadLocalDemo = new NoThreadLocalDemo();
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        executorService.execute(() -> {
            threadLocalDemo.login(1 + "");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadLocalDemo.getCur();
            threadLocalDemo.logout();
        });

        executorService.execute(() -> {
            threadLocalDemo.login(2 + "");
            threadLocalDemo.getCur();
            threadLocalDemo.logout();
        });
        System.out.println("------");
    }

}

本例中,用户1登录,延迟执行。用户2登录不延迟执行。

执行结果:

------
1 login
2 login
2 get
2 logout
null get
null logout

可以看出,线程2将用户信息释放,用户1延迟之后的执行出现错误。

  1. threadLocal方式

public class ThreadLocalDemo {
    private static final ThreadLocal<String> curName = new ThreadLocal<>();

    public void login(String name) {
        System.out.println(name + " login");
        curName.set(name);
    }

    public void getCur() {
        String s = curName.get();
        System.out.println(s + " get");
    }

    public void logout() {
        System.out.println(curName.get() + " logout");
        curName.remove();
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        executorService.execute(() -> {

            threadLocalDemo.login(1 + "");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadLocalDemo.getCur();
            threadLocalDemo.logout();
        });
        executorService.execute(() -> {
            threadLocalDemo.login(2 + "");
            threadLocalDemo.getCur();
            threadLocalDemo.logout();
        });
        System.out.println("---------");
    }

}

用户1登录,延迟执行。用户2登录不延迟执行。

2 login
2 get
2 logout
---------
1 login
1 get
1 logout

可以看出,用户1和用户2正常执行。

核心原理

set

public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取当前线程中的threadlocalMap,每个线程都会有一个threadlocalMap属性。
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //key是threadLocal,value是具体的值。
        map.set(this, value);
    } else {
        //如果是InheritableThreadLocal则会赋值给Thread的inheritableThreadLocals
        createMap(t, value);
    }
}

get

public T get() {
    //获取当前线程
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //ThreadLocalMap保存Entry集合。根据thradLocal,从ThreadLocalMap中获取
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

thread,threadLocal,threadLocalMap,Entry关系:

  1. thread包含threadLocalMap属性。

  2. threadLocal封装了对threadLocalMap的操作。

  3. threadLocalMap维护着Entry列表。

  4. Entry是一种弱引用,key是threadLocal,value是具体的值。

Drawing
关系图

ThreadlocalMap

是一种专门为保存线程本地变量而设计的一种hash map。

set

private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    //计算所在数组下标
    int i = key.threadLocalHashCode & (len-1);
    //1.优先查看现有的数组下标是否有值。
    //2.如果当前数组下标有值,则会循环向后1个位置获取,直到所在数组位置没有值。
    //3.此循环是解决hash冲突或者多个hash取余后,槽位已经有值的情况下,则会遍历向后存放元素。
    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;
        }
    }
    //经过遍历后,没有找到槽位,进行扩容,幅度是加1。
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //如果数组长度超过负载,则会进行rehash。
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

resize

rehash里面主要是会进行resize。重点看下这个:

private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    //新数组扩大2倍
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;

    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                //重新进行hash计算,放进数组。
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

get

private Entry getEntry(ThreadLocal<?> key) {
    //通过hash值和数组长度,确定槽位。
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        //如果当前槽位没有,说明有hash冲突,因此进行循环查找。
        return getEntryAfterMiss(key, i, e);
}

不足

无法将变量传递到子线程。

public void getCur() {
    String s = curName.get();
    System.out.println(s + " get");
    new Thread(
            ()->{
                System.out.println("子线程获取name:"+curName.get());
            }
    ).start();
}

输出:

1 login
---------
2 login
2 get
2 logout
子线程获取name:null
1 get
1 logout
子线程获取name:null

InheritableThreadLocal 对此做出了优化。

Last updated

Was this helpful?