Java并发——ThreadLocal分析

·  阅读 2129

简述

jdk源码注解中有这样一段描述:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

这个类提供线程局部变量。这些变量与其正常的对应方式不同,因为访问一个的每个线程(通过其get或set方法)都有自己独立初始化的变量副本。 ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态字段(例如,用户ID或事务ID)

需要明确的是ThreadLocal不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制

ThreadLocal使用示例


public class SeqCount {
     
    private static ThreadLocal seqCount = new ThreadLocal(){
        // 实现initialValue()
        public Integer initialValue() {
            return 0;
        }
    };
     
    public int nextSeq(){
        seqCount.set(seqCount.get() + 1);
      
        return seqCount.get();
    }
    
    public void remove() {
        seqCount.remove();
    }
     
    public static void main(String[] args){
        SeqCount seqCount = new SeqCount();
     
        SeqThread thread1 = new SeqThread(seqCount);
        SeqThread thread2 = new SeqThread(seqCount);
        SeqThread thread3 = new SeqThread(seqCount);
        SeqThread thread4 = new SeqThread(seqCount);
     
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
        
    private static class SeqThread extends Thread{
        private SeqCount seqCount;
         
        SeqThread(SeqCount seqCount){
            this.seqCount = seqCount;
        }
         
        public void run() {
            try {
                for(int i = 0 ; i < 3 ; i++){
                    System.out.println(Thread.currentThread().getName() + " seqCount :" + seqCount.nextSeq());
                }
            } finally {
                seqCount.remove();
            }
        }
    }
     
}
复制代码

运行结果:


Thread-0 seqCount :1
Thread-0 seqCount :2
Thread-0 seqCount :3
Thread-1 seqCount :1
Thread-1 seqCount :2
Thread-1 seqCount :3
Thread-3 seqCount :1
Thread-3 seqCount :2
Thread-3 seqCount :3
Thread-2 seqCount :1
Thread-2 seqCount :2
Thread-2 seqCount :3
复制代码

从结果可以得知,ThreadLocal确实是可以达到线程隔离机制,保证了变量的安全性

ThreadLocal实现原理

ThreadLocal是为每一个线程创建一个单独的变量副本,所以每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。从其几个方法入手

set方法


    public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 通过当前线程实例获取ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 若map不为null,则以当前threadLocal为键,value为值存放
        if (map != null)
            map.set(this, value);
        // 若map为null,则创建ThreadLocalMap,以当前threadLocal为键,value为值
        else
            createMap(t, value);
    }
复制代码

获取当前线程实例,调用getMap()获取此线程的ThreadLocalMap


    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
复制代码

然后判断map是否为null,若为null则还需创建threadLocalMap,以当前threadLocal为键,value为值存放在threadLocalMap中,若不为null直接存储即可

get方法


    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取线程关联的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // 若map不为null,从map中获取以当前threadLocal实例为key的数据
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 若map为null或者entry为null,则调用此方法初始化
        return setInitialValue();
    }
复制代码

get方法获取当前线程关联的ThreadLocalMap。若map不为null,以threadLocal实例为key获取数据;若map为null或entry为null调用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;
    }
复制代码

与set方法差不多,但多了initialValue()方法,此方法需要子类重写


    protected T initialValue() {
        return null;
    }
复制代码

remove方法


    public void remove() {
         // 根据当前线程获取其所关联的ThreadLocalMap
         ThreadLocalMap m = getMap(Thread.currentThread());
         // 若map不为null,删除以当前threadLocal为key的数据
         if (m != null)
             m.remove(this);
     }
复制代码

ThreadLocalMap

从ThreadLocal那几个核心方法来看,其实现都基于内部类ThreadLocalMap

ThreadLocalMap属性


    // 初始化容量
    private static final int INITIAL_CAPACITY = 16;
    // 哈希表     
    private Entry[] table;
    // 元素个数     
    private int size = 0;
    // 扩容阈值(threshold = 底层哈希表table的长度 len * 2 / 3)
    private int threshold;
复制代码

内部类entry


    static class Entry extends WeakReference> {
        /** The value associated with this ThreadLocal. */
        Object value;
         
        Entry(ThreadLocal k, Object v) {
            super(k);
            value = v;
        }
    }
复制代码

从源码中可以得知Entry的key是Threadlocal,并且Entry继承WeakReference弱引用。注意Entry中并没有next属性,相对于HashMap采用链地址法处理冲突,ThreadLocalMap采用开放定址法

set方法


    private void set(ThreadLocal key, Object value) {
               
        Entry[] tab = table;
        int len = tab.length;
        // 根据ThreadLocal的hashcode值,寻找对应Entry在数组中的位置
        int i = key.threadLocalHashCode & (len-1);
             
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
             
            ThreadLocal k = e.get();
            // 若找到对应key,替换旧值返回 
            if (k == key) {
                e.value = value;
                return;
            }
            // 若key == null,因为e!=null肯定存在entry
            // 说明之前的ThreadLocal对象已经被回收
            if (k == null) {
                // 替换旧entry
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        // 创建新entry  
        tab[i] = new Entry(key, value);
        // 元素个数+1
        int sz = ++size;
        // cleanSomeSlots 清除旧Entry(key == null)
        // 如果没有要清除的数据,元素个数仍然大于阈值则扩容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
复制代码

每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647。在插入过程中先根据threadlocal对象的hash值,定位哈希表的位置:
1、若此位置是空的,就创建一个Entry对象放在此位置上,调用cleanSomeSlots()方法清除key为null的旧entry,若没有要清除的旧entry则判断是否需要扩容
2、若此位置已经有Entry对象了,如果这个Entry对象的key正好是所要设置的key或key为null,则替换value值
3、若此位置Entry对象的key不符合条件,寻找哈希表此位置+1(若到达哈希表尾则从头开始)

我们可以发现ThreadLocalMap采用了开放定址法来解决冲突,一旦发生了冲突,就去寻找下一个空的散列地址,而HashMap采用链地址法解决冲突在原位置利用链表处理

getEntry方法


    private Entry getEntry(ThreadLocal key) {
        // 定位
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        // 若此位置不为空且与entry的key返回entry对象
        if (e != null && e.get() == key)
            return e;
        else
            return getEntryAfterMiss(key, i, e);
    }
复制代码

理解了set,getEntry很好理解。先根据threadlocal对象的hash值,定位哈希表的位置。若此位置entry的key和查找的key相同的话就直接返回这个entry,若不符合调用getEntryAfterMiss()继续向后找,getEntryAfterMiss方法如下:


    private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
        
        while (e != null) {
            ThreadLocal k = e.get();
            // 找到和所需key相同的entry则返回
            if (k == key)
                return e;
            // 处理key为null的entry    
            if (k == null)
                expungeStaleEntry(i);
            else
                // 继续找下一个
                i = nextIndex(i, len);
            e = tab[i];
        }
        return null;
    }
复制代码

remove方法


    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)]) {
            // 若找到所需key 
            if (e.get() == key) {
                // 将entry的key置为null
                e.clear();
                // 将entry的value置为null同时entry置空
                expungeStaleEntry(i);
                return;
            }
        }
    }
复制代码

定位在哈希表的位置,找到相同key的entry,调用clear方法将key置为null,调用expungeStaleEntry方法删除对应位置的过期实体,并删除此位置后key = null的实体


        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            // 将此位置的entry对象置空以及value置空
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            // 元素个数-1
            size--;
             
            // Rehash until we encounter null
            Entry e;
            int i;
            // 清除此位置后key为null的entry对象以及rehash位置不同的entry直至有位置为空为止
            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;
        }
复制代码

内存泄漏

先附上四种引用与gc关系

引用类型 回收机制 用途 生存时间
强引用 从不回收 对象状态 JVM停止运行时
软引用 内存不足时回收 对象缓存 内存不足时终止
弱引用 对象不被引用时回收 对象缓存 GC后终止
虚引用 对象不被引用时回收 跟踪对象的垃圾回收 垃圾回收后终止
先看下面代码

    Son son = new Son(); 
    Parent parent = new Parent(son); 
复制代码
当我们把son置空,由于parent持有son的引用且parent是强引用,所以gc并不回收son所分配的内存空间,这就导致了内存泄露

如果是弱引用那么上述例子,GC就会回收son所分配的内存空间。而ThreadLocalMap采用ThreadLocal弱引用作为key,虽然ThreadLocal是弱引用GC会回收这部分空间即key被回收,但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能 得到释放

那么如何有效的避免呢?

在上述中我们可以看到ThreadLocalMap中的set/getEntry方法中,会对key为null(即ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。当然也可以通过调用ThreadLocal的remove方法进行释放。

总结

ThreadLocal不是用来解决共享对象的多线程访问问题,而是为了方便每个线程处理自己的状态而引入的一个机制。它为每一个线程都提供一份变量的副本,从而实现同时访问而互不影响。另外ThreadLocal可能存在内存泄漏问题,使用完ThreadLocal之后,最好调用remove方法

感谢

www.jianshu.com/p/377bb8408…
www.jianshu.com/p/ee8c9dccc…
cmsblogs.com/?p=2442

分类:
后端
收藏成功!
已添加到「」, 点击更改