ThreadLocal类

虽然这个不是标准的数据结构,但是在java中还是挺重要的结构类。所以需要好好了解一下

使用

该类的使用非常简单,大概就下面的两个操作。

static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set()
sThreadLocal.get()
sThreadLocal.remove()

原理

ThreadLocal存在一个静态内部类ThreadLocalMap。当调用set的时候,首先获取当前线程对象t。然后线程t中存在一个成员变量threadLocals (指向ThreadLocal的静态内部类ThreadLocalMap),默认为null。 //如果存在ThreadLocalMap就直接set,没有则创建ThreadLocalMap并set。 然后将ThreadLocal对象作为key,用户的值作为value值。放入map中。

其实具体是采用ThreadLocal的threadLocalHashCode属性作为key。该属性是被final修饰的int不可变属性。所以可以唯一确定一个ThreadLocal对象。
  但是如何保证两个同时实例化的ThreadLocal对象有不同的threadLocalHashCode属性:在ThreadLocal类中,还包含了一个static修饰的AtomicInteger([əˈtɒmɪk]提供原子操作的Integer类)成员变量(即类变量)和一个static final修饰的常量(作为两个相邻nextHashCode的差值)。由于nextHashCode是类变量,所以每一次调用ThreadLocal类都可以保证nextHashCode被更新到新的值,并且下一次调用ThreadLocal类这个被更新的值仍然可用,同时AtomicInteger保证了nextHashCode自增的原子性。
总的来说其原理主要是通过为每个Thread 维护一个属于自己的map。然后通过不同的ThreadLocal对象作为key来存取不同的value值。也就是说再多线程环境中,对于同一个ThreadLocal对象,当处于不同的线程中时,对应的value值是不一样的。因为线程不一样->map不一样。

可能存在的问题

  1. 对于引用类型的共有变量。我们都知道引用类型传递的是内存地址的副本。通过该地址可以直接修改引用对象。因此如果共有变量是引用类型的话,在加入ThreadLocal的时候需要重新new或者clone。
    例如:
package ztext;

/**
 * @author xgj
 */
public class MyTest {

        ThreadLocal<Long> longLocal = new ThreadLocal<>();
        ThreadLocal<String> stringLocal = new ThreadLocal<>();
        Long along = Long.valueOf(1000);
        public void set() {
            along+=100;
            longLocal.set(along);
            stringLocal.set(Thread.currentThread().getName());
        }

        public long getLong() {
            return longLocal.get();
        }

        public String getString() {
            return stringLocal.get();
        }

        public static void main(String[] args) throws InterruptedException {
            final MyTest test = new MyTest();

            test.set();

            System.out.println(test.getLong());
            System.out.println(test.getString());

            Thread thread1 = new Thread(() -> {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            });


            Thread thread2 = new Thread(() -> {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            });
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

        }
}

的运行结果如下:

也就是说对于共有变量Long,并没有起到隔离作用
如果改为:

package ztext;

/**
 * @author xgj
 */
public class MyTest {

        ThreadLocal<Long> longLocal = new ThreadLocal<>();
        ThreadLocal<String> stringLocal = new ThreadLocal<>();
        Long along = Long.valueOf(1000);
        public void set() {
            Long  blong = along+100;
            longLocal.set(blong);
            stringLocal.set(Thread.currentThread().getName());
        }

        public long getLong() {
            return longLocal.get();
        }

        public String getString() {
            return stringLocal.get();
        }

        public static void main(String[] args) throws InterruptedException {
            final MyTest test = new MyTest();

            test.set();

            System.out.println(test.getLong());
            System.out.println(test.getString());

            Thread thread1 = new Thread(() -> {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            });


            Thread thread2 = new Thread(() -> {
                test.set();
                System.out.println(test.getLong());
                System.out.println(test.getString());
            });
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

        }
}

结果:

  1. 内存泄露
    下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:

    如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
    Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
    永远无法回收,造成内存泄露。
  • 源代码策略
  1. 首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
    如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry。否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。
  2. 但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
  • 使用策略
  1. 使用ThreadLocal,建议用static修饰 static ThreadLocal headerLocal = new ThreadLocal();
  2. 使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

其他

初始容量16,负载因子2/3,解决冲突的方法是再hash法,

posted @ 2020-09-16 12:55  大嘤熊  阅读(190)  评论(0编辑  收藏  举报