本系列主要深入解析Java并发框架的内容,这是第二部分的内容,分析java并发中如何合理使用工具ThreadLocal,简化并发逻辑。
1. ThreadLocal的使用场景
- 工具类不是线程安全的,我们让每个线程都有一个独立的工具类。
- 避免参数传递的麻烦。
- 新建:
ThreadLocal<Intger> threadLocal = new ThreadLocal<>();
场景1.每个线程需要一个独享的对象,常用在工具类中
当多个线程多次使用同一个线程不安全的对象时,会发生线程安全问题。可以使用线程池+synchronized的方法,但这种方法的性能差。
采用ThreadLocal方法。 创建一个安全类
SafeThreadLocal
,在这类中新建一个静态的ThreadLocal<> = new ThreadLocal<>();
对象。 这样所有的线程都可以调用。 这个对象需要重写initialValue()
方法,也就是每个ThreadLocal都可以初始化生成一个属于线程的dataformat
实例。对10个线程都创建一个dateFormat对象。每一个dateFormat的是线程安全的,线程在调用时,使用类.实例名.get()
得到属于每类自己的dataformat
实例。
场景2. 每个线程内保存一个内部变量,不同的方法可以直接使用,常用于对一个对象进入多个方法。
比如多个用户请求,每个线程都对应了不同的用户信息,我们希望在一个线程内维护一个变量,在调用方法的时候是通用的。可能的做法包括采用ConucurrentHashMap等。但是这种方法会导致性能降低。
采用
ThreadLocal
可以不影响性能,且无需层层传递参数。在线程周期内,通过静态的ThreadLocal
实例的get()
方法取得自己set()
的对象,避免了传参的麻烦。实现统一个线程内不同方法之间的共享。在定义类中定义静态的
ThreadLocal
,不需要重写initialValue()
方法。在线程中手动调用类名.静态实例名.set()
**,在同一个线程的其他程序调用时,直接类名.静态实例名.get()
.**
2. ThreadLocal的优点
- 可以实现线程安全。
- 不需要加锁,提高了实行效率。
- 更高效的利用内存,节省开销。只需要建立与线程池中线程数量相同的实例,节省内存。
- 避免的传参的繁琐。在同一个线程中可以共享变量。
3. 源码分析
在set
和get
时,我们首先获得当前线程的对象,然后getMap
方法得到线程的ThreadLocalMap
,这个Map里面保存了这个线程的全部local。然后进行类似与Hashmap的操作。这个ThreadLocalMap是写在Thread类内部的成员。
1 | public T get() { |
简单来说就是每一个Thread
都对应了一个ThreadLocalMap
,而每一个ThreadLocalMap
里面可以存在多个ThreadLocal
。这样是为了在一个线程内可以读取多个变量。也就是说,我们可以新建很多个ThreadLocal。
重要方法解析
initialValue()
: 返回线程对应的初始值,延迟加载,只有在调用get时候才会触发。如果之前set
了值,就不需要了。每个线程调用最多一次就可以。如果不重写,就会返回null。set(T t)
: 为这个线程设置一个新值get()
: 得到这个线程对应的value。remove()
: 删除线程的值。清空当前线程的ThreadLocal。
ThreadLocalMap类
也就是Thread.threadLocals。核心是一个键值对数组Entry[] table
- 处理哈希冲突:冲突是开放寻址法。
- 传统哈希:初始时拉链法,后续变为红黑树。
4. ThreadLocal注意点
内存泄露:某个对象不在使用,但占用的内存无法被回收。
- key 是一个弱引用,是可以被垃圾回收的。
- value是一个强引用。如果线程结束,线程是被回收的。但是如果使用线程池,线程保持,因此value无法被回收,引发内存泄漏。JDK已经考虑了,在调用
set
,remove
,rehash
方法时候会把key是null的的value也设置为null。但是一旦ThreadLocal不用了,一般也很难去调用remove,容易造成泄露。 - 避免方法:使用完ThreadLocal,主动调用remove。也就是说,在最后一个方法调用完ThreadLocal,需要主动remove,避免内存泄漏。
共享对象
注意不要存入一个static对象,否则还是会导致线程安全的问题。