多线程-ThreadLocal

别等时光非礼了梦想. 提交于 2020-02-09 17:00:36

1、ThreadLocal是什么?

  ThreadLocal这个类提供线程局部变量,这些变量与其他正常的变量不同之处在于每一个访问该变量的线程在其内部都有一个独立的初始化的变量副本。ThreadLocal实例变量通常采用private static在类中修饰。

  只要ThreadLocal的变量能被访问,并且线程存货,那每个线程都会持有ThreadLocal变量的副本。当一个线程结束时,它所持有的所有ThreadLocal相对的实例副本都可被回收。

  ThreadLocal和synchronized都是用于解决多线程并发访问,但是synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。synchronized利用锁机制,是变量或代码块在某一时刻只能被一个线程访问,而ThreadLocal为每一个线程都提供了变量的副本,使每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

2、ThreadLocal的使用?

  2.1使用ThreadLocal实现接口调用重试功能。

  需求说明:http请求调用第三方应用的过程中可能会发生因为网络抖动而失败的情况,那这样我们就需要有重试机制,尽量保证调用成功。我们想到可用ThreadLocal做重试的功能。

 Demo地址:https://gitee.com/burning-myself/thread-demo

 2.1.1、新建一个service类,用来调用第三方接口:

public class RequestAgainService {

    /**
     * 重试用到的ThreadLocal,默认重试3次
     */
    private static ThreadLocal<Integer> retryTimesThread = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 3;
        }
    };
}

  2.1.2 写调用第三方接口的方法:

这里注意的是调用threadLocal.remove()方法删除当前线程。后面会解释

/**
 * 调用第三方,获取用户信息
 * @param i 参数,传入0表示调用异常,其他数,调用正常
 */
public void gotoGetUserInfo(int i) {
    if(retryTimesThread.get() <= 0) {
        //重试次数结束后,删除当前线程的threadLocal
        retryTimesThread.remove();
        return;
    }

    try {
        requestThirdApp(i);
        //请求成功后,删除当前线程的threadLocal
        retryTimesThread.remove();

    } catch (Exception e) {
        Integer retryTimes = retryTimesThread.get();

        if(retryTimes < 0) {
            //重试调用完成后,删除当前线程的threadLocal
            retryTimesThread.remove();
        } else {
            System.out.println(Thread.currentThread().getName() + "重试" + retryTimes + "次!");
            retryTimesThread.set(--retryTimes);
            gotoGetUserInfo(i);
        }

    }

}

   2.1.3 模拟多个客户端调用

public class ClientThread {
    public static void main(String[] args) {
        RequestAgainService service = new RequestAgainService();
        for(int j = 0; j <3; j++) {
            Thread t = new Thread(new Client(service, 0));
            t.setName("线程a" + j);
            t.start();
        }
    }
}

  客户端:

class Client implements Runnable {
    private RequestAgainService service;
    private Integer i;

    public Client(RequestAgainService service, Integer i) {
        this.service = service;
        this.i = i;
    }

    @Override
    public void run() {
        service.gotoGetUserInfo(i);
    }
}

3、ThreadLocal的原理?

    ThreadLocal的实现是这样的:每个Thread维护一个ThreadLocalMap映射表,这个映射表的keyThreadLocal实例本身,value是真正需要存储的ObjectThreadLocal类比较主要的方法是set()get()方法。我们先从这两个方法看起。

        set(T value) 方法中,通过当前线程从ThreadLocalMap中获取map,如果获取到了,就往这个map中设定值,如果没有获取到就新创建ThreadLocalMap对象并设定value值。每个线程都会有一个ThreadLocalMap

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

        看一下createMap() 方法,这个方法就是new了一个ThreadLocalMap对象

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

        ThreadLocalMapThreadLocal中,是一个静态内部类。

static class ThreadLocalMap {
   ......
)

        构造函数中我们看到,它初始化了一个Entry类型的数据,对firstKey进行hash的计算后,把Entry对象放到数组中的某一个位置。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

        Entry对象继承了WeakReference,说明Entry是个弱引用。在垃圾收集器线程扫描内存区域的过程中,一旦发现了只有弱引用对象,不管当前内存空间是否足够,都会回收它的内存。不过垃圾回收器是一个优先级很低的线程,因此不一定很快能够发现那些只有弱引用的对象。

static class Entry extends WeakReference<ThreadLocal<?>> {
  ...
}

        T get()方法中,根据当前线程t 获取到了ThreadLocalMap,找到Map后再通过key找到value,如果当前线程并没有对应的map,那就返回一个setInitialValue()

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()方法、首先是初始化一个值,这个initialVaue()是可以在创建ThreadLocal的时候就可以设定的,比如2.1.1 如果没有设定,那这个value就是null。后面的逻辑就跟               set(T value) 的逻辑一样了。

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

4、ThreadLocal的内存泄漏问题?

       Java为了最小化减少内存泄漏的可能性和影响,在ThreadLocalget,set的时候会清除线程Map里所有keynullvalue。所以最怕的情况就是,threadLocal对象设null了,开始发生内存泄漏,然后使用线程池,这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄漏。

        ThreadLocalMap使用TheradLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统在GC的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue,如果当前线程再迟迟不结束的话,这些keynullEntryvalue就会一直存在一条强引用链:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。

       其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocalget()set()remove()的时候都会清除线程ThreadLocalMap里所有的keynullvalue。但是这些被动的防御措施并不能保证不会内存泄漏。因此ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。那么怎么避免内存泄漏呢?要每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

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

        remove(ThreadLocal<?> key) 方法循环Entry[]数据,找到key对应的Entry的对象,然后调用clear()方法和expungeStaleEntry() 方法

private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

 这两个方法主要的作用就是把相关的引用设置为null

public void clear() {
    this.referent = null;
}
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

  参考:

【1】《Java高并发程序设计》,葛一鸣

【2】ThreadLocal使用场景分析,https://www.jianshu.com/p/f956857a8304

【3】面试中的ThreadLocal原理和使用场景,https://blog.csdn.net/ityouknow/article/details/90709371

【4】深入分析ThreadLocal内存泄漏问题,https://www.jianshu.com/p/1342a879f523

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!