ThreadLocal解析

1、ThreadLocal起到数据隔离的作用,相当于为每个线程提供一个变量副本,变量数据对别的线程而言是相对隔离的。在保存多线程环境下,防止自己的变量被其他线程纂改。

使用方式:线程中初始化一个ThreadLocal对象,后续只要改线程在remove之前调用get()方法取值即可。

ThreadLocal<String> localName = new ThreadLocal();
localName.set("Alice");
String name = localName.get();
localName.remove();

从以上的使用可以看出,ThreadLocal的使用主要涉及的接口:

void set(Object value)设置当前线程的局部变量值;

Object get() 返回当前线程所对应的局部变量值;

void remove() 删除当前线程局部变量的值,为了减少内存的占用。注意:当线程结束后,该线程的布局变量将自动被垃圾回收,所以显示调用该方法清楚并不是必须的,只是可以加快内存的回收速度。

2、常见的场景:
其中Spring中的事务就是采用ThreadLocal方式实现。Spring采用ThreadLocal的方式,来保存单个线程中的数据库操作使用的是同一个数据库连接。同时采用这种方式可以使业务层使用事务时不需要感知并管理Connection对象,通过传播级别巧妙的管理多个事务配置之间的切换、挂起和恢复。

在Android中,Looper类就是利用了ThreadLocal的特性,保证每个线程只存在一个Looper对象。

Looper的源码如下:

// sThreadLocal.get() will return null unless you've called prepare().
@UnsupportedAppUsage
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
}

3、接下来看下 ThreadLocal的源码解析

Thread类中有一个ThreadLocal.ThreadLocalMap 类型的属性变量 threadLocals

ThreadLocal.ThreadLocalMap threadLocals = null; 而ThreadLocal的静态内部类ThreadLocalMap类 中是由 Entry 类型数组table保存每一个数值,

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

其中, Entry 是包装成ThreadLocal<?>的弱引用static class Entry extends WeakReference<ThreadLocal<?>>

下面我们看下ThreadLocal类的 set(T value)方法源码

 public void set(T value) {
        Thread t = Thread.currentThread();//获取当前线程
        ThreadLocalMap map = getMap(t);//获取ThreadLocalMap对象
        if (map != null)
            map.set(this, value);//设置值
        else
            createMap(t, value);//创建Map对象
    }

以上set源码很简单明了,就是先获取当前现在的ThreadLocalMap对象,若存在直接设置,若不存在先创建Map对象,再设置。

然后我们先看下新建ThreadLocal类的 createMap(t, value);方法创建Map对象的源码,其中,this是当前的ThreadLocal对象

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

重点是ThreadLocalMap的构造方法源码:

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY]; //新建Entry数组,初始大小是INITIAL_CAPACITY = 16
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);// 根据斐波拉契散列获取下标值
            table[i] = new Entry(firstKey, firstValue); //构建Entry对象,放置数组中
            size = 1;
            setThreshold(INITIAL_CAPACITY); //设置扩容因子
        }

其中
// 斐波拉契散列魔数
private final int threadLocalHashCode = nextHashCode();

/**
 * The next hash code to be given out. Updated atomically. Starts at
 * zero.
 */
private static AtomicInteger nextHashCode = new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;// 魔数

/**
 * Returns the next hash code.
 */
private static int nextHashCode() {
   return nextHashCode.getAndAdd(HASH_INCREMENT); // 通过 AtomicInteger 类实现原子递增
}

再来看下 ThreadLocalMap 对象的set方法

 /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be 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); // 通过斐波拉契散列获取线程应该所在的索引下标



            // 获取数组中该下标的值,判断是否已经存在元素
            // nextIndex:获取下一个下标,通过三元表达式实现首尾相连

            // 如果Entry为空,说明当前 TheadLocal 实例不存在,同时也不可能存在在其他索引位置,分析如下:
            // 首先GC回收失效只对ThreadLocal进行清理,不会对Entry进行清理,所以Entry只会为空,不会为null
            // 此时如果当前 ThreadLocal 在此Entry有效时插入,则会顺序寻找下一个槽位。
            // 此时需要做的就是从其他槽位找到当前ThreadLocal,并把当前 ThreadLocal 拉回到正确槽位
    
            // 其次,如果当前线程在插入 ThreadLocal 时所在槽位已经被占用,此时该 ThreadLocal 占用其他槽位,
            // 之后如果占用该槽位的 ThreadLocal 被程序清理,注意程序清理会直接将Entry置null
            // 清理完成后,程序会重新向下整理一次元素的下标位置,尽量将元素放在hash算出的hash位置

            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
            
                ThreadLocal<?> k = e.get();
                
                //如果位置i不为空,这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;
                if (k == key) {
                    e.value = value;
                    return;
                }
                //如果当前位置是空的,就初始化一个Entry对象放在位置i上
                if (k == null) {
                    replaceStaleEntry(key, value, i);// 清理并存储当前节点
                    return;
                }
                //如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
            }
            
            // 如果当前下标未被占用,则直接初始化为当前 ThreadLocal
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

再看下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) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();// map为空或者entry为空,构建并填充初值
    }
    
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

ThreadLocalMap的getEntry(ThreadLocal<?> key)源码:从ThreadLocalMap中获取Entry对象

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); //key不为当前ThreadLocal,则说明已经被占用了,向后继续寻找
        }
        
        
  private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            
            // e不为空,说明槽点被占用,继续向后寻找
            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;
        }

ThreadLocal的remove源码

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);//map存在移除当前的ThreadLocal
     }

再看下ThreadLocalMap的remove

private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            // 从下标位置开始,向下寻找,获取指定的 ThreadLocal后直接移除
            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

类之间的关系


ThreadLocal的类图.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容