子线程无法访问父线程中通过ThreadLocal设置的变量

一、引出结论

学习过ThreadLocal的童鞋都知道,在子线程中,是无法访问父线程通过ThreadLocal设置的变量的。

package thread;

/**
 * @author heyunlin
 * @version 1.0
 */
public class ThreadLocalExample {

    public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();

        threadLocal.set("hello");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("run()...");

                /*
                 * 子线程中无法访问父线程中设置的ThreadLocal变量
                 */
                System.out.println(threadLocal.get());
            }
        });

        thread.start();
        thread.join();

        System.out.println(thread);
        System.out.println(threadLocal.get());
    }

}

运行结果:

InheritableThreadLocal就是为了解决这个不可见问题而生的~

package thread;

/**
 * @author heyunlin
 * @version 1.0
 */
public class InheritableThreadLocalExample {

    public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
        InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

        threadLocal.set("hello");

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("run()...");
                System.out.println(threadLocal.get());
            }
        });

        thread.start();
        thread.join();

        System.out.println(thread);
        System.out.println(threadLocal.get());
    }

}

运行结果:

二、分析原因

那么, 究竟是什么原因导致的子线程无法访问父线程中的ThreadLocal变量呢,接下来研究ThreadLocal这个类的源码。

先看一下这个ThreadLocal类上的文档注释,大概了解ThreadLocal是用来干什么的

/**
 * 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).
 *
 * <p>For example, the class below generates unique identifiers local to each
 * thread.
 * A thread's id is assigned the first time it invokes {@code ThreadId.get()}
 * and remains unchanged on subsequent calls.
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // Atomic integer containing the next thread ID to be assigned
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // Thread local variable containing each thread's ID
 *     private static final ThreadLocal&lt;Integer&gt; threadId =
 *         new ThreadLocal&lt;Integer&gt;() {
 *             &#64;Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // Returns the current thread's unique ID, assigning it if necessary
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * </pre>
 * <p>Each thread holds an implicit reference to its copy of a thread-local
 * variable as long as the thread is alive and the {@code ThreadLocal}
 * instance is accessible; after a thread goes away, all of its copies of
 * thread-local instances are subject to garbage collection (unless other
 * references to these copies exist).
 *
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */

1、关键注释

博主在类的文档注释上提取了几个关于ThreadLocal的重要的说明

This class provides thread-local variables. 

这个类提供了线程本地变量

ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread. 

ThreadLocal实例提供了可以关联一个线程的静态字段。

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

只要线程是存活状态,并且ThreadLocal对象可以访问,每个线程都拥有一个线程本地变量的副本的引用;

在线程执行完方法之后,所有线程本地变量都会被gc(垃圾收集器)回收,除非有其他变量引用了它。

2、研究源码

经过上面博主的大致翻译,已经对ThreadLocal有了一定的了解,接下来深入源码去学习ThreadLocal的工作原理。

在不了解ThreadLocal的情况下,直接从我们直接调用的get()和set()方法入手。

public class ThreadLocal<T> {

    public T get() {
        Thread thread = Thread.currentThread();
        ThreadLocalMap map = getMap(thread);

        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);

            if (e != null) {
                T result = (T)e.value;

                return result;
            }
        }

        return setInitialValue();
    }

    public void set(T value) {
        Thread thread = Thread.currentThread();
        ThreadLocalMap map = getMap(thread);

        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

}

set()方法

    public void set(T value) {
        // 获取当前线程
        Thread thread = Thread.currentThread();
        // 根据当前线程获取ThreadLocalMap对象
        ThreadLocalMap map = getMap(thread);

        // map不为null,设置初始值到map中
        if (map != null) {
            map.set(this, value);
        } else {
            // map是null,创建一个map
            createMap(t, value);
        }
    }

 getMap()

看样子和线程类Thead里的一个threadLocals变量有关。

    ThreadLocal.ThreadLocalMap getMap(Thread thread) {
        // 这个方法就是返回Thread的threadLocals变量的值
        return thread.threadLocals;
    }

ThreadLocalMap是ThreadLocal的一个静态内部类

public class ThreadLocal<T> {

    static class ThreadLocalMap {
    
    }

}

 set()

ThreadLocal.ThreadLocalMap的set()方法,有点复杂,类似HashMap的底层源码实现。

    private void set(ThreadLocal<?> key, Object value) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);

        for (ThreadLocal.ThreadLocalMap.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 ThreadLocal.ThreadLocalMap.Entry(key, value);
        int sz = ++size;
        
        if (!cleanSomeSlots(i, sz) && sz >= threshold) {
            rehash();
        }
    }

createMap()
    void createMap(Thread thread, T firstValue) {
        // 初始化线程的threadLocals变量
        thread.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
    }

set()方法总结

根据set()方法的源代码,set()方法是把值设置到Thead线程类的threadLocals变量中,这个变量应该是一个Map派生类的实例。

get()方法

    public T get() {
        Thread thread = Thread.currentThread(); // 获取当前线程
        
        // 通过getMap()方法获取一个ThreadLocalMap对象
        // 因为这个类是以Map结尾的,推测是java.util.Map的派生类
        ThreadLocalMap map = getMap(thread);

        // 获取到的map不为null
        if (map != null) {
            // 获取Map的Entry
            // 说明我们的推测大概率是正确的,因为HashMap中也有个Enty,而且也有value()方法
            ThreadLocalMap.Entry e = map.getEntry(this);

            // 获取的value不为空,则转为指定的类型T(泛型)直接返回
            if (e != null) {
                T result = (T) e.value;

                return result;
            }
        }

        // 代码运行到这里,说明获取到的map是null
        // 因为前面的if中已经通过return结束方法
        return setInitialValue();
    }

getMap()

这个方法的代码在前面set()方法里已经看了~

createMap()

这个方法的代码在前面set()方法里已经看了~

setInitialValue()

这个方法和set()方法的代码几乎一模一样。知识多了一个返回值~

T value = initialValue();

set()方法的代码

return value;

    private T setInitialValue() {
        // 通过initialValue()方法获取一个初始化值
        T value = initialValue();
        
        // 获取当前线程
        Thread thread = Thread.currentThread();
        // 根据当前线程获取ThreadLocalMap对象
        ThreadLocalMap map = getMap(thread);

        // map不为null,设置初始值到map中
        if (map != null) {
            map.set(this, value);
        } else {
            // map是null,创建一个map
            createMap(thread, value);
        }

        // 返回初始值
        return value;
    }

get()方法总结

综合上面关于get()方法的源代码,get()方法是从Thead线程类的threadLocals变量中获取数据。

ThreadLocal.ThreadLocalMap

下面是ThreadLocalMap类的一些关键的代码,很显然,这个内部类的结构设计的和HashMap非常相似,通过一个数组table保存值,根据传入的key进行特定的hash运算得到数组的下标,然后获取对应数组下标的值返回。

public class ThreadLocal<T> {

    static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

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

                value = v;
            }
        }

        private static final int INITIAL_CAPACITY = 16;

        private ThreadLocal.ThreadLocalMap.Entry[] table;

        private int size = 0;

        private int threshold;

        private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            ThreadLocal.ThreadLocalMap.Entry e = table[i];
            
            if (e != null && e.get() == key) {
                return e;
            } else {
                return getEntryAfterMiss(key, i, e);
            }
        }
    }
}

三、代码总结

文章稍微学习的深入了一点,可能有些童鞋已经懵了,但是回到set()和get()方法的代码可以发现

不管你存储值的代码如何复杂,都是通过当前线程作为key存储在ThreadLocal的静态内部类ThreadLocalMap中的,知道这点其实就够了。

在子线程中,由于通过new Thread()新开了一个线程,那么当代码执行这个新开的子线程的run()方法内部,当前线程已经不是刚开始执行方法的线程了。

比如:在main方法中,执行代码的线程是main线程,也就是主线程,它的线程名就是main。

但是新开一个线程,它的线程名是通过Threa-0开始编号的

通过一个简单的代码了解一下

/**
 * @author heyunlin
 * @version 1.0
 */
public class Example {

    public static void main(String[] args) {
        System.out.println(Thread.currentThread());

        new Thread() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread());
            }
        }.start();
    }

}

运行结果:


 

也就是说,在不同线程中,当前线程已经发生了改变,调用ThreaLocal的get()方法的key变了,自然获取不到我们当初设置的变量值。


好了,文章就分享到这里了,看完不要忘了点赞+收藏哦~

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值