java并发 ThreadLocal

文章详细介绍了ThreadLocal在解决多线程数据一致性问题中的作用,以及其内部实现原理,包括线程局部变量、弱引用和内存泄露问题。此外,还讨论了ThreadLocal的使用场景,如资源持有、线程一致性、线程安全和并发计算,并指出在使用ThreadLocal时需要注意的内存泄露问题及其解决方案。
摘要由CSDN通过智能技术生成

1 一致性问题

1.1 一致性问题简介

多线程充分利用了多核CPU的威力,为我们程序提供了很高的性能。但是有时候,我们需要多个线程互相协作,这里可能就会涉及到数据一致性的问题。 数据一致性指问题的是:发生在多个主体同一份数据无法达成共识。这里的多个主体,可能是多线程,也可能是多个服务器节点。 当然了,这里的“多个主体”也可以指朋友之间,夫妻之间,所谓“道不同,不相为谋”,说的就是这个理。

1.2 解决方法

  • 投票: 投票的话,多个人可以同时去做一件决策,或者同时去修改数据,但最终谁修改成功,是用投票来决定的。这个方式很高效,但它也会产生很多问题,比如网络中断、欺诈等等。想要通过投票达到一致性非常复杂,往往需要严格的数学理论来证明,还需要中间有一些“信使”不断来来回回传递消息,这中间也会有一些性能的开销。 我们在分布式系统中常见的Paxos和Raft算法,
  • 排队: 如果两个人对一个问题的看法不一致,那就排成一队,一个人一个人去修改它,这样后面一个人总是能够得到前面一个人修改后的值,数据也就总是一致的了。我们在操作系统中的锁、互斥量、管程、屏障等等概念,都是利用了排队的思想。排队虽然能够很好的确保数据一致性,但性能非常低。
  • 避免: 既然保证数据一致性很难,那我能不能通 过一些手段,去避免多个线程之间产生一致性问题呢?我们程序员熟悉的git就是这个实现,大家在本地分布式修改同一个文件,最后通过版本控制和“冲突解决”去解决这个问题。 而我们今天的正题,ThreadLocal,也是使用的“避免”这种方式

2 ThreadLocal

2.1 定义

ThreadLocal提供了线程局部变量,一个线程局部变量在多个线程中,分别有独立的值(副本)。

2.2 线程模型

在这里插入图片描述
左边的黑色大圆圈代表一个进程。进程里有一个线程表,红色波浪线代表一个个线程。

对于每一个线程来说,都有自己的独占数据。这些独占数据是进程来分配的,对于Java来说,独占数据很多都是在Thread类里面分配的,而每一个线程里面都有一个 ThreadLocalMap 的对象,它本身是一个哈希表,里面会放一些线程的局部变量(红色长方形)。ThreadLocal 的核心也是这个 ThreadLocalMap

2.3 基本API

  • 构造函数 ThreadLocal()
  • 初始化 initialValue()
  • 访问器 get()/set()
  • 回收 remove()

构造函数是一个泛型的,传入的类型是你要使用的局部变量变量的类型。

初始化initialValue()用于如果没有调用set()方法时,又调用get()方法,此时来返回默认值。若不重载初始化方法,则会返回null

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
      return new SuppliedThreadLocal<>(supplier);
}

示例代码:

public class ThreadLocalDemo {
    public static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(() -> {
        System.out.println("invoke initial value");
        return "default value";
    });

    public static void main(String[] args) throws InterruptedException {
        new Thread(() ->{
            THREAD_LOCAL.set("first thread");
            System.out.println(THREAD_LOCAL.get());
        }).start();

        new Thread(() ->{
            THREAD_LOCAL.set("second thread");
            System.out.println(THREAD_LOCAL.get());
        }).start();

        new Thread(() ->{
            THREAD_LOCAL.set("third thread");
            THREAD_LOCAL.remove();
            System.out.println(THREAD_LOCAL.get());
        }).start();

        new Thread(() ->{
            System.out.println(THREAD_LOCAL.get());
        }).start();

        SECONDS.sleep(1L);
    }
}

输出结果:

first thread
second thread
invoke initial value
default value
invoke initial value
default value

2.4 源码解析

ThreadLocalMap

根据源码注释得知:

  1. ThreadLocalMap只会被ThreadLocal类所维护
  2. Entrykey是弱引用(WeakReferences

Entry的注释可知:

  1. Entrykey 必须是 ThreadLocal 类型引用,并且是一个弱引用
  2. 如果 entry.get()== null 意味着某Entrykey不再被引用(指向的对象已经被GC) ,所以此 entry 就可以从table 中回收,这时此 entrytable 中被称为 stale entries

弱引用(weakReferences):如果某个对象剩下弱引用指向它,那么下一次GC的时候该对象就会被回收掉

     static class ThreadLocalMap {
        //ThreadLocalMap真正存数据的是Entry,且Entry的key使用的是弱引用(WeakReferences)
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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


            // ....省略
        }

Thread、ThreadLocal、ThreadLocalMap、Entry 的关系图如下:
在这里插入图片描述

set()

先拿到当前的线程,然后通过它去拿到一个Map,如果这个Map存在,就把value塞进去,否则就创建一个新的。

// java.lang.ThreadLocal#set
    public void set(T value) {
        Thread t = Thread.currentThread();    //拿到当前的线程
        ThreadLocalMap map = getMap(t);     //根据当前的线程拿到ThreadLocalMap
        if (map != null) {                    //如果map不为空就set value
            map.set(this, value);
        } else {
            createMap(t, value);            //否则创建一个新的ThreadLocalMap,并且set value
        }
    }

getMap(Thread t):返回当前线程的 threadLocals参数, 每个线程对应一个自己线程私有的ThreadLocalMap,它被Thread对象持有。

    // java.lang.ThreadLocal#getMap
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    //java.lang.Thread 
    ThreadLocal.ThreadLocalMap threadLocals = null;

createMap(Thread t, T firstValue) 方法: 创建一个新的ThreadLocalMap对象,并赋值给当前线程对象。ThreadLocalMapkey 是当前 ThreadLocal 对象

get()

先通过 getMap(Thread t) 方法拿到当前线程对应的Map,然后从里面取出value。如果没有value,就调用ThreadLocal提供的初始化方法,初始化一个值。

    //java.lang.ThreadLocal#get
    public T get() {
        Thread t = Thread.currentThread();                     //获得当前线程
        ThreadLocalMap map = getMap(t);                        //获取当前线程的ThreadLocalMap 对象
        if (map != null) {                                    //当前线程的ThreadLocalMap 对象 不为空
            ThreadLocalMap.Entry e = map.getEntry(this);     //从当前的ThreadLocalMap 对象中取出 key为当前 ThreadLocal 对象的 Entry
               if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;                        
                return result;
            }
        }
        return setInitialValue();                            //当前线程的ThreadLocalMap 对象 为空返回初始值        
    }

setInitialValue() 方法:
通过 initialValue() 获得初始值,将初始值赋值给当前线程的ThreadLocalMap 对象,以当前ThreadLocal对象为key ,以初始值为value。 返回该值。

remove()

remove()方法不得不提。首先我们思考一下,既然已经有了弱引用,按理说,如果线程没有持有某个value的时候,会在GC的时候自动清理掉对应的Entry,为什么会有remove()方法存在?

因为我们在开发一个多线程的程序时,往往会使用线程池。而线程池的功能就是线程的复用。那如果线程池和ThreadLocal在一起就可能会造成一个问题:

  • job A和job B共用了同一个线程
  • job A使用完ThreadLocal,ThreadLocal里面还有job A保存的值,而这个时候可能还没有清理掉
  • job B复用线程进来了,取出来是 job A的值,可能就会造成问题。

所以在有必要的时候,可以在使用完ThreadLocal的时候,显式调用一下remove()方法。remove()方法的源码也比较简单,就是调用对应的 entry 的 clear()方法。 同时,remove() 方法也能很好的避免内存泄露问题。

//java.lang.ThreadLocal#remove
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this);
    }
}
//java.lang.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(); //删除entry
            expungeStaleEntry(i); //
            return;
        }
    }
}

3 使用ThreadLocal的注意事项

3.1 ThreadLocal内存泄露

ThreadLocal内存泄露的原因

示例代码如下:

public class ThreadLocalOOMDemo {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(1);

        executorService.execute(() -> {
            ThreadLocal<RedSpider> threadLocal = new ThreadLocal<>();
            RedSpider redSpider = new RedSpider();
            threadLocal.set(redSpider);
            threadLocal=null          //将threadLocal 引用赋值为空
        });
    }
}

上述代码逻辑图如下:
在这里插入图片描述

我们已知ThreadLocalMap中的Key为弱引用,当我们执行threadLocal=null 时, 强引用②将会消失,那么ThreadLocal对象实例只剩下一个弱引用③,在下一次 GC 时就会被回收。此时就只剩下强引用①和强引用④。如果当前线程迟迟不断掉的话就会一直存在一条强引用链:thread(ref)->Thread->ThreadLocalMap->Entry->redSpider(ref) 。所以ThreadLocal 内存泄露的原因也就找到了:

  1. 堆中有一个强引用指向RedSpider实例,该实例无法被GC
  2. 因为Entry中key为null,所以没有任何途径能接触到redSpider(ref),因此也不能访问到 RedSpider对象实例。

解决办法

remove()方法 中调用了一个expungeStaleEntry() 方法,这个方法是解决问题的关键。

// staleSlot index of slot known to have null key; 
// java.lang.ThreadLocal.ThreadLocalMap#expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // ...(省略)
    return i;
}

入参:staleSlot index of slot known to have null key; 该方法的逻辑:

  • 将entry里值的强引用(强引用④)置为null(这样值对象就对被GC回收)。
  • 将entry对应引用(弱引用③)置为null(这样Entry就能被GC回收)。

这样逻辑图中只剩下强引用① 和强引用③ ,这样RedSpider对象实例 和对应的 Entry 实例就可以被回收掉了。
因此,只要调用了expungeStaleEntry() 就能将无用 Entry 回收清除掉。

ThreadLocalMapget()方法 ,set()方法,间接的调用了该方法。remove()方法直接调用了该方法,以下为expungeStaleEntry() 方法的调用链。
在这里插入图片描述
综上所述:针对ThreadLocal 内存泄露的原因,我们可以从两方面去考虑:

  1. 删除无用 Entry 对象,断掉指向ThreadLocal实例的弱引用。即 用完ThreadLocal后手动调用remove()方法。
  2. 可以让ThreadLocal 的强引用一直存在,保证任何时候都可以通过 ThreadLocal 的弱引用访问到 Entry的 value值。即 将ThreadLocal 变量定义为 private static

3.1 ThreadLocal父子线程传值

inheritateThreadLocal

使用案例:

public class InheritableThreadLocalDemo {

    public static void main(String[] args) throws InterruptedException{
        Thread a = new Thread(() -> {
            InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();  ①
            itl.set("InheritableThreadLocal");

            Thread b = new Thread(() -> {
                String str = itl.get();
                //拿到父线程放进去的“InheritableThreadLocal”,因为tl是InheritableThreadLocal
                System.out.println(str);
            });
            b.start();

            //确保子线程b执行完毕 
            Thread.sleep(10);

            itl.remove();
        });
        a.start();
    }
}

//执行结果
InheritableThreadLocal
//如果 ① 处定义的是 ThreadLocal 执行结果则为 
null

可以看到使用了 InheritableThreadLocal 后,子线程b 获取到了父线程a set 的值。

原理: 首先我们看ThreadLocal 类的 set(T value) 方法

 //java.lang.ThreadLocal#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);     //如果map为空,调用createMap 方法
    }

    //java.lang.ThreadLocal#createMap  
    void createMap(Thread t, T firstValue) {
        //ThreadLocal 类给当前线程的threadLocals变量赋值
        t.threadLocals = new ThreadLocalMap(this, firstValue); 
    }

    //java.lang.InheritableThreadLocal#createMap 
    void createMap(Thread t, T firstValue) {
        //InheritableThreadLocal的 inheritableThreadLocals 变量赋值
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); 
    }

    //与此线程有关的 ThreadLocal 值,由ThreadLocal 类维护
    //java.lang.Thread
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //与此线程有关的 InheritableThreadLocal 值,由InheritableThreadLocal 类维护
    //java.lang.Thread
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

可以看到InheritableThreadLocal#createMapThreadLocal#createMap 的唯一区别是使用的成员变量不同。当我们调用 set() 方法时,如果声明的是 InheritableThreadLocal 类时,会给当前线程的 inheritableThreadLocals 变量赋值。

Thread 类中只有两处使用到 inheritableThreadLocals 变量。分别是 init()方法和 exit()方法。exit() 方法是给当前线程的一写变量赋值为null,这里不做过多阐述。init()方法是什么?通过查看调用该方法的地方可以看到,Thread 类的所有构造函数都调用了init() 方法。即当我们新建一个线程时,就会调用init() 方法,并给线程的inheritableThreadLocals 变量赋值。相关代码如下:

 //java.lang.Thread#init
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
           //省略 ...
        Thread parent = currentThread();             //这里的parent指的是调用init()方法的线程,即所谓的父线程
        //省略 ...
        //如果inheritThreadLocals 为ture 并且当前线程的inheritableThreadLocals变量不为空
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);  //这里传入的是当前线程的 inheritableThreadLocals 变量
           //省略 ...
    }

    //java.lang.ThreadLocal#createInheritedMap
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

    //java.lang.ThreadLocalMap Constructor
    private ThreadLocalMap(ThreadLocalMap parentMap) {
            //父线程 Entry
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];
            //每个Entry都赋值到子线程的Entry
            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        //关键的一行 e.value是父线程Entry中的值,childValue()是一个可重载的方法,
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
    //java.lang.ThreadLocal#childValue
    T childValue(T parentValue) {
        throw new UnsupportedOperationException();
    }

    //java.lang.InheritableThreadLocal#childValue 对于 InheritableThreadLocal 来说,返回了传入的值。
     protected T childValue(T parentValue) {
        return parentValue;                  
    }

即线程初始化时,将父线程的inheritThreadLocal拷贝到子线程的inheritThreadLocal。不是同一个对象!!!

InheritableThreadLocal 无法向线程池中的子线程传递数据

平常我们开发时很少新建线程来并发编程,一般都是使用线程池。但是 InheritableThreadLocal 无法向线程池中的子线程传递数据 示例代码:

public class InheritableThreadLocalDemo2 {

    public static void main(String[] args) throws InterruptedException {

        ExecutorService executorService = Executors.newFixedThreadPool(1);

        InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
        itl.set("first");
        executorService.execute(() -> {
            String firstValue = itl.get();
            System.out.println(firstValue);
        });
        //确保线程池任务执行完
        Thread.sleep(10);
        itl.remove();

        itl.set("second");
        executorService.execute(() -> {
            String secondValue = itl.get();
            System.out.println(secondValue);
        });
        //确保线程池任务执行完
        Thread.sleep(10);
        itl.remove();
    }
}

执行结果:
first
first

我们预期得到的结果是 first second,然而现在输出的却是 first first。说明我们 InheritableThreadLocal 对象第二次调用 set()方法失效。这是因为Thread对象的inheritableThreadLocals 变量只有在新建线程时会从父线程的inheritableThreadLocals 变量中拷贝过来。

所以后续父线程对InheritableThreadLocal 的修改,子线程并不会知道

alibaba 提供了 transmittable-thread-local 框架来解决了 在使用线程池等会池化复用线程的执行组件情况下传递ThreadLocal值问题。

4 ThreadLocal的使用场景

4.1 资源持有

比如我们有三个不同的类。在一次Web请求中,会在不同的地方,不同的时候,调用这三个类的实例。但用户是同一个,用户数据可以保存在一个线程里。
在这里插入图片描述
这个时候,我们可以在程序1把用户数据放进ThreadLocalMap里,然后在程序2和程序3里面去用它。 这样做的优势在于:持有线程资源供线程的各个部分使用,全局获取,降低编程难度。

4.2 线程一致

这里以JDBC为例。我们经常会用到事务,它是怎么实现的呢?

在这里插入图片描述
原来,我们每次对数据库操作,都会走JDBC getConnection,JDBC保证只要你是同一个线程过来的请求,不管是在哪个part,都返回的是同一个连接。这个就是使用ThreadLocal来做的。 当一个part过来的时候,JDBC会去看ThreadLocal里是不是已经有这个线程的连接了,如果有,就直接返回;如果没有,就从连接池请求分配一个连接,然后放进ThreadLocal里。 这样就可以保证一个事务的所有part都在一个连接里。TheadLocal可以帮助它维护这种一致性,降低编程难度。

4.3 线程安全

假设我们一个线程的调用链路比较长。在中途中出现异常怎么做?我们可以在出错的时候,把错误信息放到ThreadLocal里面,然后在后续的链路去使用这个值。 使用TheadLocal可以保证多个线程在处理这个场景的时候保证线程安全。

在这里插入图片描述

4.4 并发计算

如果我们有一个大的任务,可以把它拆分成很多小任务,分别计算,然后最终把结果汇总起来。如果是分布式计算,可能是先存储在自己的节点里。而如果是单机下的多线程计算,可以把每个线程的计算结果放进ThreadLocal里面,最后取出来汇总。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值