• 聊聊ThreadLocal


          首先说说与ThreadLocal相识的背景。在项目中,service有一些逻辑处理{如对主从延迟敏感的下单逻辑}需要强制走主库的。就查了查公司数据库框架zebra强制走主库的方式,发现其主要就是在业务线程的context中写入了一个ThreadLocal<Boolean>变量,当要进行SQL路由时,根据此变量来判断是否需要强制走主库。

    首先,如上图所示,Java内存模型JMM如上图所示,每个Java通过自己的缓存与主内存相连接。当需要读写某对象或变量时,需要到主内存copy到线程内,操作完再写回主内存。那么,这些非原子性的操作就很容易引起数据不一致的问题。ThreadLocal就是用解决此问题的,顾名思义,ThreadLocal就是线程局部变量的意思,其使用方式与调用普通POJO数据对象无异,即使用get/set来进行赋值或读取。然而其在存储上,却隐含了线程私有存储之意,线程之间相互隔离,互不影响,从而实现线程安全。先来个使用样例,直接给出zebra的强制走主库的工具类实现:

    public class LocalContextReadWriteStrategy implements ReadWriteStrategy {
    
    	private static InheritableThreadLocal<Boolean> forceMaster = new InheritableThreadLocal<Boolean>();
    
    	@Override
    	public boolean shouldReadFromMaster() {
    		Boolean shouldReadFromMaster = forceMaster.get();
    
    		if (shouldReadFromMaster == null || !shouldReadFromMaster) {
    			return false;
    		} else {
    			return true;
    		}
    	}
    
    	protected static void setReadFromMaster() {
    		forceMaster.set(true);
    	}
    
    	protected static void clearContext() {
    		forceMaster.remove();
    	}
    
    	@Override
    	public void setGroupDataSourceConfig(GroupDataSourceConfig config) {
    	}
    }
    

    如上,通过一个ThreadLocal<Boolean>型的线程局部变量forceMaster,通过set()来为其赋值,get()来取值,remove()来实现clear。接下来就来看看这三个方法的实现。ThreadLocal.set()方法实现如下:

        public void set(T value) {
            Thread t = Thread.currentThread();  // 获取当前线程
            ThreadLocalMap map = getMap(t);     // 获取线程内部藏着的map
            if (map != null)
                map.set(this, value);           // 以ThreadLocal对象自身为key,将value存放到map中
            else
                createMap(t, value);
        }
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    

     从上面的set方法可以看出:

    1. Thread内部有了隐藏的ThreadLocalMap负责存放该线程的ThreadLocal变量,这个是ThreadLocal自定义的map,与HashMap有较大的区别,后续再讨论

    2. 第一次调用set的时候,才会创建并初始化这个ThreadLocalMap

    3. ThreadLocalMap是以ThreadLocal对象自身作为key的

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

    ThreadLocal的remove方法如下:

         public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
         }
    

    其实,通过上面的几个方法,已经可以看出来了,对于ThreadLocal变量的存取,关键就在于这个ThreadLocalMap了。set时无非是以ThreadLocal对象自身为key将value存放到map中,而get时,就是以ThreadLocal作为key去map中查找。接下来,就重点瞧瞧这个Thread内部偷偷藏着的自定义map--ThreadLocalMap。

    ThreadLocalMap是定义在ThreadLocal中的一个静态类,主要作为装载线程局部变量的容器。先来看看其map定义的节点Entry:

            static class Entry extends WeakReference<ThreadLocal> {
                /** The value associated with this ThreadLocal. */
                Object value;
    
                Entry(ThreadLocal k, Object v) {
                    super(k);
                    value = v;
                }
            }
    

    很有意思,Entry中只定义了一个value,没再定义key,因为key就是ThreadLocal自身。entry.get()拿到key,即threadLocal,entry.value拿到value。此外,通过super(key),将entry对key的引用定义为了弱引用。这里就要回忆下Java中的强、软、弱、虚四种引用了。仔细思量下就可以理解了,Entry对于Key[即ThreadLocal对象背身]的引用被定义为虚引用,主要是为了能让ThreadLocal在除了Entry之外再无引用时,被顺利的回收掉。因为Thread对于map的引用,map经底层Entry数据对entry的引用都是强引用,这时候,要是entry对ThreadLocal的引用也是强引用的话,那这个ThreadLocal就无法被回收了,也就造成内存泄漏了。

    再来看看第一次调用ThreadLocal.set()时创建map的constructor:

            ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
                table = new Entry[INITIAL_CAPACITY];
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
                table[i] = new Entry(firstKey, firstValue);
                size = 1;
                setThreshold(INITIAL_CAPACITY);
            }
    

    这里面就涉及到ThreadLocal的hashcode的计算了:

    private static final int HASH_INCREMENT = 0x61c88647;
    
    private final int threadLocalHashCode = nextHashCode();
    
    private static AtomicInteger nextHashCode = new AtomicInteger();
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    

    上面的几行代码就已经完成了一个ThreadLocal对象的哈希值的计算了:

    1. hash值在thread对象创建时就已经计算好了,后续不再发生变化

    2. 以神奇的魔数 0x61c88647 作为递增量,来计算新ThreadLocal对象的hash值(这个魔数在各类SDK中经常被用到,原因是据此递增出来的hash值在2的N次方的长度的链表中分布非常均匀)

    再来看看ThreadLocalMap的set方法:

    private void set(ThreadLocal key, Object value) {
                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();
            }
    
            private static int nextIndex(int i, int len) {
                return ((i + 1 < len) ? i + 1 : 0);
            }  

    上面的set方法有几个有意思的地方:

    1. hash冲突,其使用了线性探测法而非HashMap中的链表法来解决hash冲突

    2. 判空,前面说了,Entry对于key的引用是个弱引用,因此当拿出来Entry之后发现key=null了,说明作为key的ThreadLocal已经被回收了,这个节点也就是个过时的节点了,需要被处理掉了

    3. 在Java1.3中,存放ThreadLocal的那个map,使用了HashMap来实现,后来为了性能,变更为自定义map 

    至此,已然有所得了~

  • 相关阅读:
    ModelViewViewModel 设计模式介绍
    Sliverlight 3 3D 游戏开发学习 第一章:光照、照相机与行为
    【翻译】WF从入门到精通(第十八章):在你的工作流中调用Web服务
    一个能设置验证信息样式的WebTextBox基本验证复合控件
    一个封装了在Ajax下弹出Alert、showModelessDialog、showModalDialog窗口的类
    【翻译】WF从入门到精通(第十一章):并行活动
    测试驱动开发入门
    【翻译】WF从入门到精通(第三章):workflow实例
    【翻译】WF从入门到精通(第九章):逻辑流活动
    关系型数据库与面向对象开发之间的矛盾:学习LINQ有感
  • 原文地址:https://www.cnblogs.com/dosmile/p/6505919.html
Copyright © 2020-2023  润新知