ThreadLocal的原理

参考:

1 简介

ThreadLocal是线程本地变量的一种实现方式;线程本地变量线程自己私有,不同线程的本地变量互不影响,不存在线程安全问题

 下面是ThreadLocal简单使用

//ThreadLocal简单使用
static ThreadLocal<Object> threadLocal = new ThreadLocal<> ();//必须设置为静态属性,避免无意义的多实例
threadLocal.set(obj);//为当前线程设置一个本地变量
Object obj2 = threadLocal.get();//获取threadLocal作为key对应的本地变量
threadLocal.set(obj2);//覆盖之前的Value obj
threadLocal.remove();//移除这个本地变量,防止内存泄漏

2 存储结构

 首先我们来聊一聊 ThreadLocal 在多线程运行时,各线程是如何存储变量的,假如我们现在定义两个 ThreadLocal 实例如下:

static ThreadLocal<User> threadLocal_1 = new ThreadLocal<>();
static ThreadLocal<Client> threadLocal_2 = new ThreadLocal<>();

 我们分别在三个线程中使用 ThreadLocal,伪代码如下:

// thread-1中
threadLocal_1.set(user_1);
threadLocal_2.set(client_1);
// thread-2中
threadLocal_1.set(user_2);
threadLocal_2.set(client_2);
// thread-3中
threadLocal_2 .set(client_3);

 这三个线程都在运行中,那此时各线程中的存数数据应该如下图所示:

 由上图看出Thread-1和Thread-1虽然使用相同的ThreadLocal引用作为Key,但是他们的Value互不影响。

 下图是ThreadLocal的存储结构:

每一个线程都有一个ThreadLocalMap类型的threadLocals属性;而ThreadLocalMap对象持有一个Entry数组的引用,每一个Entry存储一个键值对,Key是一个ThreadLocal的弱引用(实际上是ThreadLocal的hashcode),Value是Object对象,就是本地变量

3. 源码分析

3.1 ThreadLocalMap简介

 ThreadLocalMap 是ThreadLocal 的一个内部类,然而它并没有继承Map类,因为它只供ThreadLocal 内部使用,数据结构采用 数组 + 开方地址法;它的默认变长是16;

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

 Entry 是ThreadLocalMap 的内部类,继承自 WeakReference,所以Entry存储的key是一个ThreadLocal弱引用;所以只能自动回收弱引用的key,而强引用 value 的需要手动回收(用expungeStaleEntry()方法)。

3.2 ThreadLocalMap 之 key 的 hashCode

class ThreadLocal{
	//...
  //hashcode,实例化时执行
 	private final int threadLocalHashCode = nextHashCode();
  // AtomicInteger类型,从0开始
 	private static AtomicInteger nextHashCode = new AtomicInteger();
 	// hash code每次增加1640531527
 	private static final int HASH_INCREMENT = 0x61c88647; 
  //实例化时调用
 	private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
 }
}

 每生成一个ThreadLocal对象,新的hashcode就增加1640531527

3.3 set方法

3.3.1 ThreadLocal的set方法
public void set(T value) {
    Thread t = Thread.currentThread(); 
    ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap对象
    if (map != null) // 判断map是否存在
        map.set(this, value); // 调用 map 的 set 方法
    else
        createMap(t, value); // 创建map并插入<this,value>
}

 第一次调用set时map为空,需要creatMap并插入这个键值对实体,创建方式比较简单,不详解;这里重要的还是 ThreadLocalMap 的 set 方法。

3.3.2 ThreadLocalMap的set方法
 private void set(ThreadLocal<?> key, Object value) {

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); // 用key的hashCode计算索引位置
    // hash冲突时,使用开放定址法解决冲突
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();//当前位置的key
        if (k == key) { // 若当前key与传入key相同,则覆盖value
            e.value = value; 
            return;
        }
        if (k == null) { // key = null,说明当前Entry是个过期Entry(key为null的Entry)
          	//向后找下一个插入位置并清理过期的Entry
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 当前位置为空,生成一个Entry并插入该位
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些过期的entry,并判断是否需要扩容
        rehash(); // 扩容
}

 在set一个线程本地变量的过程中,先根据ThreadLocal对象的hash值,定位到Entry table中的位置i,使用开放定址法处理Hash冲突,过程如下:

  • 如果当前位置Entry为空,生成一个Entry并插入该位置;
  • 如果当前位置Entry不为空且当前key与传入的key相同,用新value覆盖旧value;
  • 如果当前位置Entry不为空但key不同,说明hash冲突,那么只能向后找下一个位置;

3.4 get方法

3.4.1 ThreadLocal 的get() 方法
public T get() {
    Thread t = Thread.currentThread();//获取当前线程
    ThreadLocalMap map = getMap(t);// 获取当前线程的 ThreadLocalMap对象
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); //调用map的getEntry方法
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;//获取本地变量
            return result;
        }
    }
    return setInitialValue(); //如果没有set过,返回本地变量默认值(可自定义)
}
3.4.2 ThreadLocalMap的getEntry方法
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);//获取key对应的索引位置
    Entry e = table[i];//当前位置的Entry
    if (e != null && e.get() == key) //若当前key与传入key相同,则找到目标Entry(无hash冲突情况)
        return e;
    else
        return getEntryAfterMiss(key, i, e); //若不同,查找下一个位置(有hash冲突情况),这个方法中有清除过期Entry的操作
}

 在get一个线程本地变量的过程中,先根据ThreadLocal对象的hash值,定位到Entry table中的位置i;

  • 若当前key与传入key相同,则找到目标Entry(无hash冲突情况);
  • 若不同,查找下一个位置(有hash冲突情况);

3.5 remove方法

3.5.1 ThreadLocal 之 remove() 方法
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());//获取当前线程的ThreadLocalMap对象
    if (m != null)
        m.remove(this); // 调用ThreadLocalMap的remove方法
}

 先获取当前线程的ThreadLocalMap对象,然后调用ThreadLocalMap的remove方法移除指定threadLocal键对应的Entry

3.5.2 ThreadLocalMap的remove方法
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    // 根据hashCode计算出当前ThreadLocal的索引位置
    int i = key.threadLocalHashCode & (len-1);  
    // 从位置i开始遍历,直到Entry为null
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {   // 如果找到相同的key
            e.clear(); 	// 调用clear方法, 先清空key
            expungeStaleEntry(i);//后调用expungeStaleEntry方法清理过期实体
            return;
        }
    }
}

 remove指定key本地变量的过程是一个查找清理的过程,先计算当前ThreadLocal作为key对应的索引位置i,从i开始往后遍历,如果找到key相同Entry,清理掉;

 remove过程也会调用expungeStaleEntry(i)方法清理过期Entity;

set、get、remove方法,都会调用expungeStaleEntry(i)方法清理过期Entry(key=null);

4 hash冲突

 当出现不同的key相同的hashcode时就会出现hash冲突;ThreadLocalMap处理冲突的方法是开放定址法,di使用的是线性探测

5 内存泄露及解决办法

 图中虚箭头代表弱引用,实箭头代表强引用;
  • 为什么?Thread的生命周期可能会比较长;value是强引用,key是弱引用生命周期短;key被GC后是空,value可能长时间(直到当前Thread运行结束)处于无法使用也无法回收的状态;
  • 怎么解决?手动回收,每次使用完ThreadLocal后调用remove;

6 举例

public class ThreadLocalDemo {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<String> (){
        @Override
        protected String initialValue() {
            return "not be set !";
        }
    };
    static class MyRunnable implements Runnable{
        private int num;
        public MyRunnable(int num){
            this.num = num;
        }
        @Override
        public void run() {
            threadLocal.set(String.valueOf(num));
            System.out.println(Thread.currentThread().getName()+"'s local Value is "+threadLocal.get());
            threadLocal.remove();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread1=new Thread(new MyRunnable(1));
        Thread thread2=new Thread(new MyRunnable(2));
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(Thread.currentThread().getName()+"'s local Value is "+threadLocal.get());
    }
}
Thread-0's local Value is 1
Thread-1's local Value is 2
main's local Value is not be set !

 输出验证了不同线程的本地变量互不影响;

7 Android中的ThreadLocal的不同之处

 相对于原生Java的ThreadLocal,Android进行了一些细节优化,但是大致思想和使用步骤是一致的。

7.1 数据结构上的区别

  • Java中:一个线程对应一个ThreadLocalMap对象,一个ThreadLocalMap对象有一个Entry数组,存放不同的key-value,key是ThreadLocalMap类型的弱引用,而value就是本地变量。
  • Android中:一个线程对应一个Values对象,一个Values对象有一个Object数组,将ThreadLocalMap类型的弱引用和本地变量都存在这个数组里;ThreadLocalMap的弱引用和本地变量在数组中相邻存储。

7.2 不存在内存泄漏问题

8 总结

  • ThreadLocal是线程本地变量;不同线程的本地变量互不影响,不存在线程安全问题;

  • ThreadLocal简单使用:

//ThreadLocal简单使用
static ThreadLocal<Object> threadLocal = new ThreadLocal<> ();//必须设置为静态属性,避免无意义的多实例
threadLocal.set(obj);//为当前线程设置一个本地变量
Object obj2 = threadLocal.get();//获取threadLocal作为key对应的本地变量
threadLocal.set(obj2);//覆盖之前的Value obj
threadLocal.remove();//移除这个本地变量,防止内存泄漏
  • ThreadLocal的存储结构:

 每一个线程都有一个ThreadLocalMap类型的threadLocals属性;而ThreadLocalMap对象持有一个Entry数组的引用,每一个Entry存储一个键值对,Key是一个ThreadLocal的弱引用(实际上是ThreadLocal的hashcode),Value是Object对象,就是本地变量。

  • ThreadLocalMap 是ThreadLocal 的一个内部类,Entry 是ThreadLocalMap 的内部类;

  • set一个本地变量的过程:先根据ThreadLocal对象的hash值,定位到Entry table中的位置i,使用开放定址法处理Hash冲突,过程如下:

    • 如果当前位置Entry为空,生成一个Entry并插入该位置;
    • 如果当前位置Entry不为空且当前key与传入的key相同,用新value覆盖旧value;
    • 如果当前位置Entry不为空但key不同,说明hash冲突,那么只能向后找下一个位置;
  • set、get、remove方法,都会调用expungeStaleEntry(i)方法清理过期Entry(key=null);

  • hash冲突处理办法:ThreadLocalMap处理冲突的方法是开放定址法,di使用的是线性探测

  • 内存泄漏:

    • 为什么?Thread的生命周期可能会比较长;value是强引用,key是弱引用生命周期短;key被GC后是空,value可能长时间(直到当前Thread运行结束)处于无法使用也无法回收的状态;
    • 怎么解决?手动回收,每次使用完ThreadLocal后调用remove;
    • ThreadLocal变量一定要设置为static,避免创建不必要的重复对象;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值