介绍

我们都知道,ThreadLocal能够保证每个线程都会拥有一份单独的数据,现在在对数据进行操作时,只会影响本线程的数据,不会对其它线程的数据有所影响。就好比,普通共享变量就好比公共电话,只有一个。而ThreadLocal就好比手机,人手一个。

那么它是怎么实现每个线程都有一份数据的呢,并且使用时需要注意哪些事项呢,我们一起来看看源码是怎么写的。

代码跟踪和分析

下面是一个ThreadLocal的简单案例

package threadlocal;

import java.security.SecureRandom;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author lmq
 * @version 1.0
 * @datetime 2025/4/5 16:35
 **/
public class ThreadLocalDemo {

    public static void main(String[] args) throws InterruptedException {
        User user = new User();
        AtomicInteger nums = new AtomicInteger();
        CountDownLatch countDownLatch = new CountDownLatch(20);
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                int total = 0;
                try {
                    total = new SecureRandom().nextInt(20);
                    for (int j = 0; j < total; j++) {
                        user.numLocalAdd();
                    }
                    nums.addAndGet(user.numLocal.get());
                    System.err.println(Thread.currentThread().getName() + ":" +user.numLocal.get());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                } finally {
                    countDownLatch.countDown();
                    user.numLocal.remove();
                }
            },"t" + (i + 1)).start();
        }
        countDownLatch.await();
        System.out.println(user.numLocal.get());
        System.out.println(nums.get());
    }

}

class User{
    public Integer num;

    // 这里建议使用静态变量。因为这里是使用线程区分的,每个线程一份变量,不是通过类实例区分的。避免了每次new一个类实例都开辟一部分内存
    ThreadLocal<Integer> numLocal = ThreadLocal.withInitial(() -> 0);

    public void numLocalAdd(){
        numLocal.set(numLocal.get() + 1);
    }

}

代码中,新建20个线程,然后每个线程都对自己的ThreadLocal数据进行随机次数的++。我们先跟着代码走,先看下Thread类的构造器,有没有初始化ThreadLocal数据的相关代码。

上面的代码,新建线程会先进入这个构造方法,第一个参数是传入的线程任务,第二个是线程名称。我们看下一步

这个方法的参数,第一个是线程组,第二个是线程任务,第三个是线程名称,第四个是栈大小。然后再看下一步

这个方法太长,我就把代码复制过来了。然后顺便增加了一些注释和解释。到这里,新建线程的方法就结束了,线程启动方法里面没什么内容,也和ThreadLocal无关,看来线程的ThreadLocal赋值和新建线程,启动线程无关。我们继续往下走

/**
 * Initializes a Thread.
 *
 * @param g the Thread group
 * @param target the object whose run() method gets called
 * @param name the name of the new Thread
 * @param stackSize the desired stack size for the new thread, or
 *        zero to indicate that this parameter is to be ignored.
 * @param acc the AccessControlContext to inherit, or
 *            AccessController.getContext() if null
 * @param inheritThreadLocals if {@code true}, inherit initial values for
 *            inheritable thread-locals from the constructing thread
 */
@SuppressWarnings("removal")
private Thread(ThreadGroup g, Runnable target, String name,
               long stackSize, AccessControlContext acc,
               boolean inheritThreadLocals) {
    // 线程名为空时便抛出一个空指针异常(如果是正常的操作新建线程,并且名称不传null的话,都会有线程名字的)
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
    
    // 将线程名字设置到属性中去(private volatile String name;)它是一个私有的volatile变量
    this.name = name;

    // 获取当前线程(此方法是一个native方法,通过c++实现   public static native Thread currentThread();)
    Thread parent = currentThread();
    // 获取JVM的安全管理器实例(这里我没有启用,所以返回的是null)
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
        /* Determine if it's an applet or not */

        /* If there is a security manager, ask the security manager
           what to do. */
        // 若安全管理器不为null,则将新建线程的线程组设置为安全管理器实例的线程组
        if (security != null) {
            g = security.getThreadGroup();
        }

        /* If the security manager doesn't have a strong opinion
           on the matter, use the parent thread group. */
        // 若线程组为空,则将新建线程的线程组设置为父级线程(父级线程是创建线程的那个线程,我这里的案例,父级线程是main线程)
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }

    /* checkAccess regardless of whether or not threadgroup is
       explicitly passed in. */
    // 检查授权(里面是通过安全管理器检查,因为我没有开启,所以这一步是空代码)
    g.checkAccess();

    /*
     * Do we have the required permissions?
     */
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(
                    SecurityConstants.SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    // 线程组增加未启动线程
    g.addUnstarted();

    // 将新建线程的线程组设置为g(g通过刚才的一些代码,已经有值了)
    this.group = g;
    // 将当前线程的是否守护线程标识设置为父级线程的标识
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    /* 
     * inheritableThreadLocals属性用于继承父级线程的inheritableThreadLocals值
     * InheritableThreadLocal类倒是确实可以将父线程的值传递给子线程
     */
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    /* Stash the specified stack size in case the VM cares */
    this.stackSize = stackSize;

    /* Set thread ID */
    this.tid = nextThreadID();
}

跟着这个断点走,我们来到了ThreadLocal的set和get方法。

set方法

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocal的ThreadLocalMap实例对象(其实就是获取Thread的threadLocals属性)
    ThreadLocalMap map = getMap(t);
    // 若线程是第一次操作ThreadLocal,map肯定是null(除非是线程池的线程,然后使用完又没有remove掉ThreadLocal的话就会有遗留)
    if (map != null) {
        map.set(this, value);
    } else {
        // 创建ThreadLocalMap,key是当前线程对象,value是外部传过来的值(因为key是Thread对象,所以每个线程都会有一份值,互不影响)
        createMap(t, value);
    }
}

get方法

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;
        }
    }
    // 上面的方法我们就不看了,和get类似,我们直接看这个方法
    return setInitialValue();
}


private T setInitialValue() {
    // 这里会直接返回null(所以我们在使用ThreadLocal时,最好要重写默认的initialValue方法,要不然就直接返回null了)
    T value = initialValue();
    // 下面的方法就和set差不多了
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}


protected T initialValue() {
    return null;
}

我们再来看看Thread和ThreadLocal的属性和内部类相关的

public class Thread implements Runnable {
    // 用于维护每个线程的ThreadLocal值
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // 用于维护父级的InheritableThreadLocal值
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

    其它代码...
}

public class ThreadLocal<T> {

    // 这是ThreadLocal的静态内部类,我们从Thread类中可以看到,内容主要也是放在这里面的
    static class ThreadLocalMap {
      
      /* 
       *  Entry,用过集合的应该都知道,他就是k,v键值对,这里的key用的就是ThreadLocal实例对象,而value是我们需要存放的值
       *  此处还使用了弱引用来对Entry进行包装
       *  tips:弱引用的对象,在下一次gc时就会被回收
       *  所以下一次gc之后,key就会变成null
       */
      static class Entry extends WeakReference<ThreadLocal<?>> {

        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
      }
      其它代码...
    }
    其它代码...
}

初始化ThreadLocal值避免空指针异常,使用ThreadLocal.withInitial进行初始化

我们定义的接口函数最终会在initialValue中被调用,而initialValue是重写父类ThreadLocal的方法,然后在get方法中会被调用

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    // new一个SuppliedThreadLocal类,并将自定义的接口函数方法传入构造器
    return new SuppliedThreadLocal<>(supplier);
}

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    @Override
    protected T initialValue() {
        return supplier.get();
    }
}


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;
        }
    }
    // 上面的方法我们就不看了,和get类似,我们直接看这个方法
    return setInitialValue();
}


private T setInitialValue() {
    // 这里会直接返回null(所以我们在使用ThreadLocal时,最好要重写默认的initialValue方法,要不然就直接返回null了)
    T value = initialValue();
    // 下面的方法就和set差不多了
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}


protected T initialValue() {
    return null;
}

总结

Thread和ThreadLocal的关键结构代码

public class Thread implements Runnable {

    ThreadLocal.ThreadLocalMap threadLocals = null;

    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
    // 其它代码...
}

public class ThreadLocal<T> {

    static class ThreadLocalMap {
      
      static class Entry extends WeakReference<ThreadLocal<?>> {

        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
      }
      其它代码...
    }
    其它代码...
}

1、存储在哪儿:每个线程的ThreadLocal值是绑定存储在Thread的threadLocals属性中

2、何时绑定:在当前线程第一次使用ThreadLocal变量时绑定

3、如何做到每个线程一份数据:第一,数据是绑定在Thread实例的属性中。第二,存储的数据结构是k,v键值对,并且key是当前ThreadLocal对象(保证唯一)

下面是ThreadLocal的引用关系示意图(实线为强引用,虚线为弱引用)

当线程(即Thread对象)正常结束任务时(非线程池情况下),线程中的属性(ThreadLocalMap对象)会被回收,此时所有与线程关联的ThreadLocal键值对(包括 Entry 中的 key 和 value)都会被释放。这种情况,即使不调用remove方法,也不会内存泄漏。

在线程池环境下,Thread对象有可能不会回收(核心线程不会回收),当系统GC时,ThreadLocal对象将被回收,此时Entry对象中的key会变为null,将无法访问。而ThreadLocalMap和T对象都有其它对象强引用,不会被回收,此时就会造成内存泄漏。解决办法就是调用remove方法。