ThreadLocal

2020-03-12  本文已影响0人  陆阳226

ThreadLocal

ThreadLocal提供了线程本地变量,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal变量通常被private static修饰。当一个线程结束,它所使用的所有ThreadLocal变量相对的实例副本都会被回收。

基础使用

public class UserInfoHandler {
    private static ThreadLocal<UserInfo> threadLocal = ThreadLocal.withInitial(() -> new UserInfo());

    private static void test() {
        for (int i = 0; i < 5; i++) {
            int j = i;
            new Thread(() -> {
                UserInfo userInfo = threadLocal.get();
                userInfo.setName("user" + j);
                System.out.println("[" + Thread.currentThread().getName() + "]: " + userInfo);
            }).start();
        }
    }

    public static void main(String[] args) {
        test();
    }
}

@Data
class UserInfo {
    String id;
    String name;

    public UserInfo() {
        this.id = UUID.randomUUID().toString();
    }
}

每个线程都有自己独立的本地变量,相互之间不会影响

[Thread-4]: UserInfo(id=a456e9df-7267-41e3-bfc4-09c835dc2131, name=user4)
[Thread-0]: UserInfo(id=b69aa145-c879-4c55-8190-85148f839571, name=user0)
[Thread-2]: UserInfo(id=d4ac0367-3077-488b-86f0-5c481dd51c8b, name=user2)
[Thread-3]: UserInfo(id=a3f1a000-165a-4c61-b521-c4c4500e3862, name=user3)
[Thread-1]: UserInfo(id=9771ad8c-7c7b-4d23-b59d-92880b3406ad, name=user1)

主要方法

Thread对象持有ThreadLocal.ThreadLocalMap threadLocals默认为null,ThreadLocalMap是ThreadLocal类内部定义一个哈希表,只用于维护线程局部值,保存ThreadLocal对象和局部值的键值对。

set方法:设置当前线程中ThreadLocal对应的value值

public void set(T value) {
    // 获取当前执行线程
    Thread t = Thread.currentThread();
    // 获取线程t中的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

getMap,createMap方法:直接返回线程t的字段,创建ThredLocalMap赋值给线程t的threadLocals

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

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

get方法:如果ThreadLocalMap中有当前ThreadLocal对象的key则返回value,否则调用setInitialValue方法设置初始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();
}

setInitialValue方法:设置初始值,不需要使用set方法也可以get到value。调用initialValue()获取value,然后有map就设置,没有就创建并设置

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);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

initialValue是ThreadLocal的protected方法,默认返回null,重写该方法设置初始值

protected T initialValue() {
    return null;
}

重写initialValue方法示例

ThreadLocal<String> mThreadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
      return Thread.currentThread().getName();
    }
};

ThreadLocal有已经定义好的重写initialValue方法的类SuppliedThreadLocal,使用方法withIntial即可,而且可以写成lambda的形式

static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

    private final Supplier<? extends T> supplier;

    SuppliedThreadLocal(Supplier<? extends T> supplier) {
        this.supplier = Objects.requireNonNull(supplier);
    }

    @Override
    protected T initialValue() {
        return supplier.get();
    }
}

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

搭配线程池

当线程结束时,它所使用的所有的ThreadLocal变量相对的实例副本都会被回收。但使用线程池时,线程是复用的,被复用的线程会保留其上次使用的ThreadLocal变量及相对的实例。

使用线程池来执行任务:

private static void testWithPool() {
    ExecutorService service = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 5; i++) {
        int j = i;
        service.execute(() -> {
            UserInfo userInfo = threadLocal.get();
            userInfo.setName("user" + j);
            System.out.println("[" + Thread.currentThread().getName() + "]: " + userInfo);
        });
    }
    try {
        Thread.sleep(100);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    service.shutdown();
}

执行结果,线程池中2个线程复用导致了UserInfo的id重复,遗留了上次任务的值

[pool-1-thread-2]: UserInfo(id=3836447b-e7cf-4be5-b68e-f867405124f3, name=user1)
[pool-1-thread-1]: UserInfo(id=ef42d5b5-233e-4fbc-a3a6-c14ce4dc0948, name=user0)
[pool-1-thread-2]: UserInfo(id=3836447b-e7cf-4be5-b68e-f867405124f3, name=user2)
[pool-1-thread-1]: UserInfo(id=ef42d5b5-233e-4fbc-a3a6-c14ce4dc0948, name=user3)
[pool-1-thread-2]: UserInfo(id=3836447b-e7cf-4be5-b68e-f867405124f3, name=user4)

如果不想出现这种情况,解决办法是在执行任务之后调用ThreadLocal的remove方法,将ThreadLocal从当前线程的ThreadLocalMap中删除

service.execute(() -> {
    try {
        UserInfo userInfo = threadLocal.get();
        userInfo.setName("user" + j);
        System.out.println("[" + Thread.currentThread().getName() + "]: " + userInfo);
    } finally {
        threadLocal.remove();
    }
});

有些使用场景却是需要线程池复用线程时保留上次任务的ThreadLocal,simpleDateFormat就是一个;simpleDateFormat在多线程并发时不是安全的,使用了ThreadLocal封装之后解决了多线程并发不安全的问题,但是每启用一个线程都会创建一个simpleDateFormat对象,这时候在使用线程池时保留上次任务中的simpleDateFormat对象,可以减少重复创建对象,就不能使用remove了。

上一篇下一篇

猜你喜欢

热点阅读