ThreadLocal

1.定义

线程本地的变量副本,属于每个线程独有。每个线程使用ThreadLocal设置自己的值,设置的值之间不受影响,但是使用同一个ThreadLocal对象。所以设置的每个变量,是给每个线程一个独有的变量副本

2.简单使用

public class HelloThreadLocal {
    private static ThreadLocal<Loan> threadLocal = new ThreadLocal<Loan>();

    public static void main(String[] args) {
        //线程1 使用threadLocal设置自己的变量副本
        new Thread(() -> {
            threadLocal.set(new Loan("zhangsan", "1000.00"));
            System.out.println("线程-1loan:" + threadLocal.get());
        }).start();

        //线程2 使用threadLocal设置自己的变量副本
        new Thread(() -> {
            try {
                Thread.sleep(1 * 1000);
            } catch (InterruptedException e) {
            }
            HelloThreadLocal.Loan loan = threadLocal.get();
            System.out.println("线程-2loan:" + loan);
            threadLocal.set(new Loan("lisi", "2000.00"));
            loan = threadLocal.get();
            System.out.println("线程-2loan:" + loan);

        }).start();

        try {
            Thread.sleep(5 * 1000);
        } catch (InterruptedException e) {
        }

        System.out.println("main-线程loan:" + threadLocal.get());
        threadLocal.set(new Loan("wangwu", "1000.00"));
        System.out.println("main-线程loan:" + threadLocal.get());

    }

    @Data
    @AllArgsConstructor
    public static class Loan {
        private String name;
        private String amount;
    }
}

输出结果:

线程-1loan:HelloThreadLocal.Loan(name=zhangsan, amount=1000.00)
线程-2loan:null
线程-2loan:HelloThreadLocal.Loan(name=lisi, amount=2000.00)
main-线程loan:null
main-线程loan:HelloThreadLocal.Loan(name=wangwu, amount=1000.00)

可以看出,每个线程无法获取到其它线程设置的loan对象

3.源码分析

1.get方法

首先获取当前线程的ThreadLocalMap变量,如果map为空的话调用setInitialValue方法返回默认值,如果map不为空获取entry中key对应的value值

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

getMap方法:

threadLocals变量用于存储当前线程自身的ThreadLocal,所以虽然使用的是ThreadLocal的get方法,但是操作的实际是当前线程的threadLocals本地遍历副本的Map

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

setInitialValue方法:

首先初始化value为null,然后当前线程获取map,如果为null的话,创建map,否则直接set,这里会返回null,所以get方法也会返回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;
}

2.set方法

如果是第一次使用set方法,创建一个默认大小为16的ThreadLocalMap,并将key设为ThreadLocalMap对象,value用传入的value,如果不是第一次直接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);
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

3.ThreadLocalMap

ThreadLocalMap中的核心结构是一个Entry,用来存储key-value数据:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

这里继承了弱引用,弱引用是只要gc发现了它,就会回收掉,这里的key使用弱引用是为了防止内存泄漏,因为如果key是强引用的话,当一个threadLocal对象为null后,还是会指向它,导致该对象不能被回收,造成内存泄漏,这里虽然key使用了弱引用,但是还是存在value指向的强引用,所以需要用remove方法来删除之前的key,不然还是会造成内存泄漏

4.应用场景

1.Spring的Transaction机制中,将一个线程中的事务放入ThreadLocal中,可以在整个方法调用栈中随时取出事务的信息进行操作,不会影响其它线程

2.Log4j2等日志框架中的MDC

3.HDFS edits_log的txId自增后放入线程本地副本,HDFS的每条edit_log都有一个txId,会将这个txId记录到当前线程方便在整个线程过程中随时取用

小结:

ThreadLocal最常用的2个场景就是:

1.线程中,在各个方法需要共享变量时使用。除了方法之间传递入参,通过ThreadLocal可以很方便的做到这一点

2.多线程操作时,防止并发冲突,保证线程安全。比如一般会拷贝一份数据到线程本地,自己修改本地变量,是线程安全的

posted @ 2020-09-02 21:56  马晟  阅读(144)  评论(0编辑  收藏  举报