ThreadLocal源码分析(jdk1.8)

ThreadLocal,从名字上可以知道和线程本地有关系,这个类会为每个线程提供属于线程自己的局部变量。ThreadLocal可以通过initialValue()为每个线程赋值,也可以由线程自己调用ThreadLocal的set()进行赋值。每个线程操作对应的变量时,与其他线程不会发生冲突,改动仅对自己可见。总的来说,ThreadLocal适用于变量在线程间隔离的场景,这里也可以看出ThreadLocal采用了空间换时间的策略保证并发安全。

在jdk1.7和1.8中,ThreadLocal原理并不相同,本文主要分析jdk1.8的。

首先来看一个demo,ThreadLocal为主线程和t1线程分配id,并且在initialValue()中分配一次就让id++。

	public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal=new ThreadLocal<Integer>(){
            int id=0;

            @Override
            protected Integer initialValue() {
                return id++;
            }
        };
        new Thread(()->{
            System.out.println("t1线程获取数据:"+threadLocal.get());
        },"t1").start();
        System.out.println("主线程获取数据:"+threadLocal.get());
    }

在这个demo中我们重写了initialValue()方法,让每个线程都能拿到初始值,如果不重写这个方法,就需要线程自己调用set()方法向里面存值。我们从set()方法开始看起。

ThreadLocal的常用方法

set()流程

	//value:每个线程调用set存的值
	public void set(T value) {
        //获取当前线程的引用
        Thread t = Thread.currentThread();
        //获取线程保存的ThreadLocalMap
        //ThreadLocalMap是ThreadLocal的静态内部类,是真正用来存放数据的
        ThreadLocalMap map = getMap(t);
        //map是否被创建过
        if (map != null)
            //创建过,就调用map.set()
            map.set(this, value);
        else
            //第一次调用时,map并未创建,所以走这个逻辑
            createMap(t, value);
    }
	
	//getMap,就是获取每个线程自己保管的ThreadLocalMap
	// ThreadLocal.ThreadLocalMap threadLocals = null; 这句代码在Thread类中
	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

	//创建ThreadLocalMap,并赋值给当前线程的threadLocals字段
	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

到这里set流程就结束了,可以知道在jdk1.8中,ThreadLocalMap是由线程进行保管的。

get()流程

	public T get() {
        //获取当前线程引用
        Thread t = Thread.currentThread();
        //获取到线程保管的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //ThreadLocalMap的静态内部类Entry,数据存放在这里
            ThreadLocalMap.Entry e = map.getEntry(this);
            //判断Entry是否为null
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                //返回数据
                return result;
            }
        }
        //map为null,说明还未调用set(),就调用get()
        return setInitialValue();
    }

	private T setInitialValue() {
        //调用initialValue(),如果用户有重写就调用重写的方法,否则是空实现
        T value = initialValue();
        //以下是set()的逻辑,最后返回初始化的值
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

get()的逻辑也比较简单,就是通过线程保管的ThreadLocalMap里面查找数据并返回,如果Map还未创建,也会先执行初始化逻辑。

remove()流程

	public void remove() {
        //获取线程保管的map
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            //调用map的remove移除数据
            m.remove(this);
     }

到这里ThreadLocal常用的方法就分析完毕了,可以发现ThreadLocal仅仅是对ThreadLocalMap的操作进行了封装,最核心的逻辑还是在ThreadLocalMap内完成的,接下来就去看它的源码。

ThreadLocalMap源码分析

ThreadLocalMap就是ThreadLocal的内部类,也在ThreadLocal文件中。首先来看一下它的重要字段,方法,和构造。

重要的字段

//散列表的初始长度
private static final int INITIAL_CAPACITY = 16;
//ThreadLocalMap内的散列表
private Entry[] table;
//当前散列表的元素个数
private int size = 0;
//扩容阈值
private int threshold; // Default to 0

重要的方法

	//设置扩容阈值
	private void setThreshold(int len) {
        //扩容阈值为当前散列表长度的2/3
        threshold = len * 2 / 3;
    }
    
	//返回当前下标的前一个下标
	//i:下标  len:散列表长度
    private static int prevIndex(int i, int len) {
        //i-1>=0 说明当前元素下标>=1,返回i-1
        //i-1<0 说明当前元素下标已经为0了,在数组头部,那就返回数组末尾的下标,形成一个环式查找
        return ((i - 1 >= 0) ? i - 1 : len - 1);
    }

	//同上,返回当前下标的下一个下标,到数组尾就返回0
    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
    

构造

	//firstKey:在前面创建map的时候,会将ThreadLocal对象传入
	//firstValue:ThreadLocal对应的值
	ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        //创建长度为16的散列表
        table = new Entry[INITIAL_CAPACITY];
        //寻址算法   threadLocalHashCode & 长度-1
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        //将ThreadLocal和值封装成Entry放入散列表中
        table[i] = new Entry(firstKey, firstValue);
        //设置容量
        size = 1;
        //设置扩容阈值  数组长度*2/3=10
        setThreshold(INITIAL_CAPACITY);
    }

在构造中涉及到了threadLocalHashCode和Entry,现在我们来分析一下这两个知识点。

ThreadLocalHashCode

HashCode相关代码在ThreadLocal类中

	//当前ThreadLocal对象的HashCode,非静态,说明每个ThreadLocal对象独有
	private final int threadLocalHashCode = nextHashCode();
	
	//为每一个ThreadLocal对象分配HashCode,静态,说明ThreadLocal对象共享
    private static AtomicInteger nextHashCode =
        new AtomicInteger();
	
	//黄金分割数,每分配一个HashCode,就需要增加这个数,可以使hash算法分散的更均匀
    private static final int HASH_INCREMENT = 0x61c88647;

	//返回HashCode
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

Entry

Entry是ThreadLocalMap的静态内部类,用来构建key-value

	//WeakReference 弱引用,如果当前对象只有弱引用指向时,gc会回收这个对象
	//这里的key ThreadLocal就是弱引用,如果key被回收后,就为null,对应的value是强引用,会造成内存泄漏的问题
	static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

关于ThreadLocal内存泄露的问题,可以去看看这篇文章,本文不解释概念了。

测试ThreadLocal 在gc后引发的threadLocalMap的key为null,但value不为null的情况

在上面这篇文章的代码里,debug会输出一些其他值,请将上文42行的输出语句改为下面的即可

System.out.println("弱引用key:"+referenceField.get(o)+",值:"+valueField.get(o));

到这里我们可以大概清楚了ThreadLocal的结构
在这里插入图片描述

线程Thread内部持有ThreadLocalMap的引用,Map内部使用Entry数组进行保存以ThreadLocal为key的k-v键值对Entry对象。

接下来我们就去看看ThreadLocalMap的相关操作方法

set()流程

	private void set(ThreadLocal<?> key, Object value) {
        	//散列表
            Entry[] tab = table;
        	//长度
            int len = tab.length;
        	//计算当前ThreadLocal存放的下标
            int i = key.threadLocalHashCode & (len-1);
			
        	//e=tab[i] 将当前桶位的Entry赋值给e
        	//e!=null 说明当前桶位已经有数据了,发生了hash冲突
        	//e = tab[i = nextIndex(i, len)] 每一轮循环后,线性的向后查找
        	//ThreadLocalMap和HashMap不同,HashMap解决hash冲突的方法是链地址法,而ThreadLocalMap是线性探测
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                
                //获取当前Entry的key ThreadLocal对象
                ThreadLocal<?> k = e.get();
				
                //如果传入的key和当前桶位的ThreadLocal对象相同
                if (k == key) {
                    //说明是替换操作
                    e.value = value;
                    return;
                }

                //k==null 说明了什么
                //Entry不为null,而key为null,说明这是个过期数据,key已经被gc回收了,而value没有释放
                if (k == null) {
                    //传入当前桶位下标和k-v,去替换当前过期数据
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
        
			//退出循环,说明找到了合适的桶位,创建Entry放入对应的桶位。
            tab[i] = new Entry(key, value);
        	//设置容量
            int sz = ++size;
        	
        	//cleanSomeSlots 启发式清理,后面分析
        	//!cleanSomeSlots(i, sz)成立,说明没有清理到过期数据
        	//sz >= threshold成立,说明需要扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
	
		//staleSlot:是一个过期Entry的下标
		private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) {
            //获取散列表
            Entry[] tab = table;
            //获取散列表数组长度
            int len = tab.length;
            //临时变量
            Entry e;
            
            //表示开始探测式清理过期数据的开始下标。默认从当前 staleSlot开始。
            int slotToExpunge = staleSlot;
            
            //以当前staleSlot开始 向前迭代查找,找有没有过期的数据。for循环一直到碰到null结束。
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len)){
                
                //过期数据指的是Entry不为null,而key为null
                if (e.get() == null){
                    //说明向前找到了过期数据,更新探测清理过期数据的开始下标为i
                    //只有找到了过期数据才slotToExpunge
                    slotToExpunge = i;
                }
            }

            //以当前staleSlot向后去查找,直到碰到null为止。
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                //获取当前元素 key
                ThreadLocal<?> k = e.get();

                //条件成立:说明是替换
                if (k == key) {
                    //替换新数据。
                    e.value = value;

                    //交换位置的逻辑
                    //将table[staleSlot]这个过期数据放到当前循环到的table[i]这个位置,table[i]就是e
                    tab[i] = tab[staleSlot];
                    //将tab[staleSlot] 中保存为当前entry。就是将i和staleSlot的元素进行交换
                    tab[staleSlot] = e;

                    //条件成立:
                    // 1.说明replaceStaleEntry 一开始时 的向前查找过期数据 并未找到过期的entry.
                    // 2.向后检查过程中也未发现过期数据
                    if (slotToExpunge == staleSlot)
                        //开始探测式清理过期数据的下标修改为当前循环的i
                        slotToExpunge = i;
                    //cleanSomeSlots :启发式清理
                    //expungeStaleEntry:探测式清理
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                //条件1:k == null 说明当前遍历的entry是一个过期数据.
                //条件2:slotToExpunge == staleSlot 说明向前查找过期数据并未找到过期的entry.
                if (k == null && slotToExpunge == staleSlot)
                    //因为向后查询过程中查找到一个过期数据了,更新slotToExpunge 为当前位置。
                    //前提条件是前驱扫描时未发现过期数据 
                    slotToExpunge = i;
            }

            //如果执行到这里
            //说明向后查找过程中并未发现 k == key 的entry,说明当前set操作是添加
            //直接将新数据添加到 table[staleSlot]对应的桶位
            //因为调用此方法处就判断当前桶位是过期数据,所以先清空value
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);


            //条件成立:除了当前staleSlot以外 ,还发现其它的过期桶位,所以要开启清理数据的逻辑
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

因为ThreadLocal是一个弱引用,会引发内存泄漏的问题,所以在设计ThreadLocal时,内部就已经做好了两种过期数据回收,这个会在后面提到。在set()的流程中可以发现,如果发生了hash冲突,就会向当前桶位的后面线性的查找空桶位进行存放数据,这种方式被称为线性探测。

get()流程

在ThreadLocal里,get()会调用到这个方法里,所以我们从这里开始分析

	private Entry getEntry(ThreadLocal<?> key) {
        //计算下标
       int i = key.threadLocalHashCode & (table.length - 1);
        //获取当前桶位的Entry
       Entry e = table[i];
       	//如果Entry不为null,并且key相同
       if (e != null && e.get() == key)
           //返回
           return e;
       else
           //进入这里,说明发生过hash冲突,当前要查询的ThreadLocal对象被放在了其他的桶位
           return getEntryAfterMiss(key, i, e);
    }

	private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    	Entry[] tab = table;
    	int len = tab.length;
        //向后遍历数组,直到碰到空Entry为止
    	while (e != null) {
            //遍历到的Entry的ThreadLocal
        	ThreadLocal<?> k = e.get();
        	if (k == key)
                //相同就返回
            	return e;
        	if (k == null)
                //Entry!=null,k==null 过期数据
                //探测式过期数据回收
            	expungeStaleEntry(i);
	        else
                //继续向后查询
    	        i = nextIndex(i, len);
        	e = tab[i];
    	}
        //说明没有该数据
    	return null;
	}

get()的逻辑还算简单,计算下标后线性查询,并且碰到过期数据就进行探测式回收。

remove()流程

接下来是remove的逻辑

private void remove(ThreadLocal<?> key) {
    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)]) {
        //查找到对应的Entry
        if (e.get() == key) {
            //将Entry的key设置为null
            e.clear();
            //从当前下标开始进行探测式清理
            expungeStaleEntry(i);
            return;
        }
	}
}

remove也很简单,线性查找到后,从当前桶位开始探测式清理。

扩容流程

在set方法里还有一个rehash()方法没有分析,这里就设计到map的扩容流程了。

private void rehash() {
    //过期数据回收
    expungeStaleEntries();
    //如果清理完后的容量>=扩容阈值的3/4
    if (size >= threshold - threshold / 4)
        //进行扩容
        resize();
}

private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    //遍历散列表
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        //如果是过期数据
        if (e != null && e.get() == null)
            //从当前桶位开始进行一次探测式清理
            expungeStaleEntry(j);
    }
}

private void resize() {
    //获取当前散列表
    Entry[] oldTab = table;
    //获取旧表的长度
    int oldLen = oldTab.length;
    //新表长度为旧表长度的2倍
    int newLen = oldLen * 2;
    //创建新表
    Entry[] newTab = new Entry[newLen];
    //计算新表中元素的个数
    int count = 0;
    //遍历旧表
    for (int j = 0; j < oldLen; ++j) {
        //遍历到的元素
        Entry e = oldTab[j];
        //有数据
        if (e != null) {
            ThreadLocal<?> k = e.get();
            if (k == null) {
                //说明是一个过期数据,释放value
                e.value = null; // Help the GC
            } else {
                //计算该元素在新表的下标
                int h = k.threadLocalHashCode & (newLen - 1);
                //可能发生了hash冲突,所以要找到一个合适的位置存放
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                //计算数量
                count++;
            }
        }
    }
    //设置下一次的扩容阈值
    setThreshold(newLen);
    //设置容量
    size = count;
    //将新表赋值给table
    table = newTab;
}

扩容的流程也是比较简单的,但是有没有朋友看到这会想,在多线程环境下,扩容为什么没有加锁呢?其实很简单,这个map是由线程自己保管的,所以扩容是一个单线程的逻辑。

到这里ThreadLocalMap的常用方法就分析完成了,接下来就该分析探测式清理和启发式清理的逻辑。

探测式清理

探测式清理其实也比较好理解,和线性探测思想一样,遍历散列表清理过期数据。

//staleSlot:从调用处看,为过期数据的下标
private int expungeStaleEntry(int staleSlot) {
    //获取散列表
    Entry[] tab = table;
    //散列表长度
    int len = tab.length;
    //因为是过期数据,将value和Entry都设置为null
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    //清理了当前过期数据,容量-1
    size--;
    Entry e;
    int i;
    //i = nextIndex(staleSlot, len) 从过期数据开始,向后遍历
    //(e = tab[i]) != null 直到遍历桶位为null,即table[i]==null
    for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        //过期数据处理
        if (k == null) {
            //清空
            e.value = null;
            tab[i] = null;
            //容量-1
            size--;
        } else {
            //说明这里是正常的数据
            //因为前面可能清理了一部分过期数据,导致前面的桶位为null,这时要解决hash冲突带来的影响
            //ThreadLocalMap使用线性探测来解决hash冲突,导致有些桶位的数据会有一定的偏移
            //所以在这里重新计算下标,找到一个最接近hash算法计算出来的桶位进行存放
            
            //计算下标
            int h = k.threadLocalHashCode & (len - 1);
            //i是当前下标
            //成立:说明当前的Entry发生过冲突
            if (h != i) {
                //将当前桶位设置为null
                tab[i] = null;
                //从计算的下标开始遍历,找到合适的桶位进行存放
                while (tab[h] != null)
                    h = nextIndex(h, len);
                //有可能是h,也有可能是h到i中间的某个位置
                tab[h] = e;
            }
        }
    }
    //退出循环,下标为i的桶位就是table[i]==null
    return i;
}

探测式清理过程也比较好理解,从过期数据开始向后遍历查找过期数据,如果碰到正常数据,就重新计算下标,减少hash冲突带来的影响。

启发式清理

//i:从调用处来看,下标为i的桶位一定不是过期数据,启发式清理从i后面开始
//n:有可能为当前元素个数,有可能为当前散列表长度,这里按长度16来举例
private boolean cleanSomeSlots(int i, int n) {
    //是否清理过数据
    boolean removed = false;
    //散列表
    Entry[] tab = table;
    //散列表长度
    int len = tab.length;
    do {
        //获取i下一个桶位
        i = nextIndex(i, len);
        Entry e = tab[i];
        //如果是过期数据
        if (e != null && e.get() == null) {
            //将n设置为数组长度
            n = len;
            //清理过数据,所以设置为true
            removed = true;
            //从当前桶位开始再进行一次探测式清理
            i = expungeStaleEntry(i);
        }
        //n=16
        //  16 >>> 1 =8
        //  8  >>> 1 =4
        //  4  >>> 1 =2
        //  2  >>> 1 =1
        //  1  >>> 1 =0
        //当n=16时,会进行5次搜索,如果找到过期数据,会从过期数据开始进行一次探测式清理
    } while ( (n >>>= 1) != 0);
    return removed;
}

到这里,关于ThreadLocal的一些重要方法就已经分析完毕了,还没有看懂的朋友,请打开源码,跟着流程一起走一遍,有些地方需要自己动手画一画才知道这些代码做了哪些操作。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocal码是Java中一个关键的类,它提供了一种在多线程环境下实现线程本地变量的机制。在JDK 8之前和之后,ThreadLocal的内部结构有所变化。ThreadLocal码分为两部分:ThreadLocal类和ThreadLocalMap类。 ThreadLocal类是一个泛型类,它包含了两个核心方法:set()和get()。set()方法用于将一个值与当前线程关联起来,get()方法用于获取当前线程关联的值。 ThreadLocalMap类是ThreadLocal的内部类,它用于存储每个线程的本地变量。在JDK 8之前,ThreadLocalMap是通过线性探测法解决哈希冲突的,每个ThreadLocal对象都对应一个Entry对象,Entry对象包含了ThreadLocal对象和与之关联的值[2]。 在JDK 8之后,ThreadLocalMap的实现方式发生了改变。使用了类似于HashMap的方式,采用了分段锁的机制来提高并发性能。每个线程维护一个ThreadLocalMap对象,其中的Entry对象也是采用链表的形式来解决哈希冲突。 总结起来,ThreadLocal码主要由ThreadLocal类和ThreadLocalMap类组成。ThreadLocal类提供了set()和get()方法来管理线程本地变量,而ThreadLocalMap类则负责存储每个线程的本地变量,并解决哈希冲突的问题。 史上最全ThreadLocal 详解 ThreadLocal分析_02 内核(ThreadLocalMap) 【JDK码】线程系列之ThreadLocal 深挖ThreadLocal ThreadLocal原理及内存泄露预防 ThreadLocal原理详解——终于弄明白了ThreadLocal ThreadLocal使用与原理 史上最全ThreadLocal 详解。 ThreadLocal分析,主要有ThreadLocal码以及ThreadLocal的内部结构在jdk8前后的变化。 使用方式非常简单,核心就两个方法set/get public class TestThreadLocal { private static final ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { try { threadLocal.set("aaa"); Thread.sleep(500); System.out.println("threadA:" threadLocal.get()); } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { threadLocal.set("bbb"); System.out.println("threadB:" threadLocal.get()); } }).start(); } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值