Netty学习之旅----ThreadLocal原理分析与性能优化思考(思考篇)

/**
     * 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();    //@1
        ThreadLocalMap map = getMap(t);   //@2
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);     //@3
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();                      // @4
    }

1)FastThreadLocal初始化的时候,就会默认生成确定的索引下标,InternalThreadLocalMap存放索引表(数组),这样便于快速的找到对象。
2)针对remove方法,同样在类初始化的时候variablesToRemoveIndex会生成确定的索引下标,每次在进行set时都会将对应的FastThreadLocal存放到threadLocalMap中,这样方便在removeAll时进行清理对应的对象,防止内存泄露。
3)每次创建对象时,跟JDK ThreadLocal比都会相应的增加了很多索引下标,这在创建海量的FastThreadLocal对象时,数组占用的空间也不可小觑,所以这是Netty高性能之处:空间换时间的做法。

hread中的ThreadLocalMap存储ThreadLocal,ThreadLocalMap内部使用ThreadLocalMap.Entry数组存储每一个ThreadLocal,存储计算和HashMap类似,要计算key的索引位置=key.threadLocalHashCode&(len-1),中间可能需要计算冲突,使用的是线程探测方法(当前索引在被占用下,使用下一个索引)。达到一定条件后,还需扩充数组长度,rehash,效率不是太高。另外,还需要使用者注意内

 JDK中的ThreadLocalMap中的数组存放的是Entrty,key为ThreadLocal,value为真正保存的变量,而InternalThreadLocalMap中的数组里面的数据就是真正保存的变量值。JDK的ThreadLocal中将threadLocalHashCode放在了ThreadLocal类中,然后再拿这个值通过计算去定位ThreadLocalMap中的槽,而Netty的FastThreadLocal直接直接保存index,该index是通过InternalThreadLocalMap的静态方法获取的,该值是一个递增的值,也就是说不同的线程,在同一个FastThreadLocal中保存的变量值,在其实例变量InternalThreadLocalMap中的槽都是一样的。

Netty的FastThreadLocal解析

Netty 高性能之道 FastThreadLocal 源码分析(快且安全)

代码@1,获取当前线程。

代码@2,从当前线程获取ThreadLocalMap,

ThreadLocalMap getMap(Thread t) {

        return t.threadLocals;

    },这里是直接返回线程对象的threadLocals变量,有点意思吧,所以说ThreadLocal,是线程的本地变量,就是这层意思,真正存放数据的地方,就是线程对象本身,其实接下来的会更加有意思:我们进入ThreadLocalMap源码分析,得知,原来ThreadLocalMap就是一个Map结构(K-V)键值对,关于里面的源码就不一一分析了,ThreadLocalMap(ThreadLocal firstKey, Object firstValue),firstKey 为ThreadLocal,神奇吧,其实这也是为什么Thread的本地变量的数据类型为Map的原型,一个线程可以被多个ThreadLocal关联,每声明一个,就在线程的threadLocals增加为一个键值对,key 为 ThreadLocal,而value为具体存放的对象。

代码@3,如果线程的ThreadLocalMap不为空,则直接返回对,否则进入到代码@4

代码@4,初始化并获取放入ThreadLocal中的变量。

上面就是ThreadLocal的核心设计理念,为了更加直观的说明ThreadLocal原理,举例说明:

-----------------------------------------------------------------------
public class ThreadLocalDemo1 {
   private static final ThreadLocal<String> schemaLocal 
          = new ThreadLocal<String>();
 
    public void test1() {
        String a = schemaLocal.get();
 
        ThreadLocalDemo2 demo2 = new ThreadLocalDemo2();
        demo2.test(a);
    }
 
    public static void main(String[] args) {
        // TODO Auto-generated method stub
 
    }
 
}
 
public class ThreadLocalDemo2 {
 
  private static final ThreadLocal<String> slocal = new ThreadLocal<String>();
 
    public void test(String b) {
        String a = slocal.get();
        // 其他代码
        System.out.println(b);
    }
 
    public static void main(String[] args) {
        // TODO Auto-generated method stub
    }
 
}
 
public class TestMain {
 
    public static void main(String[] args) {
        // TODO Auto-generated method stub
 
        ThreadLocalDemo1 d = new ThreadLocalDemo1();
        d.test1();
 
    }
 
}

/一个线程调用 ThreadLocalDemo1 的 test1方法,在这个执行链中会涉及到两个ThreadLocal变量,调用ThreadLocal的get方法,首先会获取当前线程,然后从当前线程对象中获取线程内部属性[ThreadLocal.ThreadLocalMap threadLocals = null;],然后从ThreadLocalMap中以ThreadLocal对象为键,从threadLocals map中获取存放的值。

线程的threadLocals值为

{

     ThreadLocalDemo1.schemaLocal  : 该变量中的值,

     ThreadLocalDemo2.scloal : 存放在本线程中的值

   }

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

3、 ThreadLocal优化思考

    ThreadLocal的数据访问算法,本质上就是Map的访问特性。

 我在分析HashMap源码的时候,已经将HashMap的存储结构讲解完毕,如有兴趣,可以浏览一下我的博文:深入理解HashMap:http://blog.csdn.net/prestigeding/article/details/52861420,HashMap根据key的访问速度效率是很快的,为什么呢?因为HashMap根据key的hash,然后会定位到内部的数据槽(该数据是数组结构),众所周知,根据数组的下标访问,访问速度是最快的,也就是说HashMap根据key的定位速度比LinkedList等都快,仅次于数组访问方式,这是因为HashMap多了一步Hash定位槽的过程(当然,如果有Hash冲突那就更慢了)。所以,如果在高并发场景下,需要进一步优化ThreadLocal的访问性能,那就要从线程对象(Thread的threadLocals 数据结构下手了,如果能将数据结构修改为数组,然后每个ThreadLocal对象维护其下标那就完美了)。是的,Netty框架就是为了高并发而生的,由于并发访问的数量很大,一点点的性能优化,就会带来可观的性能提升效应,Netty主要从如下两个方面对ThreadLocal的实现进行优化

1)线程对象直接提供 set、get方法,以便直接获取线程本地存储相关的变量属性。

2)将数据存储基于数组存储。

 

4、Netty关于ThreadLocal机制的优化

由于ThreadLocal是JDK的原生实现,通用性很强,直接扩展进行定制化不是明智的选择,故Netty在优化ThreadLocal的方式是自己另起灶炉,实现ThreadLocal的语义。优化方法如下:

1)提供一个接口,FastThreadLocalAccess,并对线程池工厂类进行定制,创建的线程继承在java.lang.Thread类,并实现FastThreadLocalAccess接口,提供直接设置,获取线程本地变量的方法。

2)提供FastThreadLocal类,此类实现ThreadLocal相同的语义。

3)提供InternalThreadLocalMap类,此类作用类同于java.lang.ThreadLocal.ThreadLocalMap类,用于线程存放真实数据的结构。

4.1 扩展线程对象,提供set,get方法

     通过定制的线程池工厂,创建的线程对象为扩展后的线程对象,在Netty中对应为FastThreadLocalThread,该类本身很简单,值得大家注意的是其思想,jdk并发包中提供的线程池实现机制中,提供了线程创建的工厂的扩展点,这里就是其典型的实践。

     这里附上其源码,不做解读:

public class FastThreadLocalThread extends Thread implements FastThreadLocalAccess {
 
    private InternalThreadLocalMap threadLocalMap;
 
    public FastThreadLocalThread() { }
 
    public FastThreadLocalThread(Runnable target) {
        super(target);
    }
 
    public FastThreadLocalThread(ThreadGroup group, Runnable target) {
        super(group, target);
    }
 
    public FastThreadLocalThread(String name) {
        super(name);
    }
 
    public FastThreadLocalThread(ThreadGroup group, String name) {
        super(group, name);
    }
 
    public FastThreadLocalThread(Runnable target, String name) {
        super(target, name);
    }
 
    public FastThreadLocalThread(ThreadGroup group, Runnable target, String name) {
        super(group, target, name);
    }
 
    public FastThreadLocalThread(ThreadGroup group, Runnable target, String name, long stackSize) {
        super(group, target, name, stackSize);
    }
 
    /**
     * Returns the internal data structure that keeps the thread-local variables bound to this thread.
     * Note that this method is for internal use only, and thus is subject to change at any time.
     */
    @Override
    public final InternalThreadLocalMap threadLocalMap() {
        return threadLocalMap;
    }
 
    /**
     * Sets the internal data structure that keeps the thread-local variables bound to this thread.
     * Note that this method is for internal use only, and thus is subject to change at any time.
     */
    @Override
    public final void setThreadLocalMap(InternalThreadLocalMap threadLocalMap) {
        this.threadLocalMap = threadLocalMap;
    }
}

4.2 FastThreadLocal与InternalThreadLocalMap

InternalThreadLocalMap是线程存储本地变量的数据结构,每个线程拥有自己的InternalThreadLocalMap,其作用与java.lang.ThreadLocal.ThreadLocalMap内部类一样,而FastThreadLocal,其语义与ThreadLocal一样,对外表现与ThreadLocal一样。再次重复一下,Netty的InternalThreadLocalMap内部为数组,为什么是数组呢?线程本地变量,要从线程的执行流的角度看,一个线程在执行过程中,会经过多个类,会有多个类中声明有线程本地变量(参考上文说明ThreadLocal时候的举例),所以此处的数组就是保留线程在整个线程的执行过程中,不同的ThreadLocal变量中保存不同的数据,java.lang.ThreadLocal.ThreadLocalMap内部类的实现使用map结构,键为 ThreadLocal对象,而值为真正保存的变量值,InternalThreadLocalMap既然是数组,数组是一维的,数组最终肯定只能保存 真正要保持的变量值,那怎么区分不同的ThreadLocal在InternalThreadLocalMap中的下标呢?Netty采用的方式是将下标保存在FastThreadLocal中,我们知道,一般使用本地线程变量,FastThreadLocal的声明方式,一般是类变量(静态变量),诸如:private static final ThreadLocal aThreadLocal = new ThreadLocal();整个系统ThreadLocal的个数其实不会很多,每个FastThreadLocal在InternalThreadLocalMap的下标(偏移量)在FastThreadLocal加载时候确定,并保持不变。并且每个InternalThreadLocal内部数组的第一元素,存放系统运行中的FastThreadLocal对象。InternalThreadLocalMap的内部数据结构为:

/** Used by {@link FastThreadLocal} */

Object[] indexedVariables;

现在举例说明上述理论,

比如整个项目,有A,B,C,D,E5个类中各声明了一个静态的FastThreadLocal变量,类的加载顺序为

  A , B  , D, E, C

那们 类A中的FastThreadLocal存放在线程变量InternalThreadLocalMap的下标为1,B,为2,D为3,依次内推,

比如线程 T1,在一次请求过程中,需要用的A,E,C三个类中的FastThreadLocalMap,那么线程T1,的InternalThreadLocalMap的水库中的下标为1为A,下标为2,3的元素为空,下标为4为E,下标5存放C中的变量值。

每个线程InternalThreadLocalMap下标为0的是一Set集合,存放的是系统运行中有效的FastThreadLocal变量。

根据这样的存放后,FastThreadLocal 的get,set方法,都是根据下标直接在InternalThreadLocalMap的数组中直接存储,当然,值得一提的是InternalThreadLocalMap中数组元素长度默认为32,如果系统的FastThreadLocal的数量超过32个的话,会成倍扩容。

FastThreadLocal学习的入口,建议从set,get方法入手即可,知道上述原理后,源码的阅读应该比较容易,就不做过多讲解了。

http://blog.csdn.net/zero__007/article/details/78288448中简单介绍了一下ThreadLocal,每个ThreadLocal实例都有一个唯一的threadLocalHashCode初始值,在ThreadLocalMap中设置或获取Entry时,会根据threadLocalHashCode&(len-1)的值去对应的槽中操作。
      而ThreadLocal解决Hash 冲突使用线性探测的方法,当一个线程对应多个ThreadLocal实例的场景中,在命中的情况下基本上一次hash就可以找到位置,如果发生没有命中的情况,则会引发性能会急剧下降,本身是O(1),结果变成了O(n)当在读写操作频繁的场景,这点导致性能的后滞。

      作为一个高并发框架,Netty对ThreadLocal作了一些优化,并提供一个性能更好的FastThreadLocal。

public class FastThreadLocalThread extends Thread {
private InternalThreadLocalMap threadLocalMap;
public FastThreadLocalThread() { }
public FastThreadLocalThread(Runnable target) {
super(target);

}

public FastThreadLocalThread(ThreadGroup group, Runnable target) {
super(group, target);
}

public FastThreadLocalThread(String name) {
super(name);
}

public FastThreadLocalThread(ThreadGroup group, String name) {
super(group, name);
}


public FastThreadLocalThread(Runnable target, String name) {
super(target, name);
}


public FastThreadLocalThread(ThreadGroup group, Runnable target, String name) {
super(group, target, name);
}


public FastThreadLocalThread(ThreadGroup group, Runnable target, String name, long stackSize) {
super(group, target, name, stackSize);
}


/**
* Returns the internal data structure that keeps the thread-local variables bound to this thread.
* Note that this method is for internal use only, and thus is subject to change at any time.
*/

public final InternalThreadLocalMap threadLocalMap() {
return threadLocalMap;
}


/**
* Sets the internal data structure that keeps the thread-local variables bound to this thread.
* Note that this method is for internal use only, and thus is subject to change at any time.
*/

public final void setThreadLocalMap(InternalThreadLocalMap threadLocalMap) {
this.threadLocalMap = threadLocalMap;
}

}

      Netty专门提供一个FastThreadLocalThread,继承了JDK的Thread,内部也有个InternalThreadLocalMap实例变量,并暴露了这个变量的getter/setter方法。

      JDK中的ThreadLocalMap中的数组存放的是Entrty,key为ThreadLocal,value为真正保存的变量,而InternalThreadLocalMap中的数组里面的数据就是真正保存的变量值。

 

 
  1. public class FastThreadLocal<V> {

  2.  
  3. private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();

  4.  
  5. private final int index;

  6.  
  7. public FastThreadLocal() {

  8. index = InternalThreadLocalMap.nextVariableIndex();

  9. }

  10.  
  11. //……

  12. }

      JDK的ThreadLocal中将threadLocalHashCode放在了ThreadLocal类中,然后再拿这个值通过计算去定位ThreadLocalMap中的槽,而Netty的FastThreadLocal直接直接保存index,该index是通过InternalThreadLocalMap的静态方法获取的,该值是一个递增的值,也就是说不同的线程,在同一个FastThreadLocal中保存的变量值,在其实例变量InternalThreadLocalMap中的槽都是一样的。

 
  1. public class Test {

  2. static FastThreadLocal<String> threadLocal = new FastThreadLocal<String>(){

  3. @Override

  4. protected String initialValue() throws Exception {

  5. return "zero";

  6. }

  7. };

  8.  
  9. public static void main(String[] args) {

  10. FastThreadLocalThread thread0 = new FastThreadLocalThread(new Runnable() {

  11. @Override

  12. public void run() {

  13. System.out.println(threadLocal.get());

  14. }

  15. });

  16. FastThreadLocalThread thread1 = new FastThreadLocalThread(new Runnable() {

  17. @Override

  18. public void run() {

  19. System.out.println(threadLocal.get());

  20. }

  21. });

  22. thread0.start();

  23. thread1.start();

  24. }

  25. }

      上面的例子,线程私有的String变量,在线程的实例变量threadLocalMap里的数组,存放的位置下标都是1。由于InternalThreadLocal的index会递增,因此InternalThreadLocal实例再多的话也只会扩容,而不会发生Hash冲突。
      再提一下FastThreadLocal中variablesToRemoveIndex,该变量是static final修饰的,其值就是0,在InternalThreadLocalMap中会占index=0的槽。实际上该下标的元素是一个包装了IndentityHashMap的Set,每次FastThreadLocal中设置值的时候将自己加到该Set,移除值的时候将自己(FastThreadLocal实例)从该Set移除。也就是说初始值、设置值、删除值这几个功能的耗时比有值时的get更多。
      关于ThreadLocal与FastThreadLocal的性能对比数据,可以看下http://www.bubuko.com/infodetail-1932175.html --------------------- 本文来自 zero__007 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/zero__007/article/details/79106212?utm_source=copy

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值