写点什么

史上最全 ThreadLocal 详解

作者:EquatorCoco
  • 2024-10-28
    福建
  • 本文字数:5657 字

    阅读完需:约 19 分钟

概述


线程本地变量。当使用 ThreadLocal 维护变量时, ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。


每个线程都有一个 ThreadLocalMap ( ThreadLocal 内部类),Map 中元素的键为 ThreadLocal ,而值对应线程的变量副本。



ThreadLocal 原理


如何实现线程隔离


具体关于为线程分配变量副本的代码如下:


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


  • 首先获取当前线程对象 t, 然后从线程 t 中获取到 ThreadLocalMap 的成员属性 threadLocals

  • 如果当前线程的 threadLocals 已经初始化(即不为 null) 并且存在以当前 ThreadLocal 对象为 Key 的值, 则直接返回当前线程要获取的对象(本例中为 Connection);

  • 如果当前线程的 threadLocals 已经初始化(即不为 null)但是不存在以当前 ThreadLocal 对象为 Key 的的对象, 那么重新创建一个 Connection 对象, 并且添加到当前线程的 threadLocals Map 中,并返回

  • 如果当前线程的 threadLocals 属性还没有被初始化, 则重新创建一个 ThreadLocalMap 对象, 并且创建一个 Connection 对象并添加到 ThreadLocalMap 对象中并返回。


如果存在则直接返回很好理解, 那么对于如何初始化的代码又是怎样的呢?


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);    return value;}
复制代码


  • 首先调用上面写的重载过后的 initialValue 方法

  • 继续查看当前线程的 threadLocals 是不是空的, 如果 ThreadLocalMap 已被初始化, 那么直接将产生的对象添加到 ThreadLocalMap 中, 如果没有初始化, 则创建并添加对象到其中;


同时, ThreadLocal 还提供了直接操作 Thread 对象中的 threadLocals 的方法


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


这样也可以不实现 initialValue:


public Connection getConnection() {    Connection connection = dbConnectionLocal.get();    if (connection == null) {        try {            connection = DriverManager.getConnection("", "", "");            dbConnectionLocal.set(connection);        } catch (SQLException e) {            e.printStackTrace();        }    }    return connection;}
复制代码


看过代码之后就很清晰的知道了为什么 ThreadLocal 能够实现变量的多线程隔离了; 其实就是用了 Map 的数据结构给当前线程缓存了, 要使用的时候就从本线程的 threadLocals 对象中获取就可以了, key 就是当前线程;

当然了在当前线程下获取当前线程里面的 Map 里面的对象并操作肯定没有线程并发问题了, 当然能做到变量的线程间隔离了;


ThreadLocalMap 对象是什么


本质上来讲, 它就是一个 Map, 但是这个 ThreadLocalMap 与平时见到的 Map 有点不一样


  • 它没有实现 Map 接口;

  • 它没有 public 的方法, 最多有一个 default 的构造方法, 因为这个 ThreadLocalMap 的方法仅仅在 ThreadLocal 类中调用, 属于静态内部类

  • ThreadLocalMap 的 Entry 实现继承了 WeakReference<ThreadLocal<?>>

  • 该方法仅仅用了一个 Entry 数组来存储 Key, Value; Entry 并不是链表形式, 而是每个 bucket 里面仅仅放一个 Entry;


要了解 ThreadLocalMap 的实现, 我们先从入口开始, 就是往该 Map 中添加一个值:


private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not.
Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1);
//这里用的是Hash冲突的开放定址法的线性探测 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get();
if (k == key) { e.value = value; return; }
if (k == null) { replaceStaleEntry(key, value, i); return; } }
tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();}
复制代码


先进行简单的分析, 对该代码表层意思进行解读:


  • 看下当前 threadLocal 的在数组中的索引位置 比如: i = 2,看 i = 2 位置上面的元素(Entry)的 Key 是否等于 threadLocal 这个 Key, 如果等于就很好说了, 直接将该位置上面的 Entry 的 Value 替换成最新的就可以了;

  • 如果当前位置上面的 Entry 的 Key 为空, 说明 ThreadLocal 对象已经被回收了, 那么就调用 replaceStaleEntry

  • 如果清理完无用条目(ThreadLocal 被回收的条目)、并且数组中的数据大小 > 阈值的时候对当前的 Table 进行重新哈希 所以, 该 HashMap 是处理冲突检测的机制是向后移位, 清除过期条目 最终找到合适的位置;


了解完 Set 方法, 后面就是 Get 方法了:


private Entry getEntry(ThreadLocal<?> key) {    int i = key.threadLocalHashCode & (table.length - 1);    Entry e = table[i];    if (e != null && e.get() == key)        return e;    else        return getEntryAfterMiss(key, i, e);}
复制代码


先找到 ThreadLocal 的索引位置, 如果索引位置处的 entry 不为空并且键与 threadLocal 是同一个对象, 则直接返回; 否则去后面的索引位置继续查找


Entry 对象


static class Entry extends WeakReference<ThreadLocal<?>> {    /** The value associated with this ThreadLocal. */    Object value;
Entry(ThreadLocal<?> k, Object v) { super(k);//父类是WeakReference,也就是相当于new了一个弱引用(k) //也就相当于 map中的key是弱引用的 value = v; }}
复制代码


这里的 key 指向的 ThreadLocal 是弱引用,是为了防止 ThreadLocal 对象永远不会被回收。因为,若 key 为强引用,当 ThreadLocal 不想用了,那么就令 tl = null,但是此时 key 中还有一个强引用指向 ThreadLocal,因此也就永远无法进行回收(除非 ThreadLocalMap 不用了),所以会有内存泄露;但如果 key 使用的是弱引用,只要 GC,就会回收



但是还会有内存泄漏存在,ThreadLocal 被回收,就导致 key=null,此时 map 中也就无法访问到 value,无法访问到的 value 也就无用了,也就是说,这个 k-v 对无用了,那么 value 也应该被回收,但实际上 value 可能没有被回收,因此依然存在内存泄露


内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

弱引用:GC 时,若没有强引用指向这个对象了,只剩下弱引用,就会直接进行回收。原因就在于 GC 时无关内存是否足够,弱引用会被直接回收。所以,只要 tl=null 了,那么 GC 时,key 指向的 ThreadLocal 对象就会被回收


ThreadLocal 内存泄漏的原因?


每个线程都有⼀个 ThreadLocalMap 的内部属性,map 的 key 是 ThreaLocal ,定义为弱引用,value 是强引用类型。垃圾回收的时候会⾃动回收 key,而 value 的回收取决于 Thread 对象的生命周期。


一般会通过线程池的方式复用线程节省资源,而如果用线程池来操作 ThreadLocal 对象确实会造成内存泄露, 因为对于线程池里面不会销毁的线程, 里面总会存在着<ThreadLocal, LocalVariable>的强引用, 因为 final static 修饰的 ThreadLocal 并不会释放, 而 ThreadLocalMap 对于 Key 虽然是弱引用, 但是强引用不会释放, 弱引用当然也会一直有值, 同时创建的 LocalVariable 对象也不会释放, 就造成了内存泄露; 如果 LocalVariable 对象不是一个大对象的话, 其实泄露的并不严重, 泄露的内存 = 核心线程数 * LocalVariable 对象的大小;


所以, 为了避免出现内存泄露的情况, ThreadLocal 提供了一个清除线程中对象的方法, 即 remove, 其实内部实现就是调用 ThreadLocalMap 的 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) {            e.clear();            expungeStaleEntry(i);            return;        }    }}
复制代码


应用场景


每个线程维护了一个“序列号”


public class SerialNum {    // The next serial number to be assigned    private static int nextSerialNum = 0;
private static ThreadLocal serialNum = new ThreadLocal() { protected synchronized Object initialValue() { return new Integer(nextSerialNum++); } };
public static int get() { return ((Integer) (serialNum.get())).intValue(); }}
复制代码


Session 的管理


Web 应用中的请求处理:在 Web 应用中,一个请求通常会被多个线程处理,每个线程需要访问自己的数据,使用 ThreadLocal 可以确保数据在每个线程中的独立性。


经典的另外一个例子:


private static final ThreadLocal threadSession = new ThreadLocal();    public static Session getSession() throws InfrastructureException {      Session s = (Session) threadSession.get();      try {          if (s == null) {              s = getSessionFactory().openSession();              threadSession.set(s);          }      } catch (HibernateException ex) {          throw new InfrastructureException(ex);      }      return s;  }  
复制代码


在线程内部创建 ThreadLocal


线程池中的线程对象共享数据:线程池中的线程对象是可以被多个任务共享的,如果线程对象中需要保存任务相关的数据,使用 ThreadLocal 可以保证线程安全。


当然,在使用线程池时,ThreadLocal 可能会导致线程重用时的数据残留,从而影响程序的正确性。因此,在使用线程池时,要确保在任务执行前后清理 ThreadLocal 的值,以避免线程重用时的数据残留。


线程类内部创建 ThreadLocal,基本步骤如下:

  • 在多线程的类(如 ThreadDemo 类)中,创建一个 ThreadLocal 对象 threadXxx,用来保存线程间需要隔离处理的对象 xxx。

  • 在 ThreadDemo 类中,创建一个获取要隔离访问的数据的方法 getXxx(),在方法中判断,若 ThreadLocal 对象为 null 时候,应该 new()一个隔离访问类型的对象,并强制转换为要应用的类型。

  • 在 ThreadDemo 类的 run()方法中,通过调用 getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。


public class ThreadLocalTest implements Runnable{        ThreadLocal<Student> StudentThreadLocal = new ThreadLocal<Student>();
@Override public void run() { String currentThreadName = Thread.currentThread().getName(); System.out.println(currentThreadName + " is running..."); Random random = new Random(); int age = random.nextInt(100); System.out.println(currentThreadName + " is set age: " + age); Student Student = getStudentt(); //通过这个方法,为每个线程都独立的new一个Studentt对象,每个线程的的Studentt对象都可以设置不同的值 Student.setAge(age); System.out.println(currentThreadName + " is first get age: " + Student.getAge()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println( currentThreadName + " is second get age: " + Student.getAge()); } private Student getStudentt() { Student Student = StudentThreadLocal.get(); if (null == Student) { Student = new Student(); StudentThreadLocal.set(Student); } return Student; }
public static void main(String[] args) { ThreadLocalTest t = new ThreadLocalTest(); Thread t1 = new Thread(t,"Thread A"); Thread t2 = new Thread(t,"Thread B"); t1.start(); t2.start(); } }
class Student{ int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
复制代码


java 开发手册中推荐的 ThreadLocal


看看阿里巴巴 java 开发手册中推荐的 ThreadLocal 的用法:


import java.text.DateFormat;import java.text.SimpleDateFormat; public class DateUtils {    public static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>(){        @Override        protected DateFormat initialValue() {            return new SimpleDateFormat("yyyy-MM-dd");        }    };}
复制代码


然后再要用到 DateFormat 对象的地方,这样调用:


DateUtils.df.get().format(new Date());
复制代码


文章转载自:Seven

原文链接:https://www.cnblogs.com/seven97-top/p/18508133

体验地址:http://www.jnpfsoft.com/?from=infoq

用户头像

EquatorCoco

关注

还未添加个人签名 2023-06-19 加入

还未添加个人简介

评论

发布
暂无评论
史上最全ThreadLocal 详解_Java_EquatorCoco_InfoQ写作社区