并发编程之ThreadLocal使用及原理

ThreadLocal主要是为了解决线程安全性问题的

非线程安全举例

public class ThreadLocalDemo {

    // 非线程安全的
    private static final SimpleDateFormat sdf= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    public static Date parse(String strDate) throws ParseException {
        return sdf.parse(strDate);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 20; i++) {
            executorService.execute(() -> {
                try {
                    System.out.println(parse("2021-05-30 20:21:20"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

以上代码,构造了一个SimpleDateFormat对象,然后在main线程中,开启了20个线程执行时间的格式化,其输出结构部分如下:

可以看到,程序有部分线程打印出了结果,但结果也都不一样,并且也有部分报了异常。

分析:这是由于SimpleDateFormat在这里设置的是一个全局变量,多个线程共用的时候,必然涉及到共享资源的抢占,其parse方法内部对字符串的处理的操作就是非原子性的,因此就会出现真正执行的时候,拿到的最终字符串无法确定,导致以上报错。

思考: 如何修改? 使用ThreadLocal将DateFormat设置为线程安全的,保证每个线程的操作都是原子的。

ThreadLocal应用

public class ThreadLocalDemo {


    private static final ThreadLocal<DateFormat> dateFormatThreadLocal = new ThreadLocal<>();
    public static Date parse(String strDate) throws ParseException {
        DateFormat dateFormat = dateFormatThreadLocal.get();
        if (dateFormat == null ) {
            dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            dateFormatThreadLocal.set(dateFormat);
        }
        return dateFormat.parse(strDate);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 20; i++) {
            executorService.execute(() -> {
                try {
                    System.out.println(parse("2024-04-12 15:12:20"));
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

结果输出

ThreadLocal常用方法

set()

在当前线程范围内,设置一个值存储在ThreadLocal中,这个值仅对当前线程可见

相当于在当前线程范围内建立了副本

get()

从当前线程范围内取出set()方法设置的值

remove()

移除当前线程范围内的值

ThreadLocal原理猜想

1. 能够实现线程的隔离,当前保存的数据,只会存储在当前的线程范围内->线程内私有的

2.有一个存储结构

3.key->保存当前线程

ThreadLocal源码分析

1.初始化ThreadLocalMap

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

作为一个静态内部类,在类加载时,就会加载该线程的一个ThreadLocalMap.Entry

注意:在这里Entry采用的是一个弱引用对象,为什么要采用弱引用对象呢?这是由于ThreadMap是和线程绑在一起的,如果这个线程没有被销毁,而我们又已经不会在使用ThreadLocal引用了,那么key-value的键值对就会一直在map中存在,这对于程序来说,就出现了内存泄漏。为了避免这种情况,只要将Key设置为弱引用,那么当发生GC的时候,就会自动的把弱引用给清理掉了。

2. set主逻辑源码

   public void set(T value) {
        // 1. 获得当前线程t
        Thread t = Thread.currentThread();
        // 2. 取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 2.1 如果map不为空, 则设置当前ThreadLocal变量的value值
            map.set(this, value);
        } else {
            // 2.2 若map为空,则创建一个ThreadLocalMap
            createMap(t, value);
        }
    }

1.2 当前线程的map不为空时,如何进行set值

    private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            // 2.1.1 根据当前key获得索引值
            int i = key.threadLocalHashCode & (len-1);

            // 2.1.2 循环entry数组
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                // 2.1.3 entry不为空 获得当前entry数组元素的key
                ThreadLocal<?> k = e.get();
                // 2.1.4 若相等,则赋值value
                if (k == key) {
                    e.value = value;
                    return;
                }
                // 2.1.5若当前entry中为空 则进行替换空余的数组

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
             // 2.1.6 这里主要是对Entry数组的一个扩容知识 
                rehash();
        }

分析:当前entry为空(key值可能被GC回收了),那么该条数据就可能为脏数据,脏Entry,只有value有值 key为null  就执行replaceStaleEntry方法(注:2.1.6在这里不再展开,主要是Entry数组的一个扩容)

     private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;
            // a.当前key的索引
            int slotToExpunge = staleSlot;
            // b. 向前查询脏Entry  
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;
            // c. 向后查找可覆盖的Entry
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    // d.进行交换 避免Entry中数据重复
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    // e.从前往后清理脏Entry
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }
            
            // f. 没有找到可覆盖的Entry,则清理当前索引的value重新赋值
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);
            // h.清理查询到的脏Entry
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

该段代码其实会分为4种情况

a. 向前查找有脏Entry 向后查找到可覆盖的Entry(这里是由于存储的时候会有哈希冲突,因此向后可能会有相同的key值)

b.向前有脏Entry向后未找到可覆盖的Entry(则直接在当前索引位置直接赋值新的Entry)

c.向前没有脏Entry向后找到可覆盖的Entry

d.向前没有脏Entry向后未找到可覆盖的Entry

分析:上述set值,一定程度上避免了Entry数组的内存泄漏,因为可以向前检索到脏Entry并进行清理,但是如果向前查找提前停了下来,那么前面仍还有脏Entry未扫描到,那么仍会有部分内存泄漏。真正全部清理需要每次使用后调用remove方法

1.3 当前线程的Map为空时,进行创建并set值

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
       ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 2.2.1 初始化一个16长度大小的数组
            table = new Entry[INITIAL_CAPACITY];
            // 2.2.2 设置下标索引 采用的是线性探测法
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 2.2.3 将Entry设置到该数组索引处
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            // 2.2.4 设置阈值为10
            setThreshold(INITIAL_CAPACITY);
        }

设置索引的地方主要是采用线性探测法来解决哈希冲突,找到一篇不错的博客,感兴趣可以参考博客:ThreadLocalMap线性探测法解决hash冲突_thread t = thread.currentthread(); threadlocalmap -CSDN博客

3. get主逻辑源码

    public T get() {
        Thread t = Thread.currentThread();
       // 1.首先获得当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 2.拿到Entry如何Entry不为空的情况下 直接返回值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 3. 如果弱引用key被回收了,则会重新创建当前线程的Entry,并赋值
        return setInitialValue();
    }

4.remove主逻辑源码

     private void remove(ThreadLocal<?> key) {
            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.get() == key) {
                   // 遍历当前Entry数组,全部清理掉
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

ThreadLocal总结

1.ThreadLocal主要是为了线程安全,避免多线程的资源共享,线程间的资源互相隔离

2..ThreadLocal的注意点: ThreadLocal可能会造成内存泄漏,因此在每次使用完后,调用remove进行清理

3.为什么ThreadLocal的key值是弱应用,而value值是强引用? 在ThreadLocalMap初始化时已经说明了key值为什么要采用弱引用,那么value值为什么不能设置为弱引用呢。假设Entry的key所引用的ThreadLocal对象还被其他的引用对象强引用着,那么这个ThreadLocal对象就不会被GC回收,但如果value是弱引用且不被其他引用对象引用着,那么GC的时候就会被回收掉了。那线程通过ThreadLocal获取value的时候就会获得null,ThreadLocal显然就是用来关联value的,value才是我们要保存的值,如果value都没了,还用ThreadLocal干嘛。所以value不能是弱引用

  • 28
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值