文档章节

为什么Netty的FastThreadLocal速度快

ksfzhaohui
 ksfzhaohui
发布于 10/14 19:30
字数 1994
阅读 833
收藏 14

前言

最近在看netty源码的时候发现了一个叫FastThreadLocal的类,jdk本身自带了ThreadLocal类,所以可以大致想到此类比jdk自带的类速度更快,主要快在什么地方,以及为什么速度更快,下面做一个简单的分析;

性能测试

ThreadLocal主要被用在多线程环境下,方便的获取当前线程的数据,使用者无需关心多线程问题,方便使用;为了能说明问题,分别对两个场景进行测试,分别是:多个线程操作同一个ThreadLocal,单线程下的多个ThreadLocal,下面分别测试:

1.多个线程操作同一个ThreadLocal

分别对ThreadLocal和FastThreadLocal使用测试代码,部分代码如下:

public static void test2() throws Exception {
        CountDownLatch cdl = new CountDownLatch(10000);
        ThreadLocal<String> threadLocal = new ThreadLocal<String>();
        long starTime = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            new Thread(new Runnable() {

                @Override
                public void run() {
                    threadLocal.set(Thread.currentThread().getName());
                    for (int k = 0; k < 100000; k++) {
                        threadLocal.get();
                    }
                    cdl.countDown();
                }
            }, "Thread" + (i + 1)).start();
        }
        cdl.await();
        System.out.println(System.currentTimeMillis() - starTime + "ms");
    }

以上代码创建了10000个线程,同时往ThreadLocal设置,然后get十万次,然后通过CountDownLatch来计算总的时间消耗,运行结果为:1000ms左右
下面再对FastThreadLocal进行测试,代码类似:

public static void test2() throws Exception {
        CountDownLatch cdl = new CountDownLatch(10000);
        FastThreadLocal<String> threadLocal = new FastThreadLocal<String>();
        long starTime = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            new FastThreadLocalThread(new Runnable() {

                @Override
                public void run() {
                    threadLocal.set(Thread.currentThread().getName());
                    for (int k = 0; k < 100000; k++) {
                        threadLocal.get();
                    }
                    cdl.countDown();
                }
            }, "Thread" + (i + 1)).start();
        }

        cdl.await();
        System.out.println(System.currentTimeMillis() - starTime);
    }

运行之后结果为:1000ms左右;可以发现在这种情况下两种类型的ThreadLocal在性能上并没有什么差距,下面对第二种情况进行测试;

2.单线程下的多个ThreadLocal

分别对ThreadLocal和FastThreadLocal使用测试代码,部分代码如下:

    public static void test1() throws InterruptedException {
        int size = 10000;
        ThreadLocal<String> tls[] = new ThreadLocal[size];
        for (int i = 0; i < size; i++) {
            tls[i] = new ThreadLocal<String>();
        }
        
        new Thread(new Runnable() {
            @Override
            public void run() {
                long starTime = System.currentTimeMillis();
                for (int i = 0; i < size; i++) {
                    tls[i].set("value" + i);
                }
                for (int i = 0; i < size; i++) {
                    for (int k = 0; k < 100000; k++) {
                        tls[i].get();
                    }
                }
                System.out.println(System.currentTimeMillis() - starTime + "ms");
            }
        }).start();
    }

以上代码创建了10000个ThreadLocal,然后使用同一个线程对ThreadLocal设值,同时get十万次,运行结果:2000ms左右;
下面再对FastThreadLocal进行测试,代码类似:

    public static void test1() {
        int size = 10000;
        FastThreadLocal<String> tls[] = new FastThreadLocal[size];
        for (int i = 0; i < size; i++) {
            tls[i] = new FastThreadLocal<String>();
        }
        
        new FastThreadLocalThread(new Runnable() {

            @Override
            public void run() {
                long starTime = System.currentTimeMillis();
                for (int i = 0; i < size; i++) {
                    tls[i].set("value" + i);
                }
                for (int i = 0; i < size; i++) {
                    for (int k = 0; k < 100000; k++) {
                        tls[i].get();
                    }
                }
                System.out.println(System.currentTimeMillis() - starTime + "ms");
            }
        }).start();
    }

运行结果:30ms左右;可以发现性能达到两个数量级的差距,当然这是在大量访问次数的情况下才有的效果;下面重点分析一下ThreadLocal的机制,以及FastThreadLocal为什么比ThreadLocal更快;

ThreadLocal的机制

因为我们常用的就是set和get方法,分别看一下对应的源码:

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

以上代码大致意思:首先获取当前线程,然后获取当前线程中存储的threadLocals变量,此变量其实就是ThreadLocalMap,最后看此ThreadLocalMap是否为空,为空就创建一个新的Map,不为空则以当前的ThreadLocal为key,存储当前value;可以进一步看一下ThreadLocalMap中的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();
        }

大致意思:ThreadLocalMap内部使用一个数组来保存数据,类似HashMap;每个ThreadLocal在初始化的时候会分配一个threadLocalHashCode,然后和数组的长度进行取模操作,所以就会出现hash冲突的情况,在HashMap中处理冲突是使用数组+链表的方式,而在ThreadLocalMap中,可以看到直接使用nextIndex,进行遍历操作,明显性能更差;下面再看一下get方法:

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

同样是先获取当前线程,然后获取当前线程中的ThreadLocalMap,然后以当前的ThreadLocal为key,到ThreadLocalMap中获取value:

        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);
        }
        
         private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

同set方式,通过取模获取数组下标,如果没有冲突直接返回数据,否则同样出现遍历的情况;所以通过分析可以大致知道以下几个问题:
1.ThreadLocalMap是存放在Thread下面的,ThreadLocal作为key,所以多个线程操作同一个ThreadLocal其实就是在每个线程的ThreadLocalMap中插入的一条记录,不存在任何冲突问题;
2.ThreadLocalMap在解决冲突时,通过遍历的方式,非常影响性能;
3.FastThreadLocal通过其他方式解决冲突的问题,达到性能的优化;
下面继续来看一下FastThreadLocal是通过何种方式达到性能的优化。

为什么Netty的FastThreadLocal速度快

Netty中分别提供了FastThreadLocal和FastThreadLocalThread两个类,FastThreadLocalThread继承于Thread,下面同样对常用的set和get方法来进行源码分析:

   public final void set(V value) {
        if (value != InternalThreadLocalMap.UNSET) {
            set(InternalThreadLocalMap.get(), value);
        } else {
            remove();
        }
    }

    public final void set(InternalThreadLocalMap threadLocalMap, V value) {
        if (value != InternalThreadLocalMap.UNSET) {
            if (threadLocalMap.setIndexedVariable(index, value)) {
                addToVariablesToRemove(threadLocalMap, this);
            }
        } else {
            remove(threadLocalMap);
        }
    }

此处首先对value进行判定是否为InternalThreadLocalMap.UNSET,然后同样使用了一个InternalThreadLocalMap用来存放数据:

    public static InternalThreadLocalMap get() {
        Thread thread = Thread.currentThread();
        if (thread instanceof FastThreadLocalThread) {
            return fastGet((FastThreadLocalThread) thread);
        } else {
            return slowGet();
        }
    }

    private static InternalThreadLocalMap fastGet(FastThreadLocalThread thread) {
        InternalThreadLocalMap threadLocalMap = thread.threadLocalMap();
        if (threadLocalMap == null) {
            thread.setThreadLocalMap(threadLocalMap = new InternalThreadLocalMap());
        }
        return threadLocalMap;
    }

可以发现InternalThreadLocalMap同样存放在FastThreadLocalThread中,不同在于,不是使用ThreadLocal对应的hash值取模获取位置,而是直接使用FastThreadLocal的index属性,index在实例化时被初始化:

    private final int index;

    public FastThreadLocal() {
        index = InternalThreadLocalMap.nextVariableIndex();
    }

再进入nextVariableIndex方法中:

    static final AtomicInteger nextIndex = new AtomicInteger();
     
    public static int nextVariableIndex() {
        int index = nextIndex.getAndIncrement();
        if (index < 0) {
            nextIndex.decrementAndGet();
            throw new IllegalStateException("too many thread-local indexed variables");
        }
        return index;
    }

在InternalThreadLocalMap中存在一个静态的nextIndex对象,用来生成数组下标,因为是静态的,所以每个FastThreadLocal生成的index是连续的,再看一下InternalThreadLocalMap中是如何setIndexedVariable的:

    public boolean setIndexedVariable(int index, Object value) {
        Object[] lookup = indexedVariables;
        if (index < lookup.length) {
            Object oldValue = lookup[index];
            lookup[index] = value;
            return oldValue == UNSET;
        } else {
            expandIndexedVariableTableAndSet(index, value);
            return true;
        }
    }

indexedVariables是一个对象数组,用来存放value;直接使用index作为数组下标进行存放;如果index大于数组长度,进行扩容;get方法直接通过FastThreadLocal中的index进行快速读取:

   public final V get(InternalThreadLocalMap threadLocalMap) {
        Object v = threadLocalMap.indexedVariable(index);
        if (v != InternalThreadLocalMap.UNSET) {
            return (V) v;
        }

        return initialize(threadLocalMap);
    }
    
    public Object indexedVariable(int index) {
        Object[] lookup = indexedVariables;
        return index < lookup.length? lookup[index] : UNSET;
    }

直接通过下标进行读取,速度非常快;但是这样会有一个问题,可能会造成空间的浪费;

总结

通过以上分析我们可以知道在有大量的ThreadLocal进行读写操作的时候,才可能会遇到性能问题;另外FastThreadLocal通过空间换取时间的方式来达到O(1)读取数据;还有一个疑问就是内部为什么不直接使用HashMap(数组+黑红树)来代替ThreadLocalMap。

© 著作权归作者所有

ksfzhaohui

ksfzhaohui

粉丝 401
博文 157
码字总数 232747
作品 3
南京
高级程序员
私信 提问
惊:FastThreadLocal吞吐量居然是ThreadLocal的3倍!!!

说明 接着上次手撕面试题ThreadLocal!!!面试官一听,哎呦不错哦!本文将继续上文的话题,来聊聊FastThreadLocal,目前关于FastThreadLocal的很多文章都有点老有点过时了(本文将澄清几个误...

匠心零度
07/02
0
0
Netty精粹之设计更快的ThreadLocal

Netty是一款优秀的开源的NIO框架,其异步的、基于IO事件驱动的设计以及简易使用的API使得用户快速构建基于NIO的高性能高可靠性的网络服务器成为可能。Netty除了使用Reactor设计模式加上精心设...

Float_Luuu
2016/02/09
5K
2
Netty源码阅读入门实战(十)-性能优化

1 性能优化工具类 FastThreadLocal 传统的ThreadLocal ThreadLocal最常用的两个接口是set和get 最常见的应用场景为在线程上下文之间传递信息,使得用户不受复杂代码逻辑的影响 我们使用set的...

芥末无疆sss
2018/10/22
0
0
使用netty,创建线程池的另外一种选择

之所以称之为 Fast,因为没有使用 JDK 的使用线性探测法的 Map,如果你使用的是Netty 线程池工厂创建的线程,搭配 Netty 的 ftl,性能非常好,如果你使用自定义的线程,搭配 ftl,性能也会比...

暗中观察
07/06
112
0
Redisson 3.11.4 发布,Redis 客户端

Redisson 3.11.4 发布了,Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格 (In-Memory Data Grid) 。 Redisson 在基于 NIO 的 Netty 框架上,充分利用了 Redis 键值数据库提供的一...

xplanet
09/26
2.6K
2

没有更多内容

加载失败,请刷新页面

加载更多

OSChina 周五乱弹 —— 你已经是个成熟的熊猫了

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @Sharon啊 :#今日歌曲推荐# 分享黑鸭子的单曲《羞答答的玫瑰静悄悄的开》 《羞答答的玫瑰静悄悄的开》- 黑鸭子 手机党少年们想听歌,请使劲儿...

小小编辑
30分钟前
120
5
结合Spring Security进行web应用会话安全管理

在本文中,将为大家说明如何结合Spring Security 和Spring Session管理web应用的会话。 一、Spring Security创建使用session的方法 Spring Security提供4种方式精确的控制会话的创建: alwa...

fightinging
35分钟前
3
0
83、Mybatis和Hibernate重要区别

Mybatis;入门简单,程序容易上手开发,节省开发成本。Mybatis需要程序猿自己编写sql语句,是一个不完全的ORM框架,对sql修改和优化非常容易实现。 Mybatis适合开发需求变更频繁的系统,比如...

lianbang_W
今天
5
0
设计模式之状态模式

定义 Allow an object to alter its behavior when its internal state changes.The object will appear to change its class.(当一个对象内在状态改变时允许其改变行为,这个对象看起来像改...

陈年之后是青葱
今天
6
0
Python常用模块之os.path

os.path.abspath(path) 输入相对路径,返回绝对路径 Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 27 2018, 04:59:51) [MSC v.1914 64 bit (AMD64)] on win32Type "copyright", "credits" or "lic......

松鼠大帝
今天
11
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部