多个线程ThreadLocal中存的是什么

之前所学不精,现在看一下确实是,我ThreadLocal里如果都存的是一个共享变量的话,那么肯定是会两边都相同的。其实现在回头看这些代码就没有了当初学术不精时候的疑惑了,反正也被喷了,趁这个被喷的时间索性更正一下ThreadLocal的存储机制

测试代码相当简单

public static void main(String[] args){
        ThreadLocal<String> tl1 = new ThreadLocal<>();
        tl1.set("tl1");
  
        System.out.println(tl1.get());
    }

这里要分析的也就两行

  1. ThreadLocal是怎么set的
  2. ThreadLocal是怎么get的

ThreadLocal是怎么set的

我们直接就看ThreadLocal的set方法

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

这里面有四个点

  1. ThreadLocalMap类是个什么东西
  2. getMap方法是什么
  3. map.set方法是怎么set的
  4. createMap方法是什么,为什么需要线程参数 t

其实这里对于第四个点,我没点进去看也是有点疑惑的,为什么这个createMap方法的两个参数和上一句map.set的两个参数不一样,这不都是set一个键值对么,然后点进去就什么都知道了。(我之所以这么说是因为我觉得总会有人和我想的一样的)

下面逐一解释这4个点

ThreadLocalMap类是个什么东西

ThreadLocalMap是ThreadLocal的一个静态内部类,内部指的看一下的东西如下

  • Entry类,这个类比HashMap里的Entry简单多了,就一个构造,参数一个是Threadlocal键对象,一个Object值对象
  • Entry数组table,做hash存储用的,懂HashMap的我就不说了
  • 再就是阈值啊,初始大小之类的参数,这些在此文章就不关心了

getMap方法是什么​​​​​​​:

getMap(Thread t)方法也是ThreadLocal类的一个内部方法

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

这方法的意思就是返回线程t的内部参数threadLocals,关于线程对象中threadLocals参数,总结起来就是你用不到ThreadLocal,线程对象的这个属性就一直是null,这一点了解到这里就可以了,有兴趣可以去看Thread类。接着上面的逻辑,如果getMap不是空,就用ThreadLocalMap的set方法置入一个以当前ThreadLocal对象为键,value为值得这么一个键值对;如果getMap为空,那么就以createMap方法set第一个值。

map.set方法是怎么set的​​​​​​​:

ThreadLocalMap类的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);

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

这里的处理逻辑几乎和HashMap的一样,虽然没HashMap那么细

  • 计算当前键的hash值
  • 去table里找,重复键就替换值,不重复就在该位置添加这个键值对
  • 当前容量超过阈值就扩容然后rehash()

createMap方法是什么,为什么需要线程参数 t​​​​​​​:

关于createMap方法的逻辑

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

其中ThreadLocalMap的构造方法

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);
        }

createMap方法的意思就是,构造一个新的ThreadLocalMap对象,将value对象塞进map,然后把传入的线程对象的threadLocals属性指向这个新ThreadLocalMap。

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

和HashMap一样,对应的key有就返回value,没有就null

到这里代码就讲解完了

总结(虽然我很想把总结写在开头)

  • ​​​​​​​ThreadLocal进行set的时候,是在当前线程Thread中获取到有且唯一的ThreadLocalMap对象(如果没有就新建一个ThreadLocalMap对象设置进Thread的属性里),然后把自己作为键,value作为值set进这个Map里
  • ThreadLocal进行get的时候,是从当前线程Thread中获取到有且唯一的ThreadLocalMap对象(Thread的ThreadLocalMap属性如果为空,也就是说这个线程从来都没有用过ThreadLocal设置过值,返回null),然后把自己做为键去该Map里面找,找到就返回对于的value,没有就返回null

昨天查资料看到了ThreadLocal这个类,原来一直没有仔细关注过,牛客网看到的一道题说

ThreadLocal用哈希表的形式为每一个线程都提供一个变量的副本

并且给的回答是正确的,这里我们想一下,什么叫变量的副本,如果某一个线程中副本被修改,那么,其他线程中“副本”会不会被修改。

我们来看以下代码:

public class Demo1 {
	private static ThreadLocal<Student> local = new ThreadLocal<Student>();
	public static void main(String[] args) {
		final Student student = new Student();	//所谓的副本原始对象,我们就存这个
		student.setAge(19);						//给个初始值19
		/**
		 * 实验策略是创建两个线程都进行保存student,然后都休息一段时间(给个3秒)
		 * A线程休息完后修改student中的年龄为11
		 * B线程在休息完3秒后继续休息2秒,目的是为了等A修改完
		 * B线程休息完后取出自己所存的Student,看看里面的age到底是19还是11
		 */
		new Thread(){				//A线程
			public void run() {
				local.set(student);				
				try {
					Thread.sleep(3000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				local.get().setAge(11);
			};
		}.start();
		new Thread(){				//B线程
			public void run() {
				local.set(student);
				try {
					Thread.sleep(5000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				System.out.println(local.get().getAge());
			};
		}.start();
	}
}
class Student{
	private int age;

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}
}

最后结果为11,这个结果也就是说两个线程里面存的是同一个Student对象,修改时线程之间会被影响,而不是所谓的各自一个“副本”,谁也影响不了谁

具体ThreadLocal中是怎么存的,简单来说就是ThreadLocal类有方法调用当前Thread的ThreadMap对象(该对象不是HashMap的子类,但是同样实现了HashMap中的拉链式的结构,并且是Thread的内部类),拿到对象后把自己(ThreadLocal)当键,在里面找有没有已经存在的自己,也就是判断自己是否以前存过东西,存过就替换值,没存过就新开辟地方存值。

对于ThreadLocal的具体源码解析,博主http://blog.csdn.net/wanzaixiaoxinjiayou/article/details/49703135有具体分析。

猜你喜欢

转载自blog.csdn.net/lyandyhk/article/details/50953997
今日推荐