360SDN.COM

首页/Java/列表

设计ThreadLocal的那段日子

来源:ImportSource  2017-10-09 10:48:48    评论:0点击:

假设现在让你去实现一个连接类,要支持多个线程访问,同时每个线程独占一个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兄果断的写了这么两个个方法:

public T get() {
  //todo
}
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内部其实就是一个数组,比如要有下面的这些代码:

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

还有capacity等等:

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

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

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

static class Entry {  
}

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

static class Entry extends WeakReference<ThreadLocal> {
 
}

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

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

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:

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数组中,就是下面这句:

tab[i] = new Entry(key, value);

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

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

先写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);
}

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

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

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

Thread.currentThread()

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

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

好,继续写get方法:

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返回即可。

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

写上我们的大名吧:

* @author  Josh Bloch and Doug Lea

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

* @since   1.2

Cheers!

上自拍:


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


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

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

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。

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

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来操作就好了。

阅读原文

为您推荐

友情链接 |九搜汽车网 |手机ok生活信息网|ok生活信息网|ok微生活
 Powered by www.360SDN.COM   京ICP备11022651号-4 © 2012-2016 版权