ThreadLocal详解

ThreadLocal 分析

首先我们看一下下面这个程序

public class ThreadLockDemo {

    //初始tl per对象名是 zs
	static ThreadLocal<per> tl = new ThreadLocal<per>() {
		 protected per initialValue() {
		        return new per("zs");
		    }
	};
	static class per{
		String name;
		public per(String n) {
			this.name = n; 
		}
	} 
		new Thread(()->{
			try {
                //线程0 在tl中放入了新的per对象 ls ,并将线程停留
				tl.set(new per("ls"));
				System.out.println(tl.get().name);
			} catch (Exception e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}).start();
		new Thread(()->{
			try {
                //线程1 首先睡眠 1S
				Thread.sleep(1000);
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
            //按照正常的逻辑,tl被 线程0 set 了 ls 对象,此时取出应该是 ls,而结果却是 zs 是tl设定的初始值
			System.out.println(tl.get().name);
		}).start();
	}
}

zs
ls

然后我们在看下面这个程序,按照上面程序的理解,下面程序的输出结果应该是zs ls zs 但是结果却是 zs ls ls,证明了tl 里面的对象并不是在本地新建一个对象,而是在本地复制了对象的引用,然后线程1通过这个引用把对象修改了,所以线程0后面取到的数据就是修改后的了。

public class ThreadLockDemo {
	static ThreadLocal<per> tl = new ThreadLocal<per>();
	static class per{
		String name;
		public per(String n) {
			this.name = n; 
		}
	} 
	public static void main(String[] args) throws InterruptedException {
		per p  = new per("zs");
		new Thread(()->{
			try {
                //在线程0中添加p对象
				tl.set(p);
				System.out.println(p.name); //输出此时p对象name
				Thread.sleep(5000);
				System.out.println(tl.get().name);//获取此时p对象name
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}).start();
		new Thread(()->{
            //同样添加p对象
			tl.set(p);			
			per pp = tl.get();
            //修改p对象
			pp.name = "ls";
			System.out.println(tl.get().name);
		}).start();
	}
}
zs
ls
ls

下面是第二个程序的分析图

下面是ThreadLoacl的引用关系,简单说明一下set和get过程

set

在Thread内部的ThreadLocalMao中添加一个key 指向 ThreadLocal,value指向 需要保存对象的 entey 对象,此时的key指向entry是虚引用,这样就能保证在执行remove的时候,3这条指向断开或者指向其他对象),2这个指向因为是虚引用,所以Entry可以被垃圾回收,不然的话这个Entry被new ThreadLocal引用,造成内存泄漏。当然,如果不执行remove,5这条指向不会断开,同样会造成 new per("ZS")这个地方内存泄漏。
补充: 除了2这条指向new ThreadLocal的引用,还有一个tl指向它的强引用,这样才能保证new ThreadLocal这个对象不被回收。

下面是源码分析

//ThreadLocal的set方法  
public void set(T value) {
    //获取当前线程
    Thread t = Thread.currentThread();
    //得到当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    	/*
    		这是getMap方法,显然是返回了一个当前线程的ThreadLocalMap对象
            ThreadLocalMap getMap(Thread t) {
                return t.threadLocals;
            }
         */
    //如果map不是空的
    if (map != null)
        //将当前的value设置到map里面,此时的this是当前线程,
        //所以这个map的key是当前线程,以第二个程序为例,value是一个 per对象的引用连接,这里可以在设置之前打印看一下,是一个对象的引用
        map.set(this, value);
    	/*
    		这是set方法实现细节
    		private void set(ThreadLocal<?> key, Object value) {
			因为此时map不为空,需要拿到当前Thread里面的ThreadLocalMap里面的enery数组
			Entry[] tab = table;
            int len = tab.length;
            拿到当前key的hashCode(被重写了,线程使用了AtomicInteger类)并和len-1,len肯定是2的倍数,与len-1 进行与操作可以大幅度保证 i 与hashCode相同
            int i = key.threadLocalHashCode & (len-1);
			从当前hashCode的位置开始找位置放当前value
            for (Entry e = tab[i]; 
            	 如果不为空就一直找下去
                 e != null;
                 如果 i+1 < len  nextIndex 返回 i+1 ,否则返回 0 ,作用:如果当前key有值,就一直往后找,到达最后还没有找到空值位置再从第一个开始找。不会找不到的,因为上一次循环结束如果没位置了就会扩容,保证下一个有空位置。
                 e = tab[i = nextIndex(i, len)]) {
                下面过程要记得key 是 ThreadLocal 对象,不是 Thread 对象
                ThreadLocal<?> k = e.get();
				如果存在相同key则直接覆盖,这是第一个程序中tl.set(new per("ls")) ls 覆盖 zs 处理部分
				结束循环
                if (k == key) {
                    e.value = value;
                    return;
                }
				如果当前ThreadLocal不存在这个key,结束循环
                if (k == null) {
                	从当前位置开始将脏的Entry都处理掉
                    replaceStaleEntry(key, value, i);
                    return;
                }

            }
			把当前元素的值给到Entry[i]
            tab[i] = new Entry(key, value);
            大小加1
            int sz = ++size;
            如果清理了一部分脏数据就判断是否需要扩容,否则判断一下
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
            	扩容,这个依旧是先判断有没有脏数据,如果清理成功就不扩容
                rehash();
        }
    	*/
    else
        //创建一个新的ThreadLocalMap
        createMap(t, value);
    	/*
    	void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        	INITIAL_CAPACITY = 16
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            设置上限为16 * 2/3
            setThreshold(INITIAL_CAPACITY);
        }
    	*/
}

下面考虑最后一个问题,假设同时又两个线程执行 tl.set()

解答:

执行 tl.set 首先是调用 ThreadLocal 内部的 set 方法,内部是不同的线程的各自的ThreadLocalMap对象的增减,彼此线程之间不影响,所以可以多线程使用,这部分是安全的。虽然是同一个ThreadLocal

posted @ 2021-05-13 16:12  二五树  阅读(87)  评论(0编辑  收藏  举报