ThreadLocal原理与实战

本文深入探讨了Java中的ThreadLocal,包括其基本使用、常见使用场景、内部结构演变以及源码分析。ThreadLocal提供线程隔离的变量副本,确保在多线程环境中变量的独立性,常用于数据库连接、Session管理等。文章还详细解析了ThreadLocal的set、get和remove方法,指出使用WeakReference避免内存泄露,并提出了规范使用ThreadLocal的建议。
摘要由CSDN通过智能技术生成

1. 基本使用

ThreadLocal的英文字面意思为“本地线程”,实际上ThreadLocal代表的是线程的本地变量。如果程序创建了一个ThreadLocal实例,那么在访问这个变量的值时,每个线程都会拥有一个独立的、自己的本地值。“线程本地变量”可以看成专属于线程的变量,不受其他线程干扰,保存着线程的专属数据。当线程结束后,每个线程所拥有的那个本地值会被释放

在这里插入图片描述

代码示例:

package threadpool;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadLocalDemo {
    static class Foo {
        static final AtomicInteger AMOUNT = new AtomicInteger(0);// 实例总数
        int index = 0;// 对象编号
        int bar = 10;// 对象内容

        public Foo() {
            index = AMOUNT.incrementAndGet();// 对象总数+1
        }

        public int getIndex() {
            return index;
        }

        public void setIndex(int index) {
            this.index = index;
        }

        public int getBar() {
            return bar;
        }

        public void setBar(int bar) {
            this.bar = bar;
        }

        @Override
        public String toString() {
            return "Foo [bar=" + bar + ", index=" + index + "]";
        }

    }

    private static final ThreadLocal<Foo> LOCAL_FOO = new ThreadLocal<>();

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 3; i++) {
            pool.execute(new Runnable() {

                @Override
                public void run() {
                    if (LOCAL_FOO.get() == null) {
                        LOCAL_FOO.set(new Foo());
                    }
                    System.out.println(Thread.currentThread().getName()+" 初始本地值:" + LOCAL_FOO.get());
                    for(int i=0;i<10;i++)
                    {
                        Foo foo=LOCAL_FOO.get();
                        foo.setBar(foo.getBar()+1);
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("累加10次后的本地值:"+LOCAL_FOO.get());
                    LOCAL_FOO.remove();//删除本地值

                }

            });
        }
        pool.shutdown();
    }
}

在这里插入图片描述

上面的创建了一个ThreadLocal实例,然后创建了3个线程来处理3个任务,每个线程中的Foo对象是自己独有的,与其他线程的Foo对象互不干扰,最后3个线程中累加10次的结果都是正确的。


2. ThreadLocal的使用场景

  1. 线程隔离

ThreadLocal的主要价值在于线程隔离,ThreadLocal中的数据只属于当前线程,其本地值对别的线程是不可见的,在多线程环境下,可以防止自己的变量被其他线程篡改。
常见的ThreadLocal使用场景为数据库连接独享、Session数据管理等

	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 (Hi bernateException ex) {
    throw new InfrastructureException (ex) ;
    }
    return S;

以上面这段Hibernate中获取数据库连接代码为例,一个Session代表一个数据库连接,Sesseion被存放进了ThreadLocal变量中,这个Session相当于线程的私有变量,而不是所有线程共用的,显然其他线程中是取不到这个Session的

  1. 跨函数传递数据

ThreadLocal的特性,同一线程在某些地方进行设置,在随后的任意地方都可以获取到。线程执行过程中所执行到的函数都能读写ThreadLocal变量的线程本地值,从而可以方便地实现跨函数的数据传递
可以为每个线程绑定一个Session(用户会话)信息,这样一个线程所有调用到的代码都可以非常方便地访问这个本地会话,而不需要通过参数传递

  • 用来传递请求过程中的用户ID
  • 用来传递请求过程中的用户会话(Session)
  • 用来传递HTTP的用户请求实例HttpRequest
  • 其他需要在函数之间频繁传递的数据

3. ThreadLocal内部结构演进

早期:

  • ThreadLocal的内部结构是一个Map,其中每一个线程实例作为Key,线程在“线程本地变量”中绑定的值为Value(本地值)
  • 其拥有者为ThreadLocal,每一个ThreadLocal实例拥有一个Map实例
    在这里插入图片描述

jdk1.8:

ThreadLocal.ThreadLocalMap threadLocals = null; Thread类的成员变量

  • 其拥有者为Thread(线程)实例, 每一个Thread(线程)实例拥有一个Map实例
  • 每一个Thread线程内部都有一个Map(ThreadLocalMap),如果给一个Thread创建多个ThreadLocal实例,然后放置本地数据,那么当前线程的ThreadLocalMap中就会有多个“Key-Value对”
    在这里插入图片描述

新版本较旧版本的变化:

  • 新版本的ThreadLocalMap拥有者为Thread,早期版本的ThreadLocalMap拥有者为ThreadLocal
  • 新版本的Key为ThreadLocal实例,早期版本的Key为Thread实例

新版本的优势:

  • 每个ThreadLocalMap存储的“Key-Value对”数量变少
  • Thread实例销毁后,ThreadLocalMap也会随之销毁,在一定程度上能减少内存的消耗

4. ThreadLocal源码分析

1. set(T value)方法

 public void set(T value) {
        Thread t = Thread.currentThread();//获取当前线程对象
        ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap成员
        if (map != null)//map存在
            map.set(this, value);//绑定value到ThreadLocal实例
        else
            createMap(t, value);
    }
    
 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }


  1. 获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量
  2. 如果map不为空,就将Value设置到map中,当前的ThreadLocal作为Key
  3. 如果map为空,为该线程创建map,然后设置第一个“Key-Value对”,Key为当前的ThreadLocal实例,Value为set()方法的参数value值

2. get()方法

public T get() {
        Thread t = Thread.currentThread();//获取当前线程
        ThreadLocalMap map = getMap(t);//获取当前线程的ThreadLocalMap成员
        if (map != null) {//成员存在
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //(map不存在)||(map存在但是map没有K-V对)
        return setInitialValue();
    }

 private T setInitialValue() {
        T value = initialValue();//初始化钩子函数
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);//map存在 设置初始值
        else
            createMap(t, value);//map不存在  创建map并设置初始值
        return value;
    }
  protected T initialValue() {
        return null;
    }
  1. 先尝试获得当前线程,然后获得当前线程的ThreadLocalMap成员,暂存于map变量
  2. 如果获得的map不为空,那么以当前ThreadLocal实例为Key尝试获得map中的Entry(条目)
  3. 如果Entry不为空,就返回Entry中的Value
  4. 如果Entry为空,就通过调用initialValue初始化钩子函数获取ThreadLocal初始值,并设置在map中。如果map不存在,还会给当前线程创建新ThreadLocalMap成员,并绑定第一个“Key-Value对”

3. remove()方法

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

用于在当前线程的ThreadLocalMap中移除“线程本地变量”所对应的值

4. initialValue()方法

  protected T initialValue() {
        return null;
    }

如果没有调用set()直接调用get(),就会调用该方法,但是该方法只会被调用一次。默认情况下,initialValue()方法返回null


5. ThreadLocalMap源码分析

ThreadLocal的操作都是基于ThreadLocalMap展开的,而ThreadLocalMap是ThreadLocal的一个静态内部类

static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
         //Map的题目类型  静态内部类
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;//Map的条目初始化为16

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;//Map的条目数组

        /**
         * The number of entries in the table.
         */
        private int size = 0;//Map的条目数量

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0 //拓展因子
.......................
.......................
      }
      

ThreadLocal源码中的get()、set()、remove()方法都涉及ThreadLocalMap的方法调用,主要调用了ThreadLocalMap的如下几个方法:

  1. set(ThreadLocal<?> key,Object value):向Map实例设置“Key-Value对”
  2. getEntry(ThreadLocal):从Map实例获取Key(ThreadLocal实例)所属的Entry
  3. remove(ThreadLocal):根据Key(ThreadLocal实例)从Map实例移除所属的Entry

Entry继承了WeakReference,它的key是一个弱引用,使用弱引用的原因如下:
在这里插入图片描述

执行上述代码,采用弱引用的内存结构如下:
在这里插入图片描述

当funA()方法执行完毕后,栈帧被销毁,因此local变量也没有了,此时ThreadLocal实例只有1个Key的弱引用,仅有弱引用(Weak Reference)指向的对象只能生存到下一次垃圾回收之前,这样ThreadLocal实例实例就会被回收,避免内存泄露

如果采用强引用,内存结构如下:
在这里插入图片描述

即使funA()方法执行结束,栈帧被销毁,依然存在一个key的强引用指向ThreadLocal实例,即使这个实例不再需要,它也不能被GC回收,会造成内存泄露

使用ThreadLocal可能发生内存泄漏的情况:

  • 线程长时间运行而没有被销毁。线程池中的Thread实例很容易满足此条件
  • ThreadLocal引用被设置为null,且后续在同一Thread实例执行期间,没有发生对其他ThreadLocal实例的get()、set()或remove()操作。只要存在一个针对任何ThreadLocal实例的get()、set()或remove()操作,就会触发Thread实例拥有的ThreadLocalMap的Key为null的Entry清理工作,释放掉ThreadLocal弱引用为null的Entry

规范使用ThreadLocal实例:
在这里插入图片描述

  • 使用static使得即使有多个ThreadLocal实例,它们共享的也是一块内存空间
  • final修饰保证ThreadLocal实例的唯一性
  • private修饰缩小使用范围

参考书籍:参考书籍:《Java高并发编程卷2》 尼恩

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CodePanda@GPF

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值