浅谈ThreadLocal

来源:互联网 发布:javascript表单提交 编辑:程序博客网 时间:2024/05/23 19:53

今天将探讨下JDK源码中出现频次很高的关键字–ThreadLocal,线程本地变量。

基本概念

ThreadLocal类,究竟是干啥的,首先来看java官方文档的说明:
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
简单的说,每个线程都有同一个变量的独立副本。

原理及用法

先看实例:

public class TestThreadLocal {    static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();    public static void main(String[] args) throws InterruptedException {        Thread child1 = new Thread() {            @Override            public void run() {                System.out.println("child1 thread initial: " + threadLocal.get());                threadLocal.set(200);                System.out.println("child1 thread final: " + threadLocal.get());            }        };        Thread child2 = new Thread() {            @Override            public void run() {                System.out.println("child2 thread initial: " + threadLocal.get());                threadLocal.set(300);                System.out.println("child2 thread final: " + threadLocal.get());            }        };        threadLocal.set(100);        child1.start();        child2.start();        child1.join();        child2.join();        System.out.println("main thread final: " + threadLocal.get());    }}

运行结果:

child1 thread initial: nullchild1 thread final: 200child2 thread initial: nullchild2 thread final: 300main thread final: 100

从这个实例中可以看到ThreadLocal类的神奇之处,main线程对threadLocal变量的设置对child线程无影响,child线程对threadLocal变量改变也不会作用至main线程,他们虽然访问的同一个变量,但是每个线程有自身独立的变量副本。

来看ThreadLocal的主要方法:

ThreadLocal()就不用说了,无参的构造方法。
来看get()方法,获取值

public T get() {        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null) {            ThreadLocalMap.Entry e = map.getEntry(this);            if (e != null)                return (T)e.value;        }        return setInitialValue();    }ThreadLocalMap getMap(Thread t) {    return t.threadLocals;}

从源码可以看出,根据当前线程获取到一个map,然后以当前ThreadLocal为key查询对应的value,如果value等于null,返回默认设置的value。每个线程都有一个Map,类型为ThreadLocalMap,调用set实际上是在线程自己的Map里设置了一个条目,键为当前的ThreadLocal对象,值为value。ThreadLocalMap是一个内部类,是专门用于ThreadLocal的,与一般的Map不同,它的键类型为WeakReference 弱引用类型,使用弱引用类型便于垃圾回收。

继续看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;}

可以看到,value是从initialValue方法获取然后添加至map中,然而initialValue返回null,这是咋回事?值得注意的是initialValue方法是protected修饰的,说明此方法就是为方便子类覆盖设计的,value需要在子类中initialValue方法设定。

再看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);    }

没什么好说的,和setInitialValue方法差不多。
接着remove()

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

从map中移除该线程键值。

每个线程都有一个Map,对于每个ThreadLocal对象,调用其get/set实际上就是以ThreadLocal对象为键读写当前线程的Map,就这样实现了每个线程都有自己的独立副本。

应用场景

1、多线程操作DateFormat/SimpleDateFormat时间日期类
对于DateFormat/SimpleDateFormat这类时间日期类,是非线程安全的,实现安全的方式之一是加锁,但更好的方式是使用ThreadLocal,每个线程用自己的变量副本。来看代码:

static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(){protected DateFormat initialValue(){    return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");    }};public static String dateToString(Date date){    return threadLocal.get().format(date);}

2、上下文信息
ThreadLocal的典型用途是提供上下文信息,比如在一个Web服务器中,一个线程执行用户的请求,在执行过程中,很多代码都会访问一些共同的信息,比如请求信息、用户身份信息、数据库连接、当前事务等,它们是线程执行过程中的全局信息,如果作为参数在不同代码间传递,代码会很啰嗦,这时,使用ThreadLocal就很方便,所以它被用于各种框架如Spring中,我们看个简单的示例:

public class RequestContext {    public static class Request { //...    };    private static ThreadLocal<String> localUserId = new ThreadLocal<>();    private static ThreadLocal<Request> localRequest = new ThreadLocal<>();    public static String getCurrentUserId() {        return localUserId.get();    }    public static void setCurrentUserId(String userId) {        localUserId.set(userId);    }    public static Request getCurrentRequest() {        return localRequest.get();    }    public static void setCurrentRequest(Request request) {        localRequest.set(request);    }}

在首次获取到信息时,调用set方法如setCurrentRequest/setCurrentUserId进行设置,然后就可以在代码的任意其他地方调用get相关方法进行获取了。

最后,由于ThreadLocal的里ThreadLocalMap是弱引用类型,所以在线程池中使用时需要注意清除引用,具体可阅读《(82) 理解ThreadLocal / 计算机程序的思维逻辑》。

总结

1、ThreadLocal是使每个线程都有自己的变量副本,每个线程都有自己的map,是以ThreadLocal对象作为键存储数据的。
2、ThreadLocal经常用于存储上下文信息,简化代码。

0 0
原创粉丝点击