Java ThreadLocal 分析 ,在Java多线程中我们经常会看到两个重要的关键字:synchronized 和ThreadLocal。这两个都是解决多线程之间一些数据问题的,synchronized主要是解决多个线程访问一个共享数据的同步问题,而ThreadLocal主要是解决多个线程之间的数据隔离问题。

对比

如果是新手想要理解ThreadLocal千万不要和synchronized联系在一起,我自己一开始也陷入这样的误区,它们作用是不同的,

  1. synchronized通过Object内置的对象锁,使得多个线程访问同一个共享数据时通过加锁得机制使得任何时间只有一个线程持有锁并且访问共享数据,其他线程想要访问就必须等待当前线程释放这个锁。【synchronized主要解决多个线程访问共享数据保证数据的安全问题】
  2. ThreadLocal主要是解决多线程数据得隔离问题,保证多个线程之间的数据独立,互不干扰。通过ThreadLocal可以使得一个数据可以在线程内一直传递,很多人此时可能会进入一个误区:为什么不在内中定义一个静态变量?这也是一个误区,类中定义的静态变量,在多个线程中相当于是共享,而不是线程私有。【ThreadLocal就是为每一个线程自身提供单独的数据区,实际上由线程自身实现】

Set过程

代码图示

为了能更好的理解,下面会给出一段代码,演示TheadLocal的作用以及其相关流程分析

package threadlocal;

public class ThreadLocalDemo1 {
    static ThreadLocal<Person> tl=new ThreadLocal<>();
    public static void main(String[] args) {

        //线程1设置一个Person对象
        new Thread(()->{
            Thread.currentThread().setName("线程1");
            tl.set(new Person());
            System.out.println(Thread.currentThread().getName()+"获取的tl为:"+tl.get());
        }).start();

        //尝试用线程2去获取
        new Thread(()->{
            try {
                Thread.sleep(1000); //保证线程1先执行set()
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            Thread.currentThread().setName("线程2");
            System.out.println(Thread.currentThread().getName()+"获取的tl为:"+tl.get());
        }).start();
    }
    static class Person{
        private String name="Yremp";

        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
}

结果如下:

"E:\Program Files\Java\jdk1.8.0_201\bin\java.exe"
线程1获取的tl为:Person{name='Yremp'}
线程2获取的tl为:null

Process finished with exit code 0

只有线程1可以获取到,线程2是获取不到的。

我对着ThreadLocal源码分析一下整个过程

set方法

   /**
     * @param value代表我们要存贮在当前线程ThreadLocal中的数据
     *        
     */
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程中的ThreadLocal.ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);//map不为空则调用set()插入
        else
            createMap(t, value);//为空则调用createMap()创建map并插入
    }

看一下ThreadLocal getMap方法:

   /**
     * @param  t 当前线程
     * @return 返回当前线程的ThreadLocal.ThreadLocalMap
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

ThreadLocal.ThreadLocalMap 是ThreadLocal一个静态内部类

 static class ThreadLocalMap {
        //默认数组长度
        private static final int INITIAL_CAPACITY = 16;
        //Entry数组
        private Entry[] table;
        //统计Entry数量
        private int size = 0;
        
        private int threshold; 

        /**
         *这个ThreadLocalMap里面又有一个静态内部Entry类,相当于一个Map<K,V>
         * K是我们实例化的ThreadLocal tl 
         * V是我们实例化的Person对象
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
       /**
         *这里我重点关注set()方法
         */
         private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            //通过hash值定位数组下标
            int i = key.threadLocalHashCode & (len-1);
            
            //遍历i->len
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
                //key相同覆盖旧的value
                if (k == key) {
                    e.value = value;
                    return;
                }
                //k=null,意味着原来的ThreadLocal对象被清除
                //把key、vaule插入i的位置
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            //对应数组位置插入新的Entry对象
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash( );
        }
}

这就是插入的整个代码,下面是一个简单的理解图:

tl是ThreadLocal的强引用,而map里面Entry中的tl是一个对ThreadLocal的弱引用,具体可以看ThreadLocalMap中的内部类Entry的源码:

static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

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

get()过程

先看看ThreadLocal中的get()方法

 public T get() {
        //还是获取当前线程
        Thread t = Thread.currentThread();
       //获取当前线程的map
        ThreadLocalMap map = getMap(t);
       //map不为空则调用ThreadLocalMap的.getEntry()方法
       //,this是当前ThreadLocal实例
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                //返回value
                return result;
            }
        }
        //map为空的处理
        return setInitialValue();
    }

下面是ThreadLocalMap中的getEntry()方法源码

//利用当前调用的ThreadLocal实例作为key查询
private Entry getEntry(ThreadLocal<?> key) {
            //和插入一样通过key的hash值运算得到数组下标
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //找到对应的Entry
            if (e != null && e.get() == key)
                return e;
            else//没找到,额外的处理
                return getEntryAfterMiss(key, i, e);
        }

可以结合一开始的代码总结为下面几步:

  1. 当我们在线程中使用ThreadLocal set()方法时,把当前ThreadLocal实例tl作为key,Person实例作为Value传入。
  2. 进入ThreadLocal set(T value)方法中,首先获取当前线程。
  3. 然后获取当前线程中的ThreadLocal.ThreadLocalMap。
  4. map不为空则插入,为空则初始化map并插入。
  5. 插入的具体细节为,利用当前ThreadLocal实例的hash值进行(n-1)&hash进行数组下标定位,循环查找map中的Entry对象中的key是否相等或者有key值为null,key相同则覆盖,key为空则插入此位置,如果没有则在对应数组位置放入一个新的Entry。
  6. 线程中通过ThreadLocal实例调用get()方法获取存入的数据。具体为利用ThreadLocal实例的hash值运算,定位数组下标,然后去对比信息得到value。

注意事项

图1
图2
  1. 为什么ThreadLocalMap中的Entry中的key要使用弱引用:如上图,如果tl是对ThreadLocal对象的强引用,如果如图2中的tl不引用ThreadLocal,但是由于Map中的Entry里面的tl仍然保持对 ThreadLocal对象 的强引用,GC时还是不会回收ThreadLocal对象,这就会造成内存泄露。
  2. 还有一种情况要注意,当我们不使用某一个ThreadLocal保存的数据,要及时对其经行remove操作,否则也会造成内存泄露。如图2,tl放弃对Threadlocal的引用,GC时直接回收ThreadLocal,那么Map中的key(tl)就是null,此时我们就无法访问对应的Value,即就可能造成内存泄露。所以要及时remove()。
  3. 针对于线程池,会默认在线程执行完任务之后清理它对应的Threadlocal。

标签云

ajax AOP Bootstrap cdn Chevereto CSS Docker Editormd GC Hexo IDEA IPA JavaScript jsDeliver JS樱花特效 JVM Linux markdown Maven MyBatis MyBatis-plus MySQL Pictures Sakura SEO shadowrocket Spring Boot Spring Cloud Spring Cloud Alibaba SpringMVC SSR Thymeleaf V2ray Vue Web WebSocket Wechat Social WordPress Yoast SEO 代理 分页 图床 小幸运 苹果iOS国外账号 苹果IOS账号

Java ThreadLocal分析
Java ThreadLocal分析
本文最后更新于2020年6月19日,已超过 5 个月没更新!