Loading

JUC笔记(二)ThreadLocal

ThreadLocal

ThreadLocal是什么

感谢:

(1 封私信) Java中ThreadLocal的实际用途是啥? - 知乎 (zhihu.com)

ThreadLocal使用与原理_敖 丙的博客-CSDN博客_threadlocal使用

ThreadLocal的介绍+经典应用场景 - 掘金 (juejin.cn)十分推荐阅读,思路清晰。

ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

在JDK 1.2的版本中就提供java.lang.ThreadLocalThreadLocal解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

ThreadLocal并不是一个Thread,而是Thread局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)T get()以及T initialValue()

在处理多线程并发安全的方法中,最常用的方法,就是使用锁,通过锁来控制多个不同线程对临界区的访问。但是,无论是什么样的锁,乐观锁或者悲观锁,都会在并发冲突的时候对性能产生一定的影响。那有没有一种方法,可以彻底避免竞争呢?

答案是肯定的,这就是ThreadLocal

从字面意思上看,ThreadLocal可以解释成线程的局部变量,也就是说一个ThreadLocal的变量只有当前自身线程可以访问,别的线程都访问不了,那么自然就避免了线程竞争。

ThreadLocal类用来提供线程内部的局部变量。这种局部变量在多线程的环境下访问(通过getset方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static 类型的,用于关联线程和线程上下文。

因此,ThreadLocal提供了一种与众不同的线程安全方式,它不是在发生线程冲突时想办法解决冲突,而是彻底的避免了冲突的发生。

image-20220319132337461

image-20220319132701247

总结

ThreadLocal又叫做线程局部变量,全称thread local variable,它的使用场合主要是为了解决多线程中因为数据并发产生不一致的问题ThreadLocal每一个线程都提供了变量的副本,使得每一个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享,这样的结果无非是耗费了内存,也大大减少了线程同步所带来的性能消耗,也减少了线程并发控制的复杂度。

总的来说:ThreadLocal适用于每一个线程需要自己独立实例,而且实例的话需要在多个方法里被使用到,也就是变量在线程之间是隔离的,但是在方法或者是类里面是共享的场景。

ThreadLocal基本使用

public class Main {
  	//创建一个`ThreadLocal`对象:
    //private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    //因为ThreadLocal里面设置的值,只有当前线程可见,也就以为着你不可能通过其他线程为它初始化值
    //为了弥补这一点,ThreadLocal提供了一个withInitial()方法统一初始化所有线程的ThreadLocal的值:
    //ThreadLocal的初始值设置为6,这对全体线程都是可见的
    //private ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> 6);

    public static void main(String[] args) {
				ThreadLocal<String> threadLocal = new ThreadLocal<>();

        IntStream.range(0, 10).forEach(i -> new Thread(() -> {
            threadLocal.set(Thread.currentThread().getName() + ":" + i);
            System.out.println("线程:" + Thread.currentThread().getName() + ",local:" + threadLocal.get());
        }).start());
    }

    //public int sendAndGet() {
        //threadLocal.set(8);
        //return threadLocal.get();
    //}
}

image-20220319134510189

每一个线程都有自己的local 值,这就是TheadLocal的基本使用 。

package org.uin.ThreadLocal;

/**
 * @author wanglufei
 * @description: TODO
 * @date 2022/3/19/2:41 PM
 */
public class ThreadLocalTest {

    private static ThreadLocal<String> local = new ThreadLocal<String>();

    static void print(String str) {
        //打印当前线程中本地内存中变量的值
        System.out.println(str + " :" + local.get());
        //清除内存中的本地变量
        local.remove();
    }
    public static void main(String[] args) throws InterruptedException {

        new Thread(new Runnable() {
            public void run() {
                ThreadLocalTest.local.set("BearBrick0");
                print("A");
                //打印本地变量
                System.out.println("清除后:" + local.get());
            }
        },"A").start();
        Thread.sleep(1000);

        new Thread(new Runnable() {
            public void run() {
                ThreadLocalTest.local.set("BearBrick0");
                print("B");
                System.out.println("清除后 " + local.get());
            }
        },"B").start();
    }
}

image-20220319145024383

运行后可以看到BearBrick0的值为null,BearBrick0的值也为null,表明了两个线程都分别获取了自己线程存放的变量,他们之间获取到的变量不会错乱。

ThreadLocal的实现原理

get()

ThreadLocal变量只在单个线程内可见,那它是如何做到的呢?我们先从最基本的get()方法说起:

public T get() {
    //获得当前线程
    Thread t = Thread.currentThread();
    //每个线程 都有一个自己的ThreadLocalMap,
    //ThreadLocalMap里就保存着所有的ThreadLocal变量
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //ThreadLocalMap的key就是当前ThreadLocal对象实例,
        //多个ThreadLocal变量都是放在这个map中的
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            //从map里取出来的值就是我们需要的这个ThreadLocal变量
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map没有初始化,那么在这里初始化一下
    return setInitialValue();
}

可以看到,所谓的ThreadLocal变量就是保存在每个线程的map中的。这个map就是Thread对象中的threadLocals字段。如下:

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMap是一个比较特殊的Map,它的每个Entrykey都是一个弱引用:

image-20220319133117063 image-20220319135736225
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    //key就是一个弱引用
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

通过上面的分析,相信你对该方法已经有所理解了,首先获取当前线程,然后通过key threadlocal 获取 设置的value

这样设计的好处是,如果这个变量不再被其他对象使用时,GC可以自动回收这个ThreadLocal对象,避免可能的内存泄露(注意,Entry中的value,依然是强引用,如何回收,见下文分解)。

set()

/**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        //首先获取当前线程对象
        Thread t = Thread.currentThread();
        //获取线程中变量 ThreadLocal.ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //如果不为空,
        if (map != null)
            map.set(this, value);
        else
            //如果为空,初始化该线程对象的map变量,其中key 为当前的threadlocal 变量
            createMap(t, value);
    }

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
		//初始化线程内部变量 threadLocals ,key 为当前 threadlocal
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

       /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

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

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

汇总下,ThreadLocalMapThreadLocal 的一个静态内部类,里面定义了Entry 来保存数据。而且是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value

对于每个线程内部有个ThreadLocal.ThreadLocalMap 变量,存取值的时候,也是从这个容器中来获取。

在使用完ThreadLocal,推荐要调用一下remove()方法,这样会防止内存溢出这种情况的发生,因为ThreadLocal为弱引用。如果ThreadLocal在没有被外部强引用的情况下,在垃圾回收的时候是会被清理掉的,如果是强引用那就不会被清理。

总结

每一个 Thread 对象均含有一个 ThreadLocalMap 类型的成员变量 threadLocals ,它存储本线程中所有ThreadLocal对象及其对应的值。

ThreadLocalMap 由一个个 Entry 对象构成Entry 继承自 WeakReference<ThreadLocal<?>> ,一个 EntryThreadLocal 对象和 Object 构成。

由此可见, EntrykeyThreadLocal对象,并且是一个弱引用。当没指向key的强引用后,该key就会被垃圾收集器回收。

当执行set方法时,ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,将值存储进ThreadLocalMap对象中。

get方法执行过程类似。ThreadLocal首先会获取当前线程对象,然后获取当前线程的ThreadLocalMap对象。再以当前ThreadLocal对象为key,获取对应的value。

由于每一条线程均含有各自私有的ThreadLocalMap容器,这些容器相互独立互不影响,因此不会存在线程安全性问题,从而也无需使用同步机制来保证多条线程访问容器的互斥性。

有什么应用场景

在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。

ThreaLocal作用在每个线程内都都需要独立的保存信息,这样就方便同一个线程的其他方法获取到该信息的场景,由于每一个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息之后,后续方法可以通过ThreadLocal可以直接获取到,避免了传参,这个类似于全局变量的概念。比如像用户登录令牌解密后的信息传递、用户权限信息、从用户系统中获取到的用户名

image-20220326174403428

//用户微服务配置token解密信息传递例子
public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();
                LoginUser loginUser = new LoginUser();
                loginUser.setId(id);
                loginUser.setName(name);
                loginUser.setMail(mail);
                loginUser.setHeadImg(headImg);
                threadLocal.set(loginUser);
            
//后续想直接获取到直接threadLocal.getxxx就可以了

如何使用ThreadLocal来解决线程安全的问题

在我们平常的SpringWeb项目中,我们通常会把业务分成ControllerServiceDao等等,也知道注解@Autowired默认使用单例模式

那有没有想过,当不同的请求线程进来后,因为Dao层使用的是单例,那么负责连接数据库的Connection也只有一个了,这时候如果请求的线程都去连接数据库的话,就会造成这个线程不安全的问题,Spring是怎样来解决的呢?

Dao层里装配的Connection线程肯定是安全的,解决方案就是使用ThreadLocal方法。

当每一个请求线程使用Connection的时候,都会从ThreadLocal获取一次,如果值为null,那就说明没有对数据库进行连接,连接后就会存入到 ThreadLocal里,这样一来,每一个线程都保存有一份属于自己的Connection

每一线程维护自己的数据,达到线程的隔离效果。

最常见的ThreadLocal使用场景:

  1. 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。

  2. 线程间数据隔离

  3. 进行事务操作,用于存储线程事务信息。

  4. 数据库连接,Session会话管理。

Spring框架在事务开始时会给当前线程绑定一个Jdbc Connection,在整个事务过程都是使用该线程绑定的 connection来执行数据库操作,实现了事务的隔离性。

Spring框架里面就是用的ThreadLocal来实现这种隔离

ThreadLocal作用、场景、原理 - 简书 (jianshu.com)

面试官:不懂ThreadLocal,还谈什么并发编程? - 简书 (jianshu.com)

ThreadLocal内存泄露原因,如何避免

内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

image-20220319142920336

image-20220319143005730

注释说,Note that null keys (i.e. entry.get()* == null)如果 key threadlocalnull 了,这个 entry 就可以清除了。

ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收 。

img

虽然ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候,就会自动被回收,但是Entry中的value依然是强引用。

  1. Thread类的源码中有一个 threadLocals,就是ThreadLocalMap

  2. ThreadLocalMapEntry中的keyThreadLocal,值是我们自己设定的

  3. ThreadLocal是一个弱引用,当为null时,会被当成垃圾被JVM回收

  4. 敲重点,如果ThreadLocalnull了,也就是要被垃圾回收器回收了,但此时ThreadLocalMap生命周期和Thread是一样,它不会回收,这时候就出现了一个现象,ThreadLocalMapkey没了,但是value还在,这就造成了内存泄漏。

可以看到,只有当Thread被回收时,这个value才有被回收的机会,否则,只要线程不退出,value总是会存在一个强引用。但是,要求每个Thread都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄漏的可能。

JVM笔记(四)Java中的引用 - BeaBrick0 - 博客园 (cnblogs.com)

ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMapkey为使用弱引用ThreadLocal实例,value为线程变量的副本。

image-20220319162917133

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMapkeynull, 而value还存在着强引用,只有Thread线程退出以后,value的强引用链条才会断掉,但如果当前线程再迟迟不结束的话,这些keynullEntryvalue就会一直存在一条强引用链(红色链条)

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal正确的使用方法:

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据
  • ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
posted @ 2022-03-19 16:38  BearBrick0  阅读(87)  评论(1编辑  收藏  举报