通常我们为了规避单例模式下多线程之间共享全局变量的问题,会使用ThreadLocal,不过很多人不知道ThreadLocal里数据的存储方式和我们平时理解的不太一样。
源码解析(直奔主题):
每个线程都有一个ThreadLocalMap对象:
这个Map里存了所有线程变量中保存的数据,保存的方式:
以ThreadLocal对象作为key,ThreadLocal里“存放”的数据作为value,放在该Map中。
在我们创建了一个线程变量 maxLife=new ThreadLocal<Integer>(),之后执行其set(xxx)方法,其实是以maxLife这个对象为键,以xxx为值,将这组键值对放入当前线程的ThreadLocalMap对象中。
这种方式的存储有一个坑必须注意:
ThreadLocal对象一旦创建不可轻易替换,替换会导致所有共享该ThreadLocal对象的线程均无法找到之前为其“保存”的值。
打个比方
张三(Thread)和李四(Thread)各有一个一模一样盒子(ThreadLocalMap)。
这是个神奇的盒子,可以用任何一个钥匙(ThreadLocal对象)打开,每个钥匙打开盒子都会打开他们自己的存储空间,和别的钥匙互不干扰。
凑巧张三李四共用一个钥匙包(ThreadLocal对象的引用),这个钥匙包里只存放了一个钥匙(ThreadLocal对象)。
有一天张三用这个钥匙包里的钥匙存了一个兔子在盒子里,李四也做了同样的事
接着张三把钥匙包里的钥匙换掉了,那么李四再用这个钥匙包里的钥匙去开盒子,会发现,兔子不见了!
通过伪代码再现坑
为了说明这个坑,我们再上一些伪代码:
首先,我们在类中定义一个全局变量,后边的所有线程都会共享这个变量。
ThreadLocal<Integer> maxLife = null;
然后创建一个线程A,并在这个线程中为maxLife赋一个新对象,并且为其设置值
maxLife = new ThreadLocal<Integer>()
maxLife.set(0);
之后暂停A线程的执行,同时我们创建一个B线程,然后在B线程里,将maxLife指向一个新的对象
maxLife = new ThreadLocal<Integer>();
执行完这一段,暂停B的执行,然后再回到A线程,执行
Integer i = maxLife.get();
这个时候你如果打印i,那么你会看到一个null而不是0。
这是为什么呢?
因为maxLife的get方法,其实是以maxLife这个对象本身为键,然后去当前线程的ThreadLocalMap中获取其所对应的值,A线程中我们new了一个对象然后当成key,B线程中我们又new了一个,我们知道A和B先后new 的两个对象不是同一个对象,那么以不同的对象作为key去一个map里面取值会怎样呢?
也就是说 maxLife 这个变量,在A和B中是共享的,但是maxLife作为key在A和B各自的ThreadLocalMap中对应的值是不一样的。
总结
所以我们定义ThreadLocal变量,应该在一开始就为其new一个值,使用过程中避免修改其引用指向对象,而不是让它等于null。
并且使用完毕后调用其remove()方法,避免产生内存溢出。
完毕。