ThreadLocal“你”真的了解吗?(一)

本文围绕Java中ThreadLocal展开,介绍其为线程提供局部存储,通过为每个线程维护独立副本实现数据隔离,避免多线程同步问题,但需注意内存泄漏。还阐述了Thread基础知识、创建线程的四种方式,以及Thread与ThreadLocal的关系,包括ThreadLocalMap在Thread中的使用和初始化逻辑。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

今天想梳理一个常见的面试题。在开始之前,让我们一起来回顾一下昨天的那篇文章——《Spring 事务原理总结七》。这篇文章比较啰嗦,层次也不太清晰,所以以后有机会我一定要重新整理一番。这篇文章主要想表达这样一个观点:Spring的嵌套事务是通过ThreadLocal实现的。当一个请求从浏览器发送到后台以后,后台会启动一个线程去处理这个请求,在处理的过程中如果用到了一个需要事务的方法A,而A又调用了另一个需要事务的方法B,那么这时就发生了事务嵌套(同时还涉及到了事务传播行为,即事务A与事务B之间的相互关系)。为了解决事务嵌套的问题,Spring又定义了一个记录与事务相关的信息的类TransactionInfo。这样A就有了一个对应的TransactionInfo对象,B也有了一个对应的TransactionInfo对象。而A调用了B,所以A和B对应的TransactionInfo对象之间就有了关联关系。在TransactionInfo类中有一个oldTransactionInfo属性,这个记录的就是老的事务对象。看过前面系列文章又好好思考过的童鞋可能已经明白了,在A中通过代理调用B的时候,此时两个事务位于同一线程中,接着将A对应的TransactionInfo对象赋值给B对应的TransactionInfo对象的oldTransactionInfo属性,然后将B对应的TransactionInfo对象绑定到ThreadLocal,接着待B执行完后,将当前线程绑定的TransactionInfo还原为A对应的TransactionInfo,此时就完成了嵌套事务之间信息传递。具体参见下面这幅图:

 上面这幅图并不完美,但将前面一段的描述图形化,这应该会好理解一点吧!在这段回顾中,我们不断提到ThreadLocal,那它究竟是干什么的?原理又是什么呢?想必大家也都听说过,ThreadLocal存在内存泄漏的风险,这又是咋回事呢?

1 ThreadLocal概述

ThreadLocal,即线程本地变量,是Java中用于提供线程局部存储的类,位于java.lang包下它的核心原理在于为每个使用ThreadLocal的线程创建一个独立的副本,使得每个线程在访问ThreadLocal时获取到的是自己线程内的私有数据,而不是共享同一份数据,从而避免了多线程间的同步问题。其工作机制是这样的:

  • 内部存储结构:ThreadLocal内部维护了一个Map数据结构,其键值是当前的ThreadLocal对象,值是我们想要隔离存储的对象实例这意味着每个线程都有一个独立的ThreadLocalMap来保存自己的ThreadLocal副本
  • 存取操作:
  1. set(T value)方法允许我们设置线程局部变量的值。它会将给定的值与当前执行线程关联起来,在该线程的ThreadLocalMap中存储
  2. get()方法则返回当前线程所对应的ThreadLocal变量的副本值。如果该线程尚未初始化,则可能返回默认值或抛出异常
  3. remove():移除线程本地变量。注意在线程池的线程复用场景中在线程执行完毕时一定要调用remove,避免在线程被重新放入线程池中时被本地变量的旧状态仍然被保存。
  • 线程生命周期管理:当线程结束生命周期后,其内部的ThreadLocalMap应该被清理以释放资源然而,如果线程不再使用某个ThreadLocal但未手动删除引用,可能会导致内存泄漏,因为ThreadLocalMap中的Entry不会自动移除
  • 弱引用与内存泄漏:在内部实现上,ThreadLocalMap的条目是通过弱引用(WeakReference)指向ThreadLocal实例的,这意味着只有当没有强引用指向ThreadLocal实例时,这些条目才能被垃圾回收器回收。但如果仅ThreadLocalMap中的条目持有目标对象的唯一引用,即使线程已经完成任务,由于弱引用的存在,目标对象也不会立即被回收,这可能导致无用对象占据内存空间,直到下次JVM进行垃圾回收周期时才可能清除。
  • 使用场景:ThreadLocal常用于需要在线程间隔离状态信息的情况,例如数据库连接、事务上下文、用户身份信息等,尤其是在处理每个线程都需要独立的实例且不希望影响其他线程的情况下。

总的来说,ThreadLocal通过为每个线程维护一份独立的数据副本,巧妙地实现了多线程环境下的数据隔离和安全性,并简化了代码的编写,减少了同步块或锁的使用。但它也要求开发者关注并正确管理其生命周期,以免引发内存泄露问题。

总的来说,ThreadLocal通过为每个线程维护一份独立的数据副本,巧妙地实现了多线程环境下的数据隔离和安全性,并简化了代码的编写,减少了同步块或锁的使用。但它也要求开发者关注并正确管理其生命周期,以免引发内存泄露问题。

2 Thread与ThreadLocal之间的关系

明白了ThreadLocal的基本概念后,我们再来看一下什么是Thread以及它与ThreadLocal之间的关系。首先来看一下Thread的基础知识:

在Java中,Thread是一个核心类,它位于java.lang包下,用于实现多线程功能。线程是程序中的一个执行流,它允许程序并发地执行多个任务。通过使用Thread类以及相关的线程机制,Java程序可以充分利用多核处理器的优势,进行高效的并发处理。

每个Java线程都与一个Thread类的实例相对应,这个实例代表了操作系统层面的一个线程实体Java通过封装操作系统的线程API,提供了跨平台的线程支持

Thread类的主要作用和方法包括:

  • 创建线程:可以通过继承Thread类并重写run()方法来创建新的线程;或者实现Runnable接口,并提供run()方法的具体实现,然后将Runnable实例传递给Thread构造器来创建线程对象
  • 启动线程:调用 `Thread` 对象的 `start()` 方法启动新线程,这会使得 JVM 创建一个新的执行线程,并在适当的时候调用该线程对应的 `run()` 方法。
  • 线程控制:`Thread` 类提供了多种方法来控制线程行为,例如:
  • sleep(long millis):使当前线程暂停指定的毫秒数。
  • join():让当前线程等待调用此方法的线程终止。
  • interrupt():中断线程。
  • isAlive():判断线程是否还在运行。
  • setPriority(int newPriority):设置线程优先级。
  • 获取线程信息:如获取或设置线程名(getName()和setName()),获取当前线程(currentThread())等。

下面我们针对创建线程一点进行展开,在java中想要创建一个线程,方法有四种,它们分别为:

  1. 继承Thread类:1) 创建一个新的类,让它继承自java.lang.Thread;2) 重写Thread类的run()方法,该方法包含了线程需要执行的任务代码;3) 创建这个子类的一个实例,并调用其start()方法来启动线程。
  2. 实现Runnable接口:1) 定义一个类实现java.lang.Runnable接口;2) 实现Runnable接口中的run()方法。3) 创建Thread对象时,将Runnable实例作为构造函数的参数传入,然后调用Thread对象的start()方法来启动线程。
  3. 实现Callable接口结合FutureTask:1) 实现java.util.concurrent.Callable接口,重写其中的call()方法,它可以有返回值且可以抛出异常;2) 使用FutureTask包装Callable对象,FutureTask实现了Runnable接口,因此可以作为Thread的目标任务;3) 创建并启动Thread。
  4. 使用ExecutorService和线程池:1) 创建一个java.util.concurrent.ExecutorService实例,通常会使用ThreadPoolExecutor或Executors工具类提供的工厂方法(如newFixedThreadPool(),newCachedThreadPool()等);2) 提交Runnable或者Callable任务到线程池中,例如使用executorService.submit()方法。

在实际开发场景中,我们可以根据实际需求选择不同的创建线程的方式。其中,使用线程池能够更有效地管理和控制线程资源,提高系统性能和响应能力。关于这四种创建方式的伪代码如下所示:

// 方式一
public class MyThread extends Thread {
    @Override
    public void run() {
        // 这里编写线程要执行的任务
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
    }
}

// 方式二
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 这里编写线程要执行的任务
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
    }
}

// 方式三
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 这里编写线程要执行的任务并返回结果
        int result = ...; // 计算结果
        return result;
    }
    public static void main(String[] args) {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        // 可以通过futureTask.get()获取线程运行的结果
    }
}

// 方式四
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        // 如果是Runnable任务
        Runnable task = () -> {
            // 这里编写线程要执行的任务
        };
        executor.execute(task);
        // 如果是Callable任务
        MyCallable callableTask = new MyCallable();
        Future<Integer> future = executor.submit(callableTask);
        // 关闭线程池
        executor.shutdown();
        // 对于Callable任务,可以通过future.get()获取结果
    }
}

上面我们梳理了线程的基本概念以及java中创建线程的几种方式,下面让我们来看一下Thread与ThreadLocal之间的关系,首先来看一下Thread的源码(由于代码很多,这里值列出类中的属性):

public class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

    private volatile String name;
    private int priority;

    /* Whether or not the thread is a daemon thread. */
    private boolean daemon = false;

    /* Interrupt state of the thread - read/written directly by JVM */
    private volatile boolean interrupted;

    /* Fields reserved for exclusive use by the JVM */
    private boolean stillborn = false;
    private long eetop;

    /* What will be run. */
    private Runnable target;

    /* The group of this thread */
    private ThreadGroup group;

    /* The context ClassLoader for this thread */
    private ClassLoader contextClassLoader;

    /* The inherited AccessControlContext of this thread */
    @SuppressWarnings("removal")
    private AccessControlContext inheritedAccessControlContext;

    /* For autonumbering anonymous threads. */
    private static int threadInitNumber;
    private static synchronized int nextThreadNum() {
        return threadInitNumber++;
    }

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

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

    /*
     * The requested stack size for this thread, or 0 if the creator did
     * not specify a stack size.  It is up to the VM to do whatever it
     * likes with this number; some VMs will ignore it.
     */
    private final long stackSize;

    /*
     * Thread ID
     */
    private final long tid;

    /* For generating thread ID */
    private static long threadSeqNumber;

    private static synchronized long nextThreadID() {
        return ++threadSeqNumber;
    }

    /*
     * Java thread status for tools, default indicates thread 'not yet started'
     */
    private volatile int threadStatus;

    /**
     * The argument supplied to the current call to
     * java.util.concurrent.locks.LockSupport.park.
     * Set by (private) java.util.concurrent.locks.LockSupport.setBlocker
     * Accessed using java.util.concurrent.locks.LockSupport.getBlocker
     */
    volatile Object parkBlocker;

    /* The object in which this thread is blocked in an interruptible I/O
     * operation, if any.  The blocker's interrupt method should be invoked
     * after setting this thread's interrupt status.
     */
    private volatile Interruptible blocker;
    private final Object blockerLock = new Object();
}

在代码中我们可以看到这样两句:ThreadLocal.ThreadLocalMap threadLocals = null和ThreadLocal.ThreadLocalMap inheritableThreadLocals = null它们是实例属性,也就是每个Thread对象都会有这两个属性。从代码可以看出,这两个属性是ThreadLocalMap类型,跟Map类似,理论上可以通过它在线程上存储很多数据(后面再详细梳理)。也就是从这里Thread和ThreadLocal产生了联系。那现在问题来了,程序是如何通过它在线程中存储数据的呢?下面来看这样一个案例:

public class SpringTransactionApplication {
    static ThreadLocal<String> local = new ThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        local.set(Thread.currentThread().getName() + " - 0987654321");
        Thread t = new Thread() {
            @Override
            public void run() {
                local.set(Thread.currentThread().getName() + " - 1234567890");
            }
        };
        t.start();
        t.join();
    }
}

在这个案例中,首先定义了一个类级别的ThreadLocal类型的变量local,接着在main方法中调用其set()方法开始保存值,随后又继续创建Thread对象t并在其run()方法中再次调用ThreadLocal的set方法开始保存值。看到这里,我又有点不明白了,这样就可以了?这个set()方法究竟做了什么?这还得看ThreadLocal的源码,如下所示:

public class ThreadLocal<T> {
    /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    /**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns {@code null}; if the
     * programmer desires thread-local variables to have an initial
     * value other than {@code null}, {@code ThreadLocal} must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }

    /**
     * Creates a thread local variable. The initial value of the variable is
     * determined by invoking the {@code get} method on the {@code Supplier}.
     *
     * @param <S> the type of the thread local's value
     * @param supplier the supplier to be used to determine the initial value
     * @return a new thread local variable
     * @throws NullPointerException if the specified supplier is null
     * @since 1.8
     */
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    /**
     * Creates a thread local variable.
     * @see #withInitial(java.util.function.Supplier)
     */
    public ThreadLocal() {
    }

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }

    /**
     * Returns {@code true} if there is a value in the current thread's copy of
     * this thread-local variable, even if that values is {@code null}.
     *
     * @return {@code true} if current thread has associated value in this
     *         thread-local variable; {@code false} if not
     */
    boolean isPresent() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        return map != null && map.getEntry(this) != null;
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        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;
    }

    /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

    /**
     * Factory method to create map of inherited thread locals.
     * Designed to be called only from Thread constructor.
     *
     * @param  parentMap the map associated with parent thread
     * @return a map containing the parent's inheritable bindings
     */
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

    /**
     * Method childValue is visibly defined in subclass
     * InheritableThreadLocal, but is internally defined here for the
     * sake of providing createInheritedMap factory method without
     * needing to subclass the map class in InheritableThreadLocal.
     * This technique is preferable to the alternative of embedding
     * instanceof tests in methods.
     */
    T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }

    /**
     * An extension of ThreadLocal that obtains its initial value from
     * the specified {@code 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();
        }
    }

    /**
     * ThreadLocalMap is a customized hash map suitable only for
     * maintaining thread local values. No operations are exported
     * outside of the ThreadLocal class. The class is package private to
     * allow declaration of fields in class Thread.  To help deal with
     * very large and long-lived usages, the hash table entries use
     * WeakReferences for keys. However, since reference queues are not
     * used, stale entries are guaranteed to be removed only when
     * the table starts running out of space.
     */
    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * Decrement i modulo len.
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

        /**
         * Construct a new map including all Inheritable ThreadLocals
         * from given parent map. Called only by createInheritedMap.
         *
         * @param parentMap the map associated with parent thread.
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (Entry e : parentTable) {
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

        /**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.refersTo(key))
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                if (e.refersTo(key))
                    return e;
                if (e.refersTo(null))
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

        /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            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.refersTo(key)) {
                    e.value = value;
                    return;
                }

                if (e.refersTo(null)) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

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

        /**
         * Remove the entry for key.
         */
        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.refersTo(key)) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

        /**
         * Replace a stale entry encountered during a set operation
         * with an entry for the specified key.  The value passed in
         * the value parameter is stored in the entry, whether or not
         * an entry already exists for the specified key.
         *
         * As a side effect, this method expunges all stale entries in the
         * "run" containing the stale entry.  (A run is a sequence of entries
         * between two null slots.)
         *
         * @param  key the key
         * @param  value the value to be associated with key
         * @param  staleSlot index of the first stale entry encountered while
         *         searching for key.
         */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.refersTo(null))
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                if (e.refersTo(key)) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (e.refersTo(null) && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

        /**
         * Expunge a stale entry by rehashing any possibly colliding entries
         * lying between staleSlot and the next null slot.  This also expunges
         * any other stale entries encountered before the trailing null.  See
         * Knuth, Section 6.4
         *
         * @param staleSlot index of slot known to have null key
         * @return the index of the next null slot after staleSlot
         * (all between staleSlot and this slot will have been checked
         * for expunging).
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

        /**
         * Heuristically scan some cells looking for stale entries.
         * This is invoked when either a new element is added, or
         * another stale one has been expunged. It performs a
         * logarithmic number of scans, as a balance between no
         * scanning (fast but retains garbage) and a number of scans
         * proportional to number of elements, that would find all
         * garbage but would cause some insertions to take O(n) time.
         *
         * @param i a position known NOT to hold a stale entry. The
         * scan starts at the element after i.
         *
         * @param n scan control: {@code log2(n)} cells are scanned,
         * unless a stale entry is found, in which case
         * {@code log2(table.length)-1} additional cells are scanned.
         * When called from insertions, this parameter is the number
         * of elements, but when from replaceStaleEntry, it is the
         * table length. (Note: all this could be changed to be either
         * more or less aggressive by weighting n instead of just
         * using straight log n. But this version is simple, fast, and
         * seems to work well.)
         *
         * @return true if any stale entries have been removed.
         */
        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.refersTo(null)) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

        /**
         * Re-pack and/or re-size the table. First scan the entire
         * table removing stale entries. If this doesn't sufficiently
         * shrink the size of the table, double the table size.
         */
        private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }

        /**
         * Double the capacity of the table.
         */
        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (Entry e : oldTab) {
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

        /**
         * Expunge all stale entries in the table.
         */
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.refersTo(null))
                    expungeStaleEntry(j);
            }
        }
    }
}

通过源码可以清楚的看到,和前面说的一样,ThreadLocal中提供了几个常用的方法,不过还需要注意的是在这个类里面有个内部类ThreadLocalMap(其核心是一个Entry数组,这个Entry继承了WeakReference),关于这个类,后面会仔细梳理。这里我们只关注其在Thread中的使用(定义)及初始化。这里再啰嗦一下,在Thread中使用其定义了两个属性,一个名为threadLocals,一个名为inheritableThreadLocals,其中后者会在Thread的构造函数中进行初始化,而前者不会。后者的初始化逻辑如下所示:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

这段代码中的inheritThreadLocals的值是调用者传进来的,创建线程时一般使用的是默认构造方法,此时这个值默认就是true。此时就要再判断一下parent. inheritableThreadLocals是否为空了,这个parent指的是当前线程,也可以理解成是待创建线程的父线程,获取这个线程的代码为:Thread parent = currentThread()。根据前面的案例来看,这里的parent值就是主线程,即main,这个时候parent. inheritableThreadLocals!=null这个判断条件的结果为false,所以此时案例中创建的线程t也不会拥有inheritableThreadLocals值。具体如下图所示:

这里假设存在,那么最终会调用ThreadLocal.createInheritedMap()方法创建一个ThreadLocalMap对象,这个方法的源码为:

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

至此我们把inheritableThreadLocals的创建逻辑梳理完了。下面再看一下threadLocals的初始化逻辑:前面讲过Thread中没有创建或初始化这个属性的地方,所以我们需要回到案例中ThreadLocal.set(value)这段逻辑上,先来看一下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);
    }
}

这段代码会首先获取当前线程,拿案例t线程run()方法的处理逻辑来说吧,这里拿到的Thread是线程t,接着会调用getMap(Thread)来获取当前线程上的ThreadLocalMap,即线程对象上名为threadLocals的属性,该方法的源码如下所示:

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

继续回到set(T)这个方法中,如果拿到的ThreadLocalMap值不为空,则直接向其上设置一个新值,否则调用createMap()方法,先来看一下createMap()方法的源码:

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

至此threadLocals的初始化逻辑就梳理完了。但是其中很关键的ThreadLocalMap部分,这里并没有给予过多的描述,因为我还不清楚要咋描述。等梳理好了再说吧!

好了,今天先到这里吧。通过梳理,我明白了1) 线程的几种创建方式;2) ThreadLocal的基本用法;2) Thread、ThreadLocal及ThreadLocalMap之间的相互关系:Thread是一个线程;ThreadLocal是一个辅助操作Thread中的ThreadLocalMap对象的工具;而ThreadLocalMap是Thread中的一个变量,用于存储当前线程的本地数据。这里还想跟大家分享一个因为知其然而不知其所以然闹出的伤心的故事:

面试官:ThreadLocal有用过吗?

我:有!ThreadLocal是线程本地变量,通过它每个线程可以存储自己的数据,从而避免线程安全问题。

面试官:那你能说一下它得原理吗?

我:通过调用ThreadLocal的set()方法,会向线程的ThreadLocalMap对象中存储一个数据。之前有跟过源码,但有一点我一直没弄明白,存储数据的时候会有一个this,就是说在每个线程的run方法中无论调多少次set方法,线程的本地变量集中都会只有一个值。我很奇怪,为啥一定要定义成map呢?

面试官:∙∙∙∙∙,行今天先到这里吧!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

机器挖掘工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值