参考尚硅谷周阳《JUC并发编程》
是什么
ThreadLocal提供线程局部变量,这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类的私有静态字段,使用它的目的是希望将状态(例如,用户ID,或事务ID)与线程关联起来。
能干嘛
实现每一个都有自己专属的本地变量副本。
解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或者将其值更改为当前线程所存储的副本的值,从而避免了线程安全问题。
使用案例
class House {
int saleCount = 0;
public synchronized void saleHouse() {
saleCount++;
}
/**
* 调用withInitial就是设置一个初始值,如果这里不设置初始值,就是null,那么下面的 saleVolume.set(1 + saleVolume.get()); 就会报空指针。
*/
static ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
public void saleVolumeByThreadLocal() {
// get和set方法只会操作当前线程存储在saleVolume里面独立的那一份对象,实现了对象的隔离,保证线程的安全。
saleVolume.set(1 + saleVolume.get());
}
}
/**
*
* 需求1:5个销售卖房子,集团高层只关心销售总量的准确统计数。
*
* 需求2:5个销售卖完随机数房子,各自独立销售额度
*
*/
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException{
House house = new House();
for (int i = 0; i < 5; i++) {
new Thread(()->{
int size = new Random().nextInt(5)+1;
for (int i1 = 0; i1 < size; i1++) {
house.saleHouse();
house.saleVolumeByThreadLocal();
}
System.out.println(ThreadUtils.currentThreadName() + "\t" + "号卖出:" + House.saleVolume.get() + "套");
},String.valueOf(i)).start();
}
ThreadUtils.sleep(300);
System.out.println(ThreadUtils.currentThreadName() + "\t" + house.saleCount);
}
}
set和get的执行流程
set方法的执行流程,get方法的执行流程也类似,每个线程对象持有自己的ThreadLocalMap用来保存自己的副本。
为什么ThreadLocal类内部的ThreadLocalMap要用Entry数组实现
ThreadLocal底层为什么不能仅使用一个Entry来实现,或者说仅使用一个object来实现,原因很简单。
这是为了适用于一个线程中存在多个ThreadLocal的情况
比如一个线程定义了很多个ThreadLocal
ThreadLocal<String> t1 = new ThreadLocal<>();
ThreadLocal<String> t2 = new ThreadLocal<>();
ThreadLocal<String> t3 = new ThreadLocal<>();
set方法在进行设置值的时候,是通过getMap方法来获取到线程自身的ThreadLocal.ThreadLocalMap对象,getMap传入的是当前的线程对象,多个ThreadLocal同属于这个对象,也即无论一个线程里面定义了多少个ThreadLocal,无论这些ThreadLocal要怎样去设置自己的值,都会访问到这个线程对象自身里面的threadLocals属性,以此来设置。
而set方法就是传入每一个ThreadLocal对象,t1就传入t1,t2就传入t2,然后通过哈希码&entry数组长度来找到自身需要设置的值。
这样就可以适用于多个ThreadLocal对象同时设置值,保证线程安全的情况了。
ThreadLocal的内存泄露问题
entry的key为什么会被定义成弱引用?
什么时候会移除掉key为空,但是value不为空的entry?
set()、get()、remove()都会进行移除掉这类的entry
-
set(T value) -> map.set(this, value) -> replaceStaleEntry(key, value, i)
-
get() -> map.getEntry(this) -> getEntryAfterMiss(key, i, e) -> expungeStaleEntry(i)
-
remove() -> m.remove(this) -> expungeStaleEntry(i)
以上并不能完全解决ThreadLocal的内存泄露问题
因为ThreadLocal能实现了线程的数据隔离,不在于他自己本身,而在于thread的ThreadLocalMap,所以,ThreadLocal一般初始化一次,只分配一块存储空间,就是定义成static类型,这种类型的都是强引用的。
对于这种类型的ThreadLocal只能在业务不再需要的时候调用remove方法。
try {
threadLocal.set("");
// TODO:
// TODO:
threadLocal.get();
}finally {
threadLocal.remove();
}
注意事项:
在线程池当中尤其要注意到一个线程的逻辑运行完成之后,去执行remove方法,避免线程复用带来的逻辑错误。
小总结:
- ThreadLocal能实现了线程的数据隔离,不在于他自己本身,而在于thread的ThreadLocalMap,所以,ThreadLocal可以只初始化一次,只分配一块存储空间就足以了,没必要作为成员变量多次被初始化。
- ThreadLocal并不解决线程间共享数据的问题
- ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
- ThreadLocal通过隐式的不同线程内创建独立实例副本避免了实例线程安全的问题
- 每个线程持有一个只属于自己的专属map并维护了ThreadLocal对象与具体实例的映射,该map由于只被持有他的线程访问,故不存在线程安全以及锁的问题。
- ThreadLocalMap的entry对ThreadLocal为弱引用,避免了ThreadLocal对象无法被回收的问题
- 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为null的entry对象的值(即为具体实例)以及entry本身从而防止内存泄漏,属于安全加固的方法
- 群雄逐鹿起纷争,人各一份安天下