ThreadLocal的应用及原理

一、Thread-Local变量

1、ThreadLocal 定义

  官方JDK的定义:此类提供线程局部变量。这些变量与其正常对应变量的不同之处在于,每个访问一个(通过其get或set方法)的线程都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,这些字段希望将状态与线程(例如,用户ID或事务ID)相关联。
例如,下面的类生成每个线程本地的唯一标识符。线程的id在第一次调用ThreadId.get()时被分配,并且在随后的调用中保持不变。

ThreadLocal是用来存放线程相关数据的一个容器,这个容器叫做ThreadLocalMap,它是ThreadLocal的一个静态内部类,同时作为Thread类的一个成员变量。ThreadLocal在使用时,先拿到当前线程的成员变量ThreadLocalMap,以当前的ThreadLocal对象作为key,变量作为value存入ThreadLocalMapThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。然后每个线程取变量都是从线程各自的ThreadLocalMap中取值,自然是线程安全的了。因为变量只在自己线程的生命周期内起作用,所以说ThreadLocal提供线程局部变量,或者叫线程本地变量。

ThreadLocal实例通常总是以静态字段初始化如下:

static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();

 2、ThreadLocal的使用

ThreadLocal 的常用方法:

  1. public ThreadLocal():通过构造器创建对象。一般是静态的。
  2. static <S> ThreadLocal<S>withInitial(Supplier<? extends S> supplier):初始化一个 ThreadLcoal。
  3. void set(T value):设置当前线程绑定的局部变量。
  4. T get():返回此线程局部变量的当前线程副本中的值。
  5. void remove():删除当前线程绑定的局部变量。

对于多任务,Java标准库提供的线程池可以方便地执行这些任务,同时复用线程。Web应用程序就是典型的多任务应用,每个用户请求页面时,我们都会创建一个任务,类似:

public void process(User user) {
    checkPermission();
    doWork();
    saveStatus();
    sendResponse();
}

process()方法需要传递的状态就是User实例。

这种在一个线程中,横跨若干方法调用,需要传递的对象,我们通常称之为上下文(Context),它是一种状态,可以是用户身份、任务信息等。

给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User对象就传不进去了。

Java标准库提供了一个特殊的ThreadLocal,它可以在一个线程中传递同一个对象。

ThreadLocal实例通常总是以静态字段初始化如下:

static ThreadLocal<User> threadLocalUser = new ThreadLocal<>();

它的典型使用方式如下:

void processUser(user) {
    try {
        threadLocalUser.set(user);
        step1();
        step2();
    } finally {
        threadLocalUser.remove();
    }
}

通过设置一个User实例关联到ThreadLocal中,在移除之前,所有方法都可以随时获取到该User实例:

void step1() {
    User u = threadLocalUser.get();
    log();
    printUser();
}

void log() {
    User u = threadLocalUser.get();
    println(u.name);
}

void step2() {
    User u = threadLocalUser.get();
    checkUser(u.id);
}

注意到普通的方法调用一定是同一个线程执行的,所以,step1()step2()以及log()方法内,threadLocalUser.get()获取的User对象是同一个实例。

实际上,可以把ThreadLocal看成一个全局Map<Thread, Object>:每个线程获取ThreadLocal变量时,总是使用Thread自身作为key:

Object threadLocalValue = threadLocalMap.get(Thread.currentThread());

因此,ThreadLocal相当于给每个线程都开辟了一个独立的存储空间,各个线程的ThreadLocal关联的实例互不干扰。

最后,特别注意ThreadLocal一定要在finally中清除:

try {
    threadLocalUser.set(user);
    ...
} finally {
    threadLocalUser.remove();
}

这是因为当前线程执行完相关代码后,很可能会被重新放入线程池中,如果ThreadLocal没有被清除,该线程执行其他代码时,会把上一次的状态带进去。

为了保证能释放ThreadLocal关联的实例,我们可以通过AutoCloseable接口配合try (resource) {...}结构,让编译器自动为我们关闭。例如,一个保存了当前用户名的ThreadLocal可以封装为一个UserContext对象:

public class UserContext implements AutoCloseable {

    static final ThreadLocal<String> ctx = new ThreadLocal<>();

    public UserContext(String user) {
        ctx.set(user);
    }

    public static String currentUser() {
        return ctx.get();
    }

    @Override
    public void close() {
        ctx.remove();
    }
}

3、ThreadLocal原理解析

ThreadLocal 的原理要从它的set(T value)get()、remove()方法的源码入手:

1、set()方法

public void set(T value) {
        //获取当前线程
    Thread t = Thread.currentThread();
    //获取维护当前线程变量的ThreadLocalMap数据,一种类似于HashMap的数据结构
    ThreadLocalMap map = getMap(t);
    //如果当前线程已经存在了Map,直接调用map.set
    if (map != null)
        map.set(this, value);
    //不存在Map,则先进行新增map,再进行set
    else
        createMap(t, value);
}

set方法中出现了一个ThreadLocalMap这个数据结构,点进去看一下

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
        
        //代码太多不一一贴出来了
}

其中维护了一个entry结构用来用来维护节点的数据,细心地同学应该已经发现了Entry这个结构继承了WeakReference,从构造方法可以看出,ThreadLocalMap的Key是软引用维护的。这个地方很重要,至于为什么重要,后面再细说。

再继续点击一下发现ThreadLocal成员变量里面定义了这么一句话

ThreadLocal.ThreadLocalMap threadLocals = null;

这句话的出现表明了,针对于每一个线程,都是独立维护一个ThreadLocalMap,一个线程也可以拥有多个ThreadLocal变量。

2、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();
}
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;
}
protected T initialValue() {
    return null;
}

get()方法整体上比较简单,贴上了关键逻辑逻辑代码,调用get()时,如果存在值,则将值返回,不存在值调用setInitialValue()获取值,其中初始化的值为null,也就是说如果ThreadLocal变量未被赋值,或者赋值后被remove掉了,直接调用get()方法不会报错,将会返回null值。

3、remove()方法

//ThreadLocal
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
//ThreadLocalMap
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode &amp; (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

remove方法调用时会判断当前线程中ThreadLocalMap是否存在,如果存在则调用ThreadLocalMap.remove(key);遍历链表结构移除entry节点

4、ThreadLocal 如何存多个变量

ThreadLocal 使用set方法存数据时,key 用的this对象,就是当前正在使用的 ThreadLocal 对象,说明一个 ThreadLocal 对象,在一个线程中,只能存一个线程本地变量。多个线程虽然都是用的是一个 key,但是不同的线程用的是不同的ThreadLocalMap

具体做法有两种:

1、 生成ThreadLocal 对象,每个 ThreadLocal 对象对应一个业务变量

2、给 ThreadLocal 初始化一个HashMap,这是最常规的做法。比如下面:

public class ThreadLocalTest {
    private static final ThreadLocal<Map<String, Object>> context =
            ThreadLocal.withInitial(HashMap::new);

    private String getUserId() {
        return String.valueOf(context.get().get("userId"));
    }

    private void setUserId(String userId) {
        context.get().put("userId", userId);
    }

    public void setUserName(String userName) {
        context.get().put("userName", userName);
    }

    public String getUserName() {
        return String.valueOf(context.get().get("userName"));
    }

    public static void main(String[] args) {
        ThreadLocalTest test = new ThreadLocalTest();
        for (int i = 1; i < 5; i++) {
            Thread thread = new Thread(() -> {
                String threadName = Thread.currentThread().getName();
                test.setUserId(threadName + "的userId");
                test.setUserName(threadName + "的userName");
                System.out.println("===执行业务代码===");
                System.out.println(threadName + "-->" + test.getUserId() + "," + test.getUserName());
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

二、继承Thread-Local变量 

当父线程启动子线程时,父线程的线程局部变量的值都不会被子线程继承。但是,如果您希望子线程继承其父线程局部值的值,请使用Inheritable ThreadLocal类创建一个线程局部变量。
除了名为TLUSER的ThreadLocal变量外,以下示例还包括名为TLADMIN的Inheritable ThreadLocal变量:

public class TLDBConn {
    
    final static ThreadLocal<User> TLUSER = new ThreadLocal<>();
    final static InheritableThreadLocal<User> TLADMIN = new InheritableThreadLocal<>();
    
    public static String open(String info) {
        System.out.println(info + ": " + TLUSER.get().name);
        return info + ": " + TLUSER.get().name;
    }
}

以下方法在线程中启动一个名为childThread的线程。线程childThread检索名为TLADMIN的Inheritable ThreadLocal变量的值,并尝试检索名为TLSUER的ThreadLocal变量:

    public void testConnectionWithInheritableTL(User u) {
        
        Runnable r = () -> {
            TLDBConn.TLUSER.set(u);
            TLDBConn.TLADMIN.set(new User("Admin"));
            TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");
            System.out.println(TLServer.fetchOrder());
            
            try {
                Thread.sleep(new Random().nextInt(1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            
            Thread childThread = new Thread(
                () -> {
                    System.out.println("Child thread");
                    System.out.println("TLADMIN: " + TLDBConn.TLADMIN.get().name);
                    try {
                        System.out.println("TLUSER: " + TLDBConn.TLUSER.get().name);
                    } catch (NullPointerException e) {
                        System.out.println("NullPointerException: TLUSER hasn't beet set");
                    }
                }
            );
            childThread.start();
                    
                        
            TLDBConn.TLUSER.set(new User(u.name + " renamed"));
            TLDBConn.open("Thread " + Thread.currentThread().getName() + ", testConnection");            
        };
        
        Thread t = new Thread(r, u.name);
        t.start();
    }

当您调用此方法时,childThread实例化中的以下语句会抛出NullPointerException:

System.out.println("TLUSER: " + TLDBConn.TLUSER.get().name);


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值