Java之ThreadLocal

一、ThreadLocal是什么

从名称可以看出,ThreadLoacl叫做本地线程变量,意思是说,ThreadLocal中填充的是当前线程的变量,该变量对其他线程而言是封闭且隔离的。

官方所给出的定义是:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证个线程的变量相对独立于其他线程内的变量。

好处:
对于单条线程:保存每个线程绑定的数据,在需要的地方可以直接获取,避免了各层参数带来的代码耦合问题。可以使该线程在下游获取上游所绑定的数据。
对于多条线程间:各个线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。但这本质上是一种空间换时间的思想。

在这里插入图片描述

二、ThreadLocal的用法

ThreadLocal中常用的方法有set()、get()、remove()方法。

  • set()方法可以为当前线程绑定一个值
  • get()方法可以获取此threadlocal为当前线程所绑定的值
  • remove()方法可以移除此threadlocal为当前线程所绑定的值,避免内存泄漏

首先我们明确一点,ThreadLocal中之所以可以绑定值是因为Thread类中存在一个threadLocals成员变量

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

该成员变量的类型是ThreadLocal.ThreadLocalMap,简单说就是一个map,这个map容器以ThreadLocal为键,以Object为值,这个值就是我们所绑定在该thread中的值。

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

关于ThreadLocal类中的set()方法。

public void set(T value) {
		//1、获取当前线程的threadLocals
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //2、以此threadLocal为键(this),以我们绑定的value为值,构建Entry放入map中。
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

关于ThreadLocal中的get()方法

public T get() {
		//1、获取当前线程的threadLocals
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //2、以此threadLocal为键获取绑定的值
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

我们可以看到

  1. thread中可以通过多个threadlocal绑定多个值
  2. 一个threadlocal在单个线程中只能映射一个值
  3. 同一个threadlocal对象在不同线程中所get到的值不同,原因在于不同线程中的threadLocals不同

关于ThreadLocal中的remove()方法

public void remove() {
		 //1、获取当前线程的threadLocals
         ThreadLocalMap m = getMap(Thread.currentThread());
         //2、如果map非null,移除
         if (m != null) {
             m.remove(this);
         }
     }

1、避免传参,跨层取值

见代码:

package com.cup.syd.threadLocal;

public class ThreadLocalDemo {

    private static ThreadLocal threadLocal = new ThreadLocal();
    
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {
            //上游为此线程绑定一个字符串
            threadLocal.set("value");
            nextFun();
        }, "线程1");
        t1.start();
        
        t1.join();
    }

    private static void nextFun() {
        //下游获取值
        Object o = threadLocal.get();
        System.out.println(Thread.currentThread().getName() + ": " + o.toString());
    }
}

输出结果:
在这里插入图片描述

通过代码我们可以看到,我们通过方法java.lang.ThreadLocal#set为该线程(线程1)绑定了一个值,在未传参的情况下,通过java.lang.ThreadLocal#get方法将值从下游取出。这种方式做到了层级间的解耦。
spring的注入功能也能实现这种解构的效果,但spring创建的单例bean生命周期是伴随着整个应用程序的,这种方式创建出来的对象生命周期是单条线程。

2、数据隔离,具有并发性

首先明确一点,本人认为,ThreadLocal并不适合直接处理共享变量。

部分博客认为,ThreadLocal会为共享变量拷贝一份副本,并保存在线程内部,则在接下来的使用过程中通过访问本线程内部的变量副本来实现并发。这种理解存在一些问题。

首先、ThreadLocal的拷贝只是浅拷贝,对于Java中的对象来说,线程存储的只是该对象的引用。其他线程仍然可以对该对象的值进行修改。
其次,ThreadLocal的set方法并非原子性的。若该线程在set的过程中发生中断,其他线程仍然可以在此期间修改共享变量的值。

通常,ThreadLocal可以用于保存线程不安全的类,典型的需要使用的类就是 SimpleDateFormat。

public class ThreadLocalDemo {
    public static void main(String[] args) throws ParseException {
        /**
         * 注意点,在创建ThreadLocal对象时 如果赋值只是如下这种方式赋值,那么只有当前线程
         * 调用threadLocal.get()能够获取到,其它线程一律是空!!!!
         * ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal();
         * threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
         */
        ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(()->{
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        });
        for (int i = 0; i <10 ; i++) {
            int finalI = i;
            new Thread(()->{
                SimpleDateFormat simpleDateFormat = threadLocal.get();
                String format1 = simpleDateFormat.format(new Date(finalI *1000));
                System.out.println(Thread.currentThread().getName()+"=="+format1);
            },"T"+i).start();
        }
    }
}

当然,对于SimpleDateFormat多线程下不安全的问题还有其他很多解决方案,如使用DateTimeFormat、synchronize锁等方法。此处只是为了论证ThreadLocal的并发性。

3、总结

个人认为ThreadLocal适用于:
(竖线代表单条线程中的数据流)

各线程之间的数据平行无交集,ThreadLocal的使用可以使上下层解耦。对于单条线程各处使用较为频繁的对象,可以使其和线程直接绑定到一起。
在这里插入图片描述
各线程所操作的起始数据无交集,但线程结束后可进行数据整合。

不直接适用于:
在这里插入图片描述
各线程需要对共享变量进行操作,因为ThreadLocal的set方法不是原子的。而且是浅拷贝。

当然,事情不是绝对的,如果在多线程下通过加锁排队(synchronize)的方式太多消耗时间,未尝不可以考虑ThreadLocal的方式。但需要用锁来保证set方法的原子性,还要注意浅拷贝的问题。

从并发的角度上来讲,synchronize和ThreadLocal代表着两种不同的处理该问题的思维方式,一种是排队—— synchronize加锁使得各个线程中临界区的代码依次执行。另一种就是拷贝出副本,各个线程对副本进行操作,但是需要注意副本拷贝过程的原子性。

一种有趣的写法:

package com.cup.syd.threadLocal;

public class ShareValue {

    private ThreadLocal local = new ThreadLocal<>();

    private Object value;

    public Object getThreadValue() {
        return local.get();
    }

    public void setThreadValue(Object threadValue) {
        local.set(threadValue);
    }

    public Object getValue() {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }
}

ShareValue类中存在一个threadValue属性(虽然没有对应的成员变量),同一个对象的情况下,在不同线程中threadValue的值是不一样的。(不知道有什么适用场景,此处先记录一下),这种写法有些鸡肋,鸡肋的地方在于该类中的属性需要在对应线程中设置并获取,完全可以用线程内变量替换掉。但这确是一种数据的组织形式,符合结构化设计的思想。

三、ThreadLocal中的弱引用和内存泄漏问题

我们来看,在ThreadLocal.ThreadLocalMap中,其中Entry的key被设计成一种弱引用。

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

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

为什么单独把Entry中的key设计成为弱引用呢,我们来看下面这张图。
在这里插入图片描述
这是当我们在使用threadlocal时,栈和堆中,相关的引用关系。

我们发现,当我们不再使用threadlocal变量时,仍然有一条强引用的路径指向entry。
在这里插入图片描述

并且在此线程结束前,这条强引用路径不会消失,这无疑会导致内存泄漏的问题。

为了弥补这个问题,ThreadLocal的作者就把entry中的key设计成弱引用的形式,一旦栈中的ref-threadlocal强引用消失,在下一次gc时就会清除堆中的threadlocal对象。此时,该entry的key值为null。此外,ThreadLocal还有些额外的逻辑清除整个entry的指针。其通过判断entry的key值是否为null来判断该entry是否过时。
见下代码

private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null; //清除value的指针
            tab[staleSlot] = null;//清除该entry的指针
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            //遍历检查清除
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

ThreadLocal中的set()和getEntry()方法均会走到此方法。即使是在threadlocal1中设置的值,threadlocal2调用set()和getEntry()方法后也能够清除到,因为他们共用的一个thread中的threadlocals。

但这种方式只是一种保障,他仍然会造成一段时间的内存泄漏问题,正确的方式是在使用完ThreadLocal后直接remove掉此Entry。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值