ThreadLocal概念
以上是ThreadLocal的注释,大致意思是:ThreadLocal提供了线程局部变量的能力。这些变量与普通变量的不同之处在于每个线程都有自己独立的副本变量,ThreadLocal实例通常是类中希望将状态与线程关联起来的私有静态字段(例如用户ID或者事务ID)。
另外,使用ThreadLocal而不使用普通变量还有一层原因就是ThreadLocal封装的非常优雅。
该实现应用了ThreadLocal所暴露出的所有APi
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "init");
Thread[] threads = new Thread[5];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set("threadName:" + Thread.currentThread().getName());
if (Thread.currentThread().getName().equals("thread1")) threadLocal.remove();
System.out.println(threadLocal.get());
}
}, "thread" + i);
}
for (Thread thread : threads) {
thread.start();
}
执行结果:
以上是idea导出类图。ThreadLocal有2个内部类:ThreadLocalMap和SupliedThreadLocal。
ThreadLocal的主要结构是一个散列表结构,key为ThreadLocal本身,value为通过set()方法进去的数据。ThreadLocal的散列表由其内部类ThreadLocalMap实现。它与我们平时经常使用的HashMap不同,HashMap解决散列冲突的方法是拉链法,而ThreadLocalMap使用的是开放定址法,这也反向说明不要在ThraedLocal存储过多数据。
开放定址法:也叫闭散列,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的下一个空位置中去。这块可以具体看下代码:
private void set(ThreadLocal<?> key, Object value) {
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();
}
这里有个变量:threadLocalHashCode获取逻辑等同如下公式:
(n*0x61c88647)&(table.length - 1)
在上述代码中,有段逻辑值得深究:
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
在这段逻辑中Entry对象不为null,但是k却为null。这种情况是因为Entry中key值的实现是弱引用,对象在只有弱引用存在的时候,发生GC就会被回收。
当key被回收,value却还在,ThreadLocalMap中有多处实现会清空这类对象,replaceStaleEntry就是。但是这依赖于二次调用的过程,如果我们是在使用池化线程,还是需要将不用的ThreadLocal remove掉,以避免内存泄漏的发生。
ThreadLocalMap虽然维护在ThreadLocal中,但是它却被存储在Thread中,每个Thread独自保存自己的ThreadLocalMap,也就是这一实现赋予了ThreadLocal线程安全的特点,如下:
其中第二个与常规ThreadLocal不同的是,它由ThreadLocal的子类InheritableThreadLocal维护,且支持继承父线程的InheritableThreadLocal。这一实现可以查看Thread类的init方法看到。
SupliedThreadLocal同样是ThradLocal的静态内部子类,它的实现主要是提供一个初始化方法:
在我们开始的实现中使用了withInitial方法,其目的就是将ThreadLocal转换为了其静态内部子类SupliedThreadLocal,这样在使用threadlocal.get
结果为null的时候,就会自动调用我们给到的lambda函数,源码如下:
FastThreadLocal的升级点
Netty对ThreadLocal进行了进一步优化,在FastThreadLocal中不再需要散列表,而是直接使用数组,使其在频繁访问时具有更高的性能。
使用FastThreadLocal必须是在Netty实现的FastThreadLocalThread或者其子类中,由于这个原因由DefaultThreadFactory创建的所有线程都是FastThreadLocalThread。
线程执行完执行FastThreadLocal的remove操作。
variablesToRemoveIndex=0,这个过程也就是将另外一个Set放在了数组0的位置,Set存放的是当前线程所有的FastThreadLocal,方便线程执行完的时候统一清理。
众所周知,CPU和磁盘之间的速度相差悬殊,为了弥补这种差距,计算机设计了多层缓存。当CPU执行运算的时候,它先去L1查找所需的数据、再去L2、然后是L3,如果最后这些缓存中都没有,所需的数据就要去主内存拿。走得越远,运算耗费的时间就越长。所以如果你在做一些很频繁的事,你要尽量确保数据在L1缓存中。
另外,线程之间共享一份数据的时候,需要一个线程把数据写回主存,而另一个线程访问主存中相应的数据。
下面是从CPU访问不同层级数据的时间概念:
可见CPU读取主存中的数据会比从L1中读取慢了近2个数量级。
现代CPU以一整块连续的块为单位,称为Cache Line(缓存行)。所以通常情况下访问连续存储的数据会比随机访问的快。缓存行的大小一般为64b,所以对于CPU来说,每次读取数据都会加载连续的64b。
根据MESI协议,如果一个核正在使用缓存被其他核修改,那么整个缓存行就会失效。这个时候多个核的多个数据共享一个缓存行,就会导致缓存行的频繁失效。这种没有数据竞争而导致的缓存行失效就叫做伪共享。在java中一般采用字节填充的方式来解决伪共享问题。
在FastThreadLocal中有如下代码:
这里填充了72个字节来保证在启用CompressedOops的情况下,该类的一个实例至少占用128字节,一次来解决FastThreadLocal的伪共享问题,进而提高速度。
不过目前看这块有一点问题:通过在IDEA上安装插件JOL,查看IntenalThreadLocalMap
可见其一个对象136个字节,这个有点奇怪。https://github.com/netty/netty/issues/9284 问了同样的问题,
我觉得这个回答比较靠谱。
不过在JDK1.8中增加了注解@Contended来解决伪共享的问题,默认情况下,除了 JDK 内部的类,JVM 会忽略该注解。要应用代码支持的话,要设置 -XX:-RestrictContended=false,它默认为 true(意味仅限 JDK 内部的类使用)。当然,也有个 –XX: EnableContented 的配置参数,来控制开启和关闭该注解的功能,默认是 true,如果改为 false,可以减少 Thread 和 ConcurrentHashMap 类的大小。