ThreadLocal以及ThreadLocalMap详解

ThreadLocal是以线程隔离为核心,既能解决线程并发下的数据安全问题,又能简化线程内的传递数据的操作。

三个关键词:

线程隔离:TheadLocal为每个线程内部维护了一个独立专属的储存容器,线程间数据互不访问,互不干扰,实现数据私有隔离,保证了数据安全性。

线程并发:在多线程的情况下,共享变量容易出现数据错乱,ThreadLocal给每个线程分配独立的数据副本,无需加锁就能解决并发的安全问题。

传递数据:ThreadLocal存一次数据,当前线程内任意地方可以直接获取,简化了数据的传递逻辑和耦合度。无需逐层传参(Controller->Service->DAO)

为什么要使用ThreadLocal?

场景:在多线程中需要操作共享变量,容易出现数据错乱。

static String name=null;
    static int age=0;
public static void main(String[] args) {
    Thread thread = new Thread(() -> {
        try {
            name="张三";
            age=21;
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("线程1 "+name+" "+age+"岁");
    });

    Thread thread2 = new Thread(() -> {
        try {
            name="李四";
            age=22;
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("线程2 "+name+" "+age+"岁");
    });
    thread2.start();
    thread.start();
}

比如上面这个代码,多个线程操作共享变量。

想要的运行结果:

线程1 张三 21岁

线程2 李四 22岁

实际上:

出现了数据错乱。

我们可以通过加锁解决:但是加锁的话,会导致其他问题,比如并发量降低,性能因为是同步执行,性能也会下降。

 Thread thread = new Thread(() -> {
        synchronized (threadLocal1) {
            try {
                name="张三";
                age=21;
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("线程1 "+name+" "+age+"岁");
        }
    });

    Thread thread2 = new Thread(() -> {
       synchronized (threadLocal1) {
           try {
               name="李四";
               age=22;
               Thread.sleep(1000);
           } catch (InterruptedException e) {
               throw new RuntimeException(e);
           }
           System.out.println("线程2 "+name+" "+age+"岁");
       }
    });

但是ThreadLocal的出现,很好的解决了这样的问题。

public class Demo {
    String name;
    int age;
    public Demo(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public int getAge() {
        return age;
    }
}


public class Main {
    static ThreadLocal<Demo> threadLocal1 = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            threadLocal1.set(new Demo("张三", 21));
            Demo demo = threadLocal1.get();
            System.out.println("线程1 " + demo.getAge() + "岁 " + demo.getName());
        });

        Thread thread2 = new Thread(() -> {
            threadLocal1.set(new Demo("李四", 22));
            Demo demo = threadLocal1.get();
            System.out.println("线程2 " + demo.getAge() + "岁 " + demo.getName());
        });
        thread.start();
        thread2.start();
    }
}

这样就能很好的解决了数据错乱的问题。

synchronized和ThreadLocal的区别:

synchronized的使用,基于共享数据加锁,让多线程排队访问共享资源。解决线程间的数据竞争问题。是用时间换空间,而且使用了synchronized会使性能下降(串行执行),因为同步执行代码。并发性降低。

ThreadLocal的使用,基于数据副本隔离,给每个线程分配独立的数据副本。避免线程间共享资源。是用空间换时间,使用ThreadLocal是不会影响并发性的。

ThreadLocal内部结构:

每个线程在首次调用了ThreadLocal的set/get方法后才会创建ThreadLocalMap,随线程的生命周期存在。

ThreadLocalMap中,会以当前的ThreadLocal实例作为key,以及与之对应的value,放入其中。

ThreadLocal的核心方法源码:

1.set(数据):

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

set设置当前线程绑定的局部变量。

1.首先先获取当前线程,并根据当前线程获取一个Map

2.如果Map不为空,则将参数设置到Map中(key=ThreadLocal实例本身)

3.如果Map为空,就会为当前线程重新创建一个Map,并设置初始值

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

2.get(数据):

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

get是获取当前线程绑定的局部变量。

1.首先获取到当前线程,然后根据线程获取到Map

2.如果Map不为空,则根据ThreadLocal作为引用来获取Entry e,否则就会转到第四步

3.若e不存在,就会执行第四步

4.通过setInitialValue()方法,函数获取初始值,然后用ThreadLocal引用和对应value来创建新的Map

 private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

3.remove(数据):

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }

remove方法是移除当前线程所绑定的局部变量。

1.首先根据当前线程,获取到Map

2.如果Map不为空,则直接移除。

ThreadLocalMap:

ThreadLocalMap是ThreadLocal的底层存储容器,也是ThreadLocal的内部类。专属线程私有,核心作用是隔离线程间的数据

本质是一个弱化版的哈希表,规避线程安全问题的同时提升储存效率。

ThreadLocalMap源码分析:

在代码中,有几个很重要的变量:

储存结构-Entry:

在ThreadLocalMap中,也是用Entry来保存K-V结构的数据,不过Entry中的key只能用ThreadLocal对象。

另外,Entry继承了继承了WeakReference,也就是key(ThreadLocal)是弱引用,目的是将ThreadLocal对象的生命周期和线程的生命周期解绑。

弱引用和内存泄漏:

有的人认为,内存泄漏的原因是因为使用了弱引用,这是不正确的。

下面我分别从强引用和弱引用来阐述产生内存泄漏的原因:

1.使用强引用:

如图:

1.假设业务代码在使用完了TheaLocal,然后ThreadLocal Ref被回收了。

2.但是因为ThreadLocalMap的Entry强引用了ThreadLocal,造成ThreadLocal无法被回收。

3.在没有手动删除这个Entry以及CurrenThread依然运行状态下(线程池场景下),始终就会有强引用链Thread Ref->currentThread->threadLocalMap->entry,entry不会被回收(entry包括了ThreadLocal实例和value),导致内存泄漏。

2.使用弱引用:

1.当业务代码中使用完ThreadLocal Ref,ThreadLocal Ref就会被GC回收。

2.由于ThreadLocalMap中的entry只持有ThreadLocal当弱引用,所以ThreadLocal也会被GC回收,所以此时Entry中的key就会变成null。

3.但是如果没有手动删除这个Entry以及CurrentThread依然运行状态下(线程池场景),也存在强引用链:CurrentThread Ref->CurrentThread->ThreadLocalMap->entry->value,value不会被回收,而这块地址也找不到了,导致value内存泄漏。

ThreadLocalMap核心方法set:

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)]) {
                if (e.refersTo(key)) {
                    e.value = value;
                    return;
                }

                if (e.refersTo(null)) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

首先ThreadLocalMap是用线性探测法,而且数组还是环形的,保证哈希冲突继续寻找。

ThreadLocal采用弱引用,避免内存泄漏(会被GC回收)。

核心逻辑:插入 / 替换值时,先通过哈希定位初始索引,线性探测遍历;匹配到目标 Entry 则替换值,遇到过期 Entry 则清理并插入(避免弱引用导致的内存泄漏,且没有remove的兜底操作),遍历到空位置则直接插入。

插入后主动清理部分过期 Entry,负载因子过高时扩容,全量清理过期 Entry。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值