本地线程-ThreadLocal

Posted trytired

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了本地线程-ThreadLocal相关的知识,希望对你有一定的参考价值。

线程本地存储是一个自动化机制,可以为使用相同变量的每个不同的线程都创建不同的存储。简单来说,就是对于某个变量,针对不同的线程存储不同的值。

实例:

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @Description
 * @Author KToTo
 * @Date 2019/3/18 22:22
 **/
public class ThreadLoaclVariableHolder {
    //创建一个全局的ThreadLocal对象
    private static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
        private Random random = new Random(47);
        //初始化方法,此处的Random相当于共享变量,为了使演示效果明显,
        //故将该初始化方法同步
        protected synchronized Integer initialValue() {
            return random.nextInt(1000);
        }
    };
    
    //提供公有的递增和获取方法
    public static void increment() {
        value.set(value.get() + 1);
    }

    public static int get() {
        return value.get();
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newCachedThreadPool();
        for (int i = 0; i < 5; i++) {
            pool.execute(new Accessor(i));
        }
        //与Thread.sleep(long mils)方法功能一致
        TimeUnit.SECONDS.sleep(3);
        //立即关闭线程池,不熟悉的读者可以自行百度shutdown和shutdownNow的区别
        pool.shutdownNow();
    }
}

class Accessor implements Runnable {
    private final int id;

    public Accessor(int id) {
        this.id = id;
    }

    @Override
    public void run() {
        //响应中断
        while (!Thread.currentThread().isInterrupted()) {
            ThreadLoaclVariableHolder.increment();
            System.out.println(this);
            //对于多核处理器,此方法可以省略
            Thread.yield();
        }
    }

    @Override
    public String toString() {
        return "Accessor{" +
                "id=" + id + ":" + ThreadLoaclVariableHolder.get() +
                ‘}‘;
    }
}

原理分析

  • 从概念上来看,你可以将ThreadLocal<T>视为包含了Map<Thread, T>对象,其中保存了特定于该线程的值,但是实际上并非如此,这些特定于线程的值其实是保存在Thread对象中的,当线程终止后,这些值会作为垃圾回收。
  • 首先简单概括一下,当我们使用ThreadLocal的时候,我们想要的效果是针对该共享变量,每个线程中访问该变量的值是不一样的,而且是互不影响的。而ThreadLocal可以达到这一效果,其实是在每一个Thread对象中都有一个ThreadLocalMap的实例对象,当当前线程对改ThreadLocal进行初始化的时候,会以ThreadLocal的对象为key,值为value,存放到当前线程对象中的ThreadLocalMap。
  • 在上述的示例中,我们可以看到我们直接使用了ThreadLocal的Get方法,而不是先Set然后Get,下面让我们从Get方法入手,来了解一下ThreadLocal的实现

  

public T get() {
    Thread t = Thread.currentThread();
    //获取当前线程Thread对象中存放的ThreadLocalMap。
    ThreadLocalMap map = getMap(t);
    //判断当前对象是否存在本地线程对象
    if (map != null) {
        //判断当前线程对象中的threadLocals是否存储当前对象(注意这个this)的值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //如果上述两个判断均不满足,则进行初始化
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
//关于ThreadLocalMap类的定义请参考源码
public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
/**
 *调用重写的initialValue()方法获取初始值,
 *然后将当前ThreadLocal的初始化值添加到Thread对象的threadLocals变量中。
 **/
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;
}

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的副作用

  • 脏数据,线程复用会导致产生脏数据。由于线程池会重用Thread对象,那么与Thread绑定的静态属性ThreadLocal变量也会被复用
  • 内存泄露,当ThreadLocal为静态属性时,就不可以寄希望与ThreadLocal对象失去引用时,触发软引用机制来回收就不现实了。
  • 针对上述两种问题的解决办法就是在每次使用完ThreadLocal之后,必须要及时的调用remove()方法清理。

参考:《Thinking In Java》、《Java并发编程实战》、《码出高效》

以上是关于本地线程-ThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章

ThreadLocal内存泄露原因分析

Java并发多线程编程——ThreadLocal(线程本地存储)

ThreadLocal 线程本地变量 及 源码分析

深入理解线程本地变量ThreadLocal

当ThreadLocal碰上线程池

Java中的线程本地变量ThreadLocal