前言

最近写业务有一个AOP的切面使用了threadlocal方式存储了业务执行时需要交给AOP处理的数据。但是我发现Aspect使用了一个名字叫ThreadClient的类获取了一个threadlocal的引用,通常我的threadlocal都是定义到Aspect内只给当前业务使用并且是已经初始化过的静态常量(这样在类被初始化(加载字节码->链接->初始化)时,静态常量就会被赋值),所以没有见过还有单例模式提供threadlocal的,所以就看了一下怎么实现的。

现象

版本(抛开版本就是耍流氓~)
jdk8
sprintboot 2.3.12

现象
这个不是问题,我们直接看下代码

public class ThreadLocalClient {

	static volatile ThreadLocal<Map> threadLocal = null;
	
	// 类级别变量(用作锁,其实可以直接用ThreadLocalClient.class)
	private final static String LOCK = "LOCK";
	
	public static ThreadLocal<Map> getThreadLocal() {
		if (threadLocal == null) {
			synchronized (LOCK){ 
				if (threadLocal == null) {
					threadLocal = new ThreadLocal<Map>();
				}	
			}
		}
		return threadLocal;
	}

分析原因

首先有几个疑问直接在我心里出现:
1、threadlocal不是线程私有变量么?为什么需要单例?
2、threadlocal如果是静态常量,线程之间的数据不会串线吧?
3、threadlocal为什么使用volatile修饰?修饰了保持了内存可见性之后,为什么还要synchronized关键字修饰赋值的临界区?

OK,接下来开始一个个解答:
问题1和问题2:

    /**
     * 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();
    }
    .........................
        /**
     * 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;
    }

首先看一段源码,就是threadlocal.get()方法,这里面可以发现不论是任何线程,首先取得是当前执行的线程引用Thread t,Thread.currentThread();可以在任何地方执行,似乎跟当前类没有关系,唯一有关系的就是ThreadLocalMap.Entry e = map.getEntry(this);
以当前ThreadLocal引用作为key获取到当前线程t中的ThreadLocalMap成员变量的一个Entry,那么到这里我们就可以回答问题1和问题2了,首先单例模式或者使用静态变量都可以直接用来设置ThreadLocal,并且线程之间不会串线,因为每一个线程都是独立的ThreadLocalMap,虽然传入的key(threadlocal)相同。

问题3:
首先复习一下volatile关键字,这个关键字最详细的我觉得在周志明老师的《深入理解jvm虚拟机》的章节中解释的比较清晰,首先这个关键字有两个特性:第一个就是同步变量的可见性,第二个就是代码指令的重排序。具体解释这里不详细赘述了,有一个重要的结论,volatile关键字从计算机原理的角度来看,无法真的保证线程的可见性,因为线程栈是私有的,特别是在对共享变量进行修改后写入的操作时(如果只是读取就还好),当线程1要修改值时就必须要将操作数压入栈中,修改完成后再写入内存,而在读取的到栈的过程中就会出现并发的情况,但两个线程确实保持了内存的可见性,只是写内存的时候可能会互相覆盖,因此必须配合synchronized关键字来做线程同步达到强同步的作用。
理解上面的解释后,threadlocal变量的初始化就非常好解释了,首先threadlocal必须要被初始化,并且不能出现线程1初始化后线程2又覆盖了原来的单例,也就是说thread只能初始化一次!为什么呢?问题一和问题二的解答中说过,当前线程的引用作为key来获取线程变量,如果被另一个线程赋值为其他引用的话,那么先前的线程变量就会丢失!因此synchronized和volatile一起配合才能保证threadlocal只会初始化一次。

思考感悟

其实我觉得最安全的办法就是使用静态常量直接赋值的方式,在类刚加载时,该变量就已经被赋值,并且使用final修饰之后,引用更加安全。

09-13 00:27