并发容器之ThreadLocal - omigaw/spring- GitHub Wiki
1. ThreadLocal的简介
在多线程编程中通常解决线程安全的问题我们会利用synchronized或者lock控制线程对临界区资源的同步顺序从而解决线程安全的问题,但是这种加锁的方式会让未获取到锁的线程进行阻塞等待,很显然这种方式的时间效率并不是很好。线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,那么,如果每个线程都使用自己的"共享资源",各自使用各自的,又互相不影响到彼此即让多个线程间达到隔离的状态,这样就不会出现线程安全的问题。事实上,这就是一种"空间换时间"的方案,每个线程都会拥有自己的"共享资源"无疑内存会大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待的情况从而提高时间效率。 从ThreadLocal这个类名可以顾名思义的进行理解,表示线程的"本地变量",即每个线程都拥有该变量副本,达到人手一份的效果,各用各的这样就可以避免共享资源的竞争。
2. ThreadLocal 的实现原理
2.1 void set(T value)
set方法设置在当前线程中threadLocal变量的值,该方法的伪代码为: * 获取当前线程实例对象 * 通过当前线程实例获取到ThreadLocalMap对象 * 如果Map不为null,则以当前threadlocal实例为key,值为value进行存入 * map为null,则新建ThreadLocalMap并存入value 方法的逻辑很清晰,value是存放在了ThreadLocalMap里,数据的value是真正的存放在了ThreadLocalMap这个容器中了,并且是以当前threadLocal实例为key。
2.2 T get()
get方法是获取当前线程中threadLocal变量的值。 * 获取当前线程的实例对象 * 获取当前线程的threadLocalMap * 获取map中当前threadLocal实例为key的值的entry * 当前entity不为null的话,就返回相应的值value * 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value
2.3 void remove()
remove()负责从map中删除该threadLocal实例为key的键值对。
3. ThreadLocalMap详解
3.1 Entry数据结构
ThreadLocalMap是threadLocal一个静态内部类,和大多数容器一样内部维护了一个数组,同样的threadLocalMap内部维护了一个Entry类型的table数组。
`private Entry[] table;`
3.2 set方法
与concurrentHashMap、hashMap等容器一样,threadLocalMap也是采用散列表进行实现的。
解决hash冲突主要有两种方法:分离链表法、开放地址法
* 分离链表法
hashmap concurrentHashMap
* 开放地址法
开放地址法不会创建链表,当关键字散列到的数组单元已经被另外一个关键字占用的时候,就会尝试在数组中寻找其他的单元,直到找到一个空的单元。探测数组空单元的方式有很多,介绍一种最简单的----线性探测法。线性探测法就是从冲突的数组单元开始,依次往后搜索空单元,如果到数组尾部,再从头开始搜索。
ThreadLocalMap中使用开放地址法来处理散列冲突,而HashMap中使用的分离链表法。之所以采用不同的方式主要是因为:在ThreadLocalMap中的散列值分散的十分均匀,很少会出现冲突。并且ThreadLocalMap经常需要清除无用的对象,使用纯数组更加方便。
3.3 ThreadLocal 优化
- ThreadLocal的hashcode
threadLocal实例的hashCode是通过nextHashCode()方法实现的,该方法实际上总是用一个AtomicInteger加上0x61c88647来实现的。0x61c88647这个数是具有特殊意义的,它能够保证hash表的每个散列桶能够均匀分布,这是
Fibonacci Hashing。也正是能够均匀分布,所以threadLocal选择使用开放地址法来解决hash冲突的问题。 - 怎样确定新值插入奥哈希表中的位置 该操作源码为:key.threadLocalHashCode & (len-1),同hashMap和ConcurrentHashMap等容器的方式一样,利用当前key的hashcode与哈希表大小相与,因为哈希表大小总是为2的幂次方,所以相与等同于一个取模的过程,这样就可以通过key分配到具体的哈希通中去。
- 怎样解决"脏"Entry? 在set方法的for循环中寻找和当前key相同的可覆盖entry的过程中通过replaceStaleEntry方法解决脏entry的问题。
- 如何进行扩容?
4.ThreadLocal的使用场景
ThreadLocal不是用来解决共享对象的多线程访问问题的,数据实质上是放在每个thread实例引用的threadLocalMap,也就是说每个不同的线程都拥有专属于自己的数据容器(threadLocalMap),彼此不影响。因此threadLocal只适用于共享对象会造成线程安全的业务场景。比如hibernate中通过threadLocal管理Session就是一个典型的案例,不同的请求线程拥有自己的session,若将session共享出去被多线程访问,必然会带来线程安全问题。
5.为什么使用弱引用?
threadLocal存在内存泄漏的问题似乎是因为threadLocal是被弱引用修饰的。那为什么要使用弱引用呢?
- 如果使用弱引用 假设threadLocal使用的是强引用,在业务代码中执行threadLocalInstance==null操作,以清理掉threadLocal实例的目的,但是因为threadLocalMap的Entry强引用threadLocal,因此在gc的时候进行可达性分析,threadLocal依然可达,对threadLocal并不会进行垃圾回收,这样就无法真正达到业务逻辑的目的,出现逻辑错误。
- 如果使用弱引用 假设Entry弱引用threadLocal,尽管会出现内存泄露的问题,但是在threadLocal的生命周期里,都会针对key为null的脏entry进行处理。 从以上的分析可以看出,使用弱引用的话在threadLocal生命周期里会尽可能的保证不出现内存泄露的问题,达到安全的状态。
6. threadLocal最佳实践
1.每次使用完ThreadLocal,都调用它的remove()方法,清除数据。 2.在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄露的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。