前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >设计ThreadLocal的那段日子

设计ThreadLocal的那段日子

作者头像
ImportSource
发布2018-04-03 15:27:38
7490
发布2018-04-03 15:27:38
举报
文章被收录于专栏:ImportSource

假设现在让你去实现一个连接类,要支持多个线程访问,同时每个线程独占一个Connection?

这时候你会怎么实现?

也许这会你想到了给每个线程传递一个Connection instance。

这样是可以实现。但你想过没有。每次只传输一个同样配置的实例,只是引用地址不一样。是不是从设计的角度很不优雅呢?而且还容易出错,不变性也被破坏了。

于是你又想,那就把这个Connection搞成同步的。让大家排队来用一个Connection。

这种做法显然不满足需求啊。每人用一个,总比大家用一个好吧。

于是Josh Bloch 和 Doug Lea这两个神牛开始考虑一种新的思路。

既然其它都一样,只是需要一个线程一个实例而已。那我们就开发一个

基于线程的一个本地变量的支持类。(ps:这两个人设计了ThreadLocal)

这样以后遇到类似的需求,开发者们就可以使用我们的这个类了。

这个类叫什么名字呢?既然和线程强绑定,而且是线程独有的,那就是本地的喽。好吧,那就你叫ThreadLocal啦?Josh Bloch一边敲着键盘一边问Doug Lea。Doug Lea想了下,嗯,这个名字不错,就这样叫吧。而且这个类老实讲并发占的比重不大,就听Josh Bloch的吧。。。。。(lea是并发包的主要缔造者,你懂的,而且threadlocal位于java.lang包下)

于是这两货就兴致勃勃地开始开发这个类了。

既然是一个变量。我们那些普通的变量都有两个操作,写值和读值。那我们这个封装类也得提供这两个操作。这样看起来才像一个变量该有的样子。

于是lea兄果断的写了这么两个个方法:

代码语言:javascript
复制
public T get() {
   //todo 
}
代码语言:javascript
复制
public void set(T value) {
    
}

然后他想,这设置进来的值还没法指定类型。像这种该存到什么数据结构中呢?他脑中迅速闪过了map这个三个字母。M......A......P,没错,万能的map,可以存储很多种类型啊。

嗯,那就用map来搞存储吧。

于是lea和josh决定使用map来存储真实的变量实例。

这时候问题又来了。难道就用现在有的那些map来搞吗? 不行,我们还是要内部再搞一个map,就叫ThreadLocalMap。这个map反正也不对外提供服务就把他放在ThreadLocal类内部吧,搞成个静态内部类算了。

于是他们在threadlocal内部新建了一个ThreadLocalMap的内部静态类。lea熟练的把现在有的那个hashmap代码复制了进来。map的结构都是相似的。

map内部其实就是一个数组,比如要有下面的这些代码:

代码语言:javascript
复制
/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
 */
private Entry[] table;

还有capacity等等:

代码语言:javascript
复制
/**
 * The initial capacity -- MUST be a power of two.
 */
private static final int INITIAL_CAPACITY = 16;

另外还要有一个内部的entry类来承载每一个key value的存储。

自然要设计一个entry实体类。

代码语言:javascript
复制
static class Entry {  
代码语言:javascript
复制
}

由于map中的内容一直hold在里边,会造成gc回收的一些问题,我们要把这个entry设计成一个弱引用。好,改成下面这样:

代码语言:javascript
复制
static class Entry extends WeakReference<ThreadLocal> {
   
}

嗯,就这样搞。按理说这里边我们应该加两个属性,一个叫key,一个叫value。传统的map都喜欢这么做。但这里我们其实不需要key,只需要存储value,也就是那个变量的值。

代码语言:javascript
复制
static class Entry extends WeakReference<ThreadLocal> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

嗯,就这么搞吧,里边加个属性叫value。然后通过构造函数传入进来。

既然是一个map。最重要的两个方法自然少不了。

那就是set(key,value)和get(key),类似这样的。

get

代码语言:javascript
复制
private Entry getEntry(ThreadLocal key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

set:

代码语言:javascript
复制
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)]) {
        ThreadLocal k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

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

其实最核心的就是你得去new 一个entry然后设置到table数组中,就是下面这句:

代码语言:javascript
复制
tab[i] = new Entry(key, value);

ok,现在map雏形差不多了。我们开始编写threadlocal类中的get set方法吧。

数据结构都有了,接下来的事情就好搞了。

先写set方法吧。

代码语言:javascript
复制
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

逻辑很简单,就是先通过传入本地线程,然后获得一个map用来存储value。

key就是threadlocal现在的实例引用,然后存储到map中。

你也许发现了map是从哪里获取来的?其实每个线程都对应一个map,就是threadlocalmap。只要你通过

代码语言:javascript
复制
Thread.currentThread()

拿到当前线程实例,然后传入下面这个方法就可以从thread类中拿到map。

代码语言:javascript
复制
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

好,继续写get方法:

代码语言:javascript
复制
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

既然set我们已经搞清楚,get就很简单了。通过threadlocal为key去当前线程对应的map中查找到value返回即可。

好,现在这个类已经设计的差不多了。

写上我们的大名吧:

代码语言:javascript
复制
* @author  Josh Bloch and Doug Lea

瞧瞧我两好叼,再把版本加上吧。

代码语言:javascript
复制
* @since   1.2

Cheers!

上自拍:

i am josh bloch,嘿嘿,collection framework就是我搞的。

i am doug lea,并发包就是我搞的

好,继续,那ThreadLocal究竟怎么用呢?

代码语言:javascript
复制
public Class ConnectionManager {

       //创建一个私有静态的并且是与事务相关联的局部线程变量  
       private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>;

   public static Connection getConnection(){
       //获得线程变量connectionHolder的值conn  
       Connection conn = connectionHolder.get();
       if (conn == null){
           //如果连接为空,则创建连接,另一个工具类,创建连接  
           conn = DbUtil.getConnection();
           //将局部变量connectionHolder的值设置为conn  
           connectionHolder.set(conn);
       }
       return conn;
   }  
}

------------------

上面这段是ThreadLocal的一个比较典型的使用场景。就是数据库的Connection。

你看了这段代码后也许并不能感受到他的运行机制。下面上一个例子:

代码语言:javascript
复制
public class SequenceNumber {


    //通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
    private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
        public Integer initialValue() {
            return 0;
        }
    };
    //获取下一个序列值
    public int getNextNum() {
        seqNum.set(seqNum.get() + 1);
        return seqNum.get();
    }

    public static void main(String[] args) {
        SequenceNumber sn = new SequenceNumber();
        //3 个线程共享sn,各自产生序列号
        TestClient t1 = new TestClient(sn);
        TestClient t2 = new TestClient(sn);
        TestClient t3 = new TestClient(sn);
        t1.start();
        t2.start();
        t3.start();
    }

    private static class TestClient extends Thread {
        private SequenceNumber sn;

        public TestClient(SequenceNumber sn) {
            this.sn = sn;
        }

        public void run() {
            for (int i = 0; i < 3; i++) {//④每个线程打出3个序列值
                System.out.println("thread[" + Thread.currentThread().getName() +
                        "] sn[" + sn.getNextNum() + "]");
            }
        }
    }
}

运行结果是这样的:

thread[Thread-0] sn[1] thread[Thread-0] sn[2] thread[Thread-0] sn[3] thread[Thread-1] sn[1] thread[Thread-1] sn[2] thread[Thread-1] sn[3] thread[Thread-2] sn[1] thread[Thread-2] sn[2] thread[Thread-2] sn[3]

也许你发现了,每个线程所产生的序列号都是有序增长的。说明每个线程都是用的自己的副本。

此时你再仔细想想,假如没有ThreadLocal,你会怎么实现这样的功能呢?如果不通过参数传递,恐怕还真没有其它好的办法。

所以以后遇到类似的需求,你就可以通过ThreadLocal把你要声明的变量做一个包装,然后通过set get来操作就好了。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2016-12-20,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 ImportSource 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档