深入浅出ThreadLocal

本文深入讲解ThreadLocal的工作原理,包括其特性、测试示例、源码分析及在Shiro和MyBatis等开源框架中的应用,揭示线程变量隔离机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

深入浅出ThreadLocal

1.特性

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

可以简单理解为不同线程之间,变量不会互相影响

2.测试例子

2.1代码1

public class ThreadLocalTest {

    private static int num = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                num += 5;
                System.out.println(Thread.currentThread().getName() + "---->" + num);
            }, "Thread" + i).start();
        }
    }

}

执行结果:

大部分情况下线程拿到的num都是其他线程累加之后的值,当然在并发情况下会出现线程安全问题(这里不做讨论)

[外链图片转存失败(img-eUYHvKUP-1564795476680)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564736353139.png)]

怎么让每个线程都操作的是自己的num呢?

2.2代码2

public class ThreadLocalTest1 {

    private static ThreadLocal<Integer> local = new ThreadLocal() {
        @Override
        protected Object initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                int num = local.get() + 5;
                System.out.println(Thread.currentThread().getName() + "---->" + num);
            }, "Thread" + i).start();
        }
    }

}

执行结果:

线程从local中取到的值都是0,线程间并没有影响

[外链图片转存失败(img-5MIHh186-1564795476682)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564736570822.png)]

图解:

每个线程访问ThreadLocal对象时,ThreadLocal都会返回一个Integer为0的副本

[外链图片转存失败(img-t1AXnGqz-1564795476682)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564736895955.png)]

2.3代码3

这里还有一个基本类型引用类型的区别

详情可以看这篇文章 java是值传递还是引用传递?

代码2中的例子为基本类型的包装类型,本例子中则是用Index对象引用类型;

ThreadLocal初始化返回的是Index的引用类型

public class ThreadLocalTest2 {

    private static Index index = new Index();
    private static ThreadLocal<Index> local = new ThreadLocal() {
        @Override
        protected Object initialValue() {
            return index;
        }
    };

    static class Index {
        Integer num = 0;
        public void inrc() {
            num++;
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                Index index = local.get();
                index.inrc();
                System.out.println(Thread.currentThread().getName()+ "---->" + local.get().num);
            },"Thread"+i).start();
        }
    }

}

执行结果:

[外链图片转存失败(img-gRTHWfWf-1564795476683)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564737135604.png)]

明明用了ThreadLocal,但是值还是互相影响了

原因是Index对象是引用类型,操作的是引用地址的副本,但是引用地址指向的还是同一个堆中的内存地址,所以最终修改同一个内存地址中的值,还是影响到了其它的线程中的num变量

    private static ThreadLocal<Index> local = new ThreadLocal() {
        @Override
        protected Object initialValue() {
            return index;
        }
    };

修改为

    private static ThreadLocal<Index> local = new ThreadLocal() {
        @Override
        protected Object initialValue() {
            return new Index();
        }
    };

就可以了,每次返回一个新的对象(一个新的内存地址,就不会影响其它线程)

有图有真相,执行效果

在这里插入图片描述

3.源码分析

3.1类分析

环境:jdk1.8

ThreadLocal实现线程变量隔离的数据结构

[外链图片转存失败(img-PEcAgTc6-1564795476683)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564737479347.png)]

Thread类

[外链图片转存失败(img-cMiChgJH-1564795476683)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564737676294.png)]

每个线程都有一个ThreadLocakMap的对象

ThreadLocal类

ThreadLocal的ThreadLocalMap对象内部存放的是一个Entry对象

[外链图片转存失败(img-4VyECa7P-1564795476683)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564795175837.png)]

部分代码:

    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.
         */
        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;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
        ......

3.2ThreadLocal类中的get()方法

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
  1. 获取当前线程

  2. 获取线程的ThreadLocalMap对象

    此方法直接返回的是当前线程的threadLocals属性
    [外链图片转存失败(img-g6BoiVh8-1564795476683)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564737859994.png)]

3.如果不为null,则返回ThreadLocalMap存放的值

4.如果为null,初始化

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

ThreadLocalMap map = getMap(t);获取当前线程的threadLocals值

  1. 如果不为null就设置key为this当前ThreadLocal实例对象,value为这个值
  2. 如果为null则调用createMap方法继续初始化,key为当前线程,value为initialValue()方法的值,此方法为我们重载写的方法。
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

这一步就将当前线程的threadLocals属性设置为一个ThreadLocalMap的对象。

3.3ThreadLocal类中的set()方法

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

过程和get()方法基本一致

3.4结论

经过上面的过程,保证每个线程都有一个ThreadLocalMap的对象,这个对象的key为ThreadLocal的实例对象,值就是我们set()设置的值或者是重写initialValue()方法的返回值;通过这样的方式实现了线程变量的隔离效果。

4.开源框架中的应用

4.1shiro 存储当前用户subject的信息

shiro中使用aop切面的方式处理请求,进行权限的验证,获取用户权限时,aop切面中很难通过传入的参数进行获取,而ThreadLocal可以很好的突破这种限制

在发起请求的时候,线程和subject(当前用户)做绑定,进入aop切面的时候,从threadContext中取出subject(当前用户)

源码:
权限验证方法
[外链图片转存失败(img-Aqgta5wV-1564795476684)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564711661319.png)]
获取Subject方法
[外链图片转存失败(img-A6eTlNmm-1564795476684)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564711690060.png)]
从ThreadContext类中获取
[外链图片转存失败(img-EtAJMnI4-1564795476684)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564711705162.png)]

ThreadContext 部分代码
private static final ThreadLocal<Map<Object, Object>> resources核心代码

public abstract class ThreadContext {
    ......
private static final ThreadLocal<Map<Object, Object>> resources = new InheritableThreadLocalMap<Map<Object, Object>>();

    public static Subject getSubject() {
        return (Subject) get(SUBJECT_KEY);
    }
    ......

4.2mybatis 日志打印处理

在一个每个操作数据库请求,每一个关键步骤都使用ErrorContext进行了日志的收集,内部就是用ThreadLocal实现。
当一个线程报错的时候,通过追踪日志打印的信息可以判断在哪一步错误,不同的线程日志信息不会互相影响
[外链图片转存失败(img-kG3cpkOc-1564795476685)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564711937713.png)]
[外链图片转存失败(img-hgcTZXaB-1564795476685)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564711958023.png)]
[外链图片转存失败(img-Q6gY2rx2-1564795476685)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1564711987601.png)]

部分代码
核心代码private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();

public class ErrorContext {

  private static final String LINE_SEPARATOR = System.getProperty("line.separator","\n");
  private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<>();

  private ErrorContext stored;
  private String resource;
  private String activity;
  private String object;
  private String message;
  private String sql;
  private Throwable cause;

  private ErrorContext() {
  }

  public static ErrorContext instance() {
    ErrorContext context = LOCAL.get();
    if (context == null) {
      context = new ErrorContext();
      LOCAL.set(context);
    }
    return context;
  }

  public ErrorContext store() {
    ErrorContext newContext = new ErrorContext();
    newContext.stored = this;
    LOCAL.set(newContext);
    return LOCAL.get();
  }
  ......
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值