ThreadLocal

简介

线程安全是保证多个线程对同一个对象或资源进行操作时不会出错

一般有如下解决思路

  • 互斥同步:synchronized和ReentrantLock(悲观策略)
  • 非阻塞同步:CAS,Atomic原子类(乐观策略)
  • 无同步方案:栈封闭,本地存储(ThreadLocal),可重入代码

官方的表述是这样的

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID) 该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

总而言之,ThreadLocal是一个将多线程每一个线程创建单独的变量副本的类,当使用ThreadLocal来维护变量的时候,ThreadLocal会为每个线程创建单独的变量副本,避免多线程操作共享变量导致的数据不一致的情况

原理

如何实现线程隔离

这是threadlocal作连接池的代码

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectionManager {

    private static final ThreadLocal<Connection> dbConnectionLocal = new ThreadLocal<Connection>() {
        @Override
        protected Connection initialValue() {
            try {
                return DriverManager.getConnection("", "", "");
            } catch (SQLException e) {
                e.printStackTrace();
            }
            return null;
        }
    };

    public Connection getConnection() {
        return dbConnectionLocal.get();
    }
}

那么这种情况下使用ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。下面就是网上出现最多的例子:

主要是用到了Thread对象中的ThreadLocalMap类型的变量,threadLocals,负责存储当前线程关于的对象

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

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对象中并返回。

同时, 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);
}

那么现在 就很清晰的知道了为什么ThreadLocal能够实现多线程隔离了,

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

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

ThreadLocalMap

本质上来讲,就是一个map,但是这个ThreadLocalMap和平时见到的不一样

  • 没有实现map接口
  • 它没有public方法,有一个默认的构造方法,进在ThreadLocal类中调用,属于静态内部类
  • ThreadLocalMap的entry实现了WeakReference<ThreadLocal<?>> (弱引用)
  • 该方法仅仅用了一个数组来存储key与value;entry并不是链表形式,而是每个bucket里面仅仅放一个entry
 static class ThreadLocalMap {}

方法

set方法

要了解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);

    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,看i2shangmian的元素enrty是否等于threadlocal这个key,如果等于直接将该位置上面的entry的value替换成最新的就好了
  • 如果当前位置上面的entry的key为空,说明ThreadLocal对象已经被回收了,那么就调用replaceStaleEntry
  • 如果清理完无用条目(ThreadLocal被回收的条目)并且数组中的数据大小>阈值的时候,就对当前的table进行rehash,所以该map处理冲突的机制是向后移位,清除过期条目,最终找到合适的位置

这是一个参数的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方法

了解完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是同一个对象,啧直接返回,否则去后面的索引位置去查找

内存泄漏

例子

网上有这样一个例子:

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadLocalDemo {
    static class LocalVariable {
        private Long[] a = new Long[1024 * 1024];
    }

    // (1)
    final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());
    // (2)
    final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();

    public static void main(String[] args) throws InterruptedException {
        // (3)
        Thread.sleep(5000 * 4);
        for (int i = 0; i < 50; ++i) {
            poolExecutor.execute(new Runnable() {
                public void run() {
                    // (4)
                    localVariable.set(new LocalVariable());
                    // (5)
                    System.out.println("use local varaible" + localVariable.get());
                    localVariable.remove();
                }
            });
        }
        // (6)
        System.out.println("pool execute over");
    }
}

如果用线程池来操作ThreadLocal对象确实会造成内存泄漏,因为对于线程池里面不会销毁的线程,里面总会存着<ThreadLocal,LocalVariable>的强引用,因为final static修饰的ThreadLocal并不会释放,而ThreadLocalMap对于key虽然是弱引用,但是强引用不会释放,弱引用当然也会一直有值,同时创建的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;
        }
    }
}

找到对应的key,并且清除Entry的Key(ThreadLocal)置空,随后过期清除的Entry即可避免内存泄漏

原因:

由于ThreadLocal对象是弱引用,如果外部没有引用指向它,它就会被GC回收,导致Entry的key为null

如果当前情况下在栈中将threadlocal1的引用设置为null,强引用1将会失效,那堆中的threadLocal1对象因为ThreadLocalMap的key对它的引用是弱引用,将会在下一次被gc回收,那就会出现key变成null,如果这时value外部没有强引用指向他,那么value就永远也不能访问到了,按理应该被gc回收,但是由于ThreadLocalMap.Entry还在强引用value,导致value无法被回收,这时[内存泄漏]就发生了,value成了一个永远无法被访问,但是又无法被回收的对象

解决方式

  1. 将ThreadLocal设置为空之前,执行remove()方法,会将key为空的键值对清空
  2. 尽量将ThreadLocal设置成static

原理

threadlocal自身并不存储值,而是作为一个key让线程来从ThreadLocal获取value.

entry中的key是弱引用,所以jvm在垃圾回收时,如果外部没有强引用来引用它,ThreadLocal必然会被回收.但是作为ThreadLocalMap的key,ThreadLocal被回收后,ThreadLocalMap就会存在null,但valuye不为null的entry.弱当前线程一直不结束,可能是作为线程池中的一员,线程结束后不被销毁,或者分配(当前线程又创建了ThreadLocal对象)使用了又不再调用get/set方法,就看引发内存泄漏.

key弱引用并不是导致内存泄漏的原因,而是因为ThreadLocalMap的生命周期与当前线程一样长,并且没有手动删除对应value。

那么为什么要降entry中的key设置为弱引用?相反,设置为弱引用的key能语法大多数内存泄漏的情况.

如果key使用强引用,引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,threadlocal不会被回收,导致entry内存泄漏,如果key为弱引用,引用的threadLocal对象回收了,由于ThreadLocalMap持有ThreadLocal弱引用,即使没有手动删除,ThreadLocal也会被gc回。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除

前面已经说过,由于Key是弱引用,因此ThreadLocal可以通过key.get()==null来判断Key是否已经被回收,如果Key被回收,就说明当前Entry是一个废弃的过期节点,ThreadLocal会自发的将其清理掉。

hreadLocal会在以下过程中清理过期节点:

调用set()方法时,采样清理、全量清理,扩容时还会继续检查。
调用get()方法,没有直接命中,向后环形查找时。
调用remove()时,除了清理当前Entry,还会向后继续清理。

Last modification:November 21, 2022
如果觉得我的文章对你有用,请随意赞赏