从设计者的角度理解ThreadLocal

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/stupid_3/article/details/80070290

版权声明:本文为 罗周杨 [email protected] 原创,未经授权不得转载。

本文将从设计者的角度着手解析ThreadLocal。如果你是设计者,你会怎么设计?

需求

我们看到的代码都是某种需求的产物。假设有这样一个需求: 我有一些数据,我希望每一个线程都可以访问它,并且更新之后,不影响其他线程的值,也就是说,每一个线程对该数据都可以自由读写,线程之间互不干扰。
如果是你,你会怎么设计呢?

大致有两个思路。第一种,将这些线程和数据组成的关系对,存放在一个公共的区域,各个线程都可以自由获取,并且通过某些手段保证数据在线程之间互不影响。第二种,每个线程创建一个成员变量来保存这个关系对。  

首先想到的HashMap

我们首先来看看第一种方式。

你一定知道HashMap,它对每一个键,都可以存储对应的一个值,并且根据不同的键,可以获取到对应的值。HashMap是不是符合我们的需求呢?

是的,看起来完全符合。但是,这里面有一点小小的区别。因为我们要区分的是不同的线程,而不是不同的键。那我把线程当成键不就行了?那么我们试试看。  

给我们实现需求的代码起一个名字吧,就叫做ThreadLocal。按照目前的逻辑,代码大概长成这样:

public class ThreadLocal {
    private Map<Thread, Object> mMap = new HashMap<>();
    public void set(Object o) {
        mMap.put(Thread.currentThread(), o);
    }
    public Object get() {
        return mMap.get(Thread.currentThread());
    }
    public void remove() {
        mMap.remove(Thread.currentThread());
    }
}

额,干脆改成泛型吧:

public class ThreadLocal<T> {
    private Map<Thread, T> mMap = new HashMap<>();
    public void set(T o) {
        mMap.put(Thread.currentThread(), o);
    }
    public T get() {
        return mMap.get(Thread.currentThread());
    }
    public void remove() {
        mMap.remove(Thread.currentThread());
    }
}

看起来不错。但是似乎有了一个新的问题。

一个新的问题

问题是,这样的话,我每一个线程只能存储一个数据(尽管这个数据是泛型)?如果我要存储多个数据是不是就没办法了?

扫描二维码关注公众号,回复: 3676035 查看本文章

按照目前的设计,似乎是这样的。那么我们可以改进吗?看起来没有办法。因为当前是把线程本身当做键,也就是说这个键对于每一个线程来说都是唯一的,那通过这个键获取的值,也就只能是一个。问题出现在这个键上面。  

那我们是不是可以换一种方式,不要把线程当做键不就行了?事实上,不仅仅是线程本身不能作为键,所有能够唯一标志该线程的东西,都不能作为键。  

如果不能使用标志线程的东西作为键,那我们能用什么呢?一时半会儿好像没有什么注意。我们不妨整理一下目前的处境。  

首先,使用一个ThreadLocal存储线程的一个值是没问题的。但是,线程可能需要存储多个值。另外,一种想法是类似于HashMap存储多个Entry,我们使用ThreadLocalMap来存储多个Entry,把ThreadLocal作为Entry,但是ThreadLocalMap的键需要好好设计一下。  

你肯定已经发现了,使用ThreadLocalMap这个想法其实包含了以下要求:
* 这个ThreadLocalMap不能放在公共的区域,因为放在公共的区域,每个线程去获取的话,只能通过标志线程的东西作为键,这个问题我们之前已经讨论过了

这样的话,我们只能把ThreadLocalMap当做线程(Thread)的一个成员变量,让线程自己持有这些数据关系。这其实也就是我们当初的第二种想法:

每个线程创建一个成员变量来保存这个关系对

ThreadLocalMap作为Thread的成员变量

终于,我们走到这条道路上面来了。实际上JDK里面的Thread类确实包含ThreadLocalMap成员变量,但是数量上不是一个,而是两个。你可以去看看源代码,然后想想为什么。

现在我们已经明确了使用ThreadLocalMap,那么我们该如何设计ThreadLocalMap呢?  

几个既有的事实

我们先理清楚以下几个事实:  
* ThreadLocal的get()方法是通过Thread.currentThread()作为键来获取值的。  
* 不管在哪里的代码块,我们总是能够通过 Thread.currentThread()获取当前执行代码的线程
* 每个线程都可以获取自己的成员变量——ThreadLocalMap。  

这也就是说,即使是任何一个地方的ThreadLocal,我们也可以通过ThreadLocal.getMap()类似的方法来获取当前线程的ThreadLocalMap。逻辑很简单:  

code block{ 
    ThreadLocal threadLocal//ThreadLocal变量  
    Thread t = Thread.currentThread() //当前执行代码的线程
    ThreadLocalMap map = t.getMap() //当前线程的所有thread local数据
}

如果是这样的话,那我们干脆给ThreadLocal封装一个获取对应线程的ThreadLocalMap数据的方法,就叫做getMap()吧,它的内容大概应该:

public ThreadLocalMap getMap() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = t.getMap();
    return map;
}

聪明的你肯定发现了,ThreadLocalMap存放的所有该线程的数据对,那我为什么不进一步将ThreadLocalMap里面的数据取出来,直接返回该数据,而不是返回ThreadLocalMap呢

没错!!!那么的代码应该是这个样子了:

public T get(Key k) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = t.getMap();
    T value = map.get(k);
    return value;
}

看起来不错,也就是说我们可以直接根据KeyThreadLocal类而不是ThreadLocalMap类获取线程独立的数据。那么这个Key应该是什么呢?  

问题还是在于如何设计Key  

我们终于看到了一点希望。刚刚我们说的如何设计键的问题再次出现了。

等等,好像有点不太对,我们一开始的ThreadLocal的get方法没有参数呀,我们想要的是把当前线程作为一种区分约束,直接获取线程独立的数据值。但是我们现在的get方法为何出现了Key这个参数?  

看起来我们已经偏离需求了。那怎么办呢?按照需求来吧,先把参数Key去掉,然后我们再想办法解决其他问题。  

现在的问题是,我们已经失去了Key这个参数,但是ThreadLocalMap又需要一个Key来获取对应的值。那该怎么办呢?这个Key总不能无中生有吧?  

无中生有是不可能了。但是根据你的经验,应该知道,有些东西其实是真是存在的,但是一般情况下似乎隐藏起来了。比如代表当前对象的this。  

神奇的this

说到这里,你可能焕然大悟:ThreadLocalMap的键用ThreadLocal不就行了!!!
好像信息量有点大,我们一步一步理清楚。第一个问题,thisThreadLocalMap的键是如何联系起来的?第二个问题,用ThreadLocal作为键真的可以吗?  

第一个问题其实很简单,看代码吧:  

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = t.getMap();
    T value = map.get(this); //其实就是把当前ThreadLocal对象作为键
    return value;
}

第二个问题,这样可行吗?假设就是把ThreadLocal作为Key,我们先理一理现在的ThreadLocal、ThreadLocalMap、Thread大概是什么样的。  
Thread包含ThreadLocalMap成员变量:

public class Thread {
    ...
    ThreadLocalMap map;
    ...
}

ThreadLocalMap包含多个Entry,就类似于HashMap,每个Entry的键为ThreadLocal,值类型和ThreadLocal的值类型保持一致:

public class ThreadLocalMap {
    Entry[] table;
    private static class Entry<ThreadLocal, T> {
    ...
    }
    ...
}

ThreadLocal相对简单,主要还是那几个方法。根据前面的讨论,数据其实都存放在ThreadLocalMap里面了,因此ThreadLocal的几个方法都是对ThreadLocalMap对应方法的封装:  

public class ThreadLocal<T> {
    public void set(T o) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = t.getMap();
        map.set(this, o);
    }
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = t.getMap();
        T value = map.get(this);
        return value;
    }
    public void remove() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = t.getMap();
        map.remove(this);
    }
}

仔细看看代码,好像完全满足了需求!!!首先,当前线程想要存放在ThreadLocal里面的数据,全部保存在了Thread的成员变量ThreadLocalMap里面,这当然是线程无关的。其次,当前线程想要获取值的时候,直接从Thread的成员变量ThreadLocalMap里面取出来即可。再者,如果当前线程需要存放多个数据,只需要构造出多个ThreadLocal对象,存到自己的ThreadLocalMap即可

结语

至此,其实我们已经设计出了一个符合需求的ThreadLocal。尽管目前的设计是很粗糙的,我们的思路其实是完全正确的。  

好好感受一下我们从零开始到现在设计出一个粗糙的ThreadLocal的历程,再结合JDK的源代码,感受一下大师们的成品是怎么样的,你会收获更多。  

希望本文这种从设计者的角度理解ThreadLocal的方式对你有帮助。

联系我:

Email: [email protected]
WeChat: luozhouyang0528

猜你喜欢

转载自blog.csdn.net/stupid_3/article/details/80070290