通俗易懂的ThreadLocal详解

ThreadLocal详解

ThreadLocal是什么

ThreadLocal 类提供线程局部变量,它通常是私有类中希望将状态与线程关联的静态字段。

  • 从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

  • ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

    • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
    • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
  • ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。

  • ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景

  • 熟悉 Spring 的同学可能知道 Bean 的作用域(Scope),而 ThreadLocal 的作用域就是线程

图解:

在这里插入图片描述

ThreadLocal怎么用

线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里我说的是remove之前。

他是能做到线程间数据隔离的,所以别的线程使用get()方法是没办法拿到其他线程的值的。

测试代码一:

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

    new Thread(() -> {
    
    
        threadLocal.set("Thread1 -> cvzhanshi");
        System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
    },"Thread1").start();

    new Thread(() -> {
    
    
        System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
    },"Thread2").start();

    new Thread(() -> {
    
    
        System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
    },"Thread3").start();
}

在这里插入图片描述

示例代码二:

public static void main(String[] args) {
    
    

    // 创建一个ThreadLocal对象
    ThreadLocal<String> threadLocal = new ThreadLocal<>();

    new Thread(() -> {
    
    
        threadLocal.set("Thread1 -> cvzhanshi");
        System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
    },"Thread1").start();

    new Thread(() -> {
    
    
        threadLocal.set("Thread2 -> cvzhanshi");
        System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
    },"Thread2").start();


    new Thread(() -> {
    
    
        threadLocal.set("Thread3 -> cvzhanshi");
        System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
    },"Thread3").start();
}

在这里插入图片描述
由于ThreadLocal里设置的值,只有当前线程自己看得见,这意味着你不可能通过其他线程为它初始化值。为了弥补这一点,ThreadLocal提供了一个withInitial()方法统一初始化所有线程的ThreadLocal的值:

// ThreadLocal的初始值设置为cvzhanshi,这对全体线程都是可见的 
ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "cvzhanshi");

示例代码三:

public static void main(String[] args) {
    
    

    // 创建一个ThreadLocal对象
    ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "cvzhanshi");

    new Thread(() -> {
    
    
        threadLocal.set("Thread1 -> cvzhanshi");
        System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
    },"Thread1").start();

    new Thread(() -> {
    
    
        // threadLocal.set("Thread2 -> cvzhanshi");
        System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
    },"Thread2").start();


    new Thread(() -> {
    
    
        // threadLocal.set("Thread3 -> cvzhanshi");
        System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get());
    },"Thread3").start();
}

在这里插入图片描述

ThreadLocal的实现原理

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();
}

private T setInitialValue() {
    
    
    // 给默认值 且初始化map
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

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

ThreadLocal.ThreadLocalMap threadLocals = null;

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

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

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

ThreadLocal的使用场景

Spring框架源码使用场景

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,代码如下所示:

private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

private static final ThreadLocal<Map<Object, Object>> resources =
    new NamedThreadLocal<>("Transactional resources");

private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
    new NamedThreadLocal<>("Transaction synchronizations");

private static final ThreadLocal<String> currentTransactionName =
    new NamedThreadLocal<>("Current transaction name");

开发中的使用场景

  • 项目中的时间不对问题

    • 问题原因:当时我们使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

    • 解决方法:当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能

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

  • 线程间数据隔离

  • 进行事务操作,用于存储线程事务信息

  • 数据库连接,Session会话管理

ThreadLocal的内存泄露问题

ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。

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

在这里插入图片描述

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

以getEntry()为例:

private Entry getEntry(ThreadLocal<?> key) {
    
    
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        //如果找到key,直接返回
        return e;
    else
        //如果找不到,就会尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来
        return getEntryAfterMiss(key, i, e);
}

getEntryAfterMiss()的实现:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    
    
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
    
    
        // 整个e是entry ,也就是一个弱引用
        ThreadLocal<?> k = e.get();
        //如果找到了,就返回
        if (k == key)
            return e;
        if (k == null)
            //如果key为null,说明弱引用已经被回收了
            //那么就要在这里回收里面的value了
            expungeStaleEntry(i);
        else
            //如果key不是要找的那个,那说明有hash冲突,这里是处理冲突,找下一个entry
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

真正用来回收value的是expungeStaleEntry()方法,在remove()和set()方法中,都会直接或者间接调用到这个方法进行value的清理:

从这里可以看到,ThreadLocal为了避免内存泄露,也算是花了一番大心思。不仅使用了弱引用维护key,还会在每个操作上检查key是否被回收,进而再回收

value。但是从中也可以看到,ThreadLocal并不能100%保证不发生内存泄漏。

比如,你的get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行,如果你没有机会调用set()和remove(),那么这个内存泄漏依然会发

生。因此,要养成一个好习惯:当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的

ThreadLocalMap中的Hash冲突处理

ThreadLocalMap作为一个HashMap和java.util.HashMap的实现是不同的。对于java.util.HashMap使用的是链表法来处理冲突:

在这里插入图片描述

但是,对于ThreadLocalMap,它使用的是简单的线性探测法,如果发生了元素冲突,那么就使用下一个槽位存放:

在这里插入图片描述

查看set()方法:

private void set(ThreadLocal<?> key, Object value) {
    
    
    
    Entry[] tab = table;
    int len = tab.length;
    // 根据hash找到数组的位置
    int i = key.threadLocalHashCode & (len-1);
	// 如意如意这个位置没有被占用,说明没有冲突,那就不用循环了,直接使用这个位置
    // 如果发生冲突,那么就要一直往下找,找到一个可以用的位置
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
    
    
        // 到了循环里,说明已经发生了冲突
        ThreadLocal<?> k = e.get();
	
    	// 如果是重复值,那么就直接覆盖    
        if (k == key) {
    
    
            e.value = value;
            return;
        }
		// 如果key为null,说明原来的key被回收了,那么就要启动清理了
        if (k == null) {
    
    
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	
    // 一旦找到合适的位置,就把这个Entry放进去
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

可以被继承的ThreadLocal

在实际开发过程中,我们可能会遇到这么一种场景。主线程开了一个子线程,但是我们希望在子线程中可以访问主线程中的ThreadLocal对象,也就是说有些数据需要进行父子线程间的传递。比如像这样:

public static void main(String[] args) {
    
    
    ThreadLocal threadLocal = new ThreadLocal();
    IntStream.range(0,10).forEach(i -> {
    
    
        //每个线程的序列号,希望在子线程中能够拿到
        threadLocal.set(i);
        //这里来了一个子线程,我们希望可以访问上面的threadLocal
        new Thread(() -> {
    
    
            System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
        }).start();
        try {
    
    
            Thread.sleep(1000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
    });
}

运行结果,子线程拿不到父线程的值

在这里插入图片描述

因为在子线程中,是没有threadLocal的。如果我们希望子线可以看到父线程的ThreadLocal,那么就可以使用InheritableThreadLocal。顾名思义,这就是一个支

持线程间父子继承的ThreadLocal,将上述代码中的threadLocal使用InheritableThreadLocal:

InheritableThreadLocal threadLocal = new InheritableThreadLocal();

再运行查看结果,子线程能拿到父线程的值:

在这里插入图片描述

可以看到,每个线程都可以访问到从父进程传递过来的一个数据。虽然InheritableThreadLocal看起来挺方便的,但是依然要注意以下几点:

  • 变量的传递是发生在线程创建的时候,如果不是新建线程,而是用了线程池里的线程,就不灵了
  • 变量的赋值就是从主线程的map复制到子线程,它们的value是同一个对象,如果这个对象本身不是线程安全的,那么就会有线程安全问题

Thread源码中,我们看看Thread.init初始化创建的时候的步骤

public class Thread implements Runnable {
    
    
  ……
   if (inheritThreadLocals && parent.inheritableThreadLocals != null)
      this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  ……
}

如果线程的inheritThreadLocals变量不为空,而且父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals

猜你喜欢

转载自blog.csdn.net/qq_45408390/article/details/122217538