生存中的ThreadLocal

考试题只有一套,老师把考试题打印出多份,发给每位考生,而后考生各自写各自的试卷。考生之间不能互相窃窃私语(会当做舞弊)。各自写进去的答案不会影响别人的分数。

留神:考试题、考生、试卷。

用代码来实现:

public class ThreadLocalDemo {    //线程共享变量 localVar    public static ThreadLocal<String> localVar = new ThreadLocal<>();    static void print(String str) {        //打印以后线程中本地内存中本地变量的值        System.out.println(str + " :" + localVar.get());        //革除本地内存中的本地变量        localVar.remove();    }    public static void main(String[] args) {        Thread t1 = new Thread(new Runnable() {            @Override            public void run() {                //设置线程1中本地变量的值                localVar.set("全副写完");                String threadName = Thread.currentThread().getName();                //调用打印办法                print(threadName);            }        }, "张三");        Thread t2 = new Thread(new Runnable() {            @Override            public void run() {                //设置线程2中本地变量的值                localVar.set("写了一半");                String threadName = Thread.currentThread().getName();                //调用打印办法                print(threadName);            }        }, "李四");        Thread t3 = new Thread(new Runnable() {            @Override            public void run() {                //设置线程2中本地变量的值                localVar.set("齐全没写");                String threadName = Thread.currentThread().getName();                //调用打印办法                print(threadName);            }        }, "王二");        t1.start();        t2.start();        t3.start();    }}

输入

李四 :写了一半王二 :齐全没写张三 :全副写完

-

背景

ThreadLocal:字面意思为线程本地或者本地线程。然而其实真正含意并非如此,真正的含意是线程本地变量(正本)。

java.lang.ThreadLocalJDK1.2版本的时候引入的,本文是基于JDK1.8版本进行解说的。

下面考试场景中的几个关键点咱们这么能够这么了解:

考试题----共享变量,大家共享

试卷-----考试题的正本

考试----线程

ThreadLocal能够了解为每个线程想绑定本人的货色,互相不受烦扰。比方下面的考试场景,考试题大家都是一样的。然而考试题进行复印进去后,每人一份,各自写写各自的,互相不受影响,这就正是ThreadLocal想要实现的性能。

当应用ThreadLocal保护变量时,ThreadLocal为每个应用该变量的线程提供独立的变量正本,所以每一个线程都能够独立地扭转本人的正本,而不会影响其它线程所对应的正本。

能够想想生存中还有没有相似的例子。必定十分多,只有咱们用心去领会。

上面咱们就来看看ThreadLocal到底是如何实现的。

ThreadLocal设计原理

ThreadLocal名字中第一个单词Thread示意线程,Local示意本地,咱们就了解为线程本地变量了。想理解更多Thread,可看:疾速把握并发编程---Thread罕用办法

先看看ThreadLocal的整体

最关怀的三个私有办法:set、get、remove

构造方法

 public ThreadLocal() { }

构造方法里没有任何逻辑解决,就是简略的创立一个实例。

set办法

源码为

public void set(T value) {    //获取以后线程     Thread t = Thread.currentThread();    //这是什么鬼?     ThreadLocalMap map = getMap(t);            if (map != null)                    map.set(this, value);           else        createMap(t, value);}

先看看ThreadLocalMap是个什么东东

ThreadLocalMapThreadLocal的动态外部类。

set办法整体为

ThreadLocalMap构造方法

//这个属性是ThreadLocal的,就是获取hashcode(这列很有学识,然而咱们的目标不是他)private final int threadLocalHashCode = nextHashCode();private Entry[] table;private static final int INITIAL_CAPACITY = 16;//Entry是一个弱援用 static class Entry extends WeakReference<ThreadLocal<?>> {    Object value;    Entry(ThreadLocal<?> k, Object v) {        super(k);        value = v;       } }ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {    //数组默认大小为16    table = new Entry[INITIAL_CAPACITY];    //len 为2的n次方,以ThreadLocal的计算的哈希值依照Entry[]取模(为了更好的散列)    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);    table[i] = new Entry(firstKey, firstValue);    size = 1;    //设置阈值(扩容阈值)    setThreshold(INITIAL_CAPACITY);  }

而后咱们看看map.set()办法中是如何解决的

 private void set(ThreadLocal<?> key, Object value) {            Entry[] tab = table;            int len = tab.length;            //len 为2的n次方,以ThreadLocal的计算的哈希值依照Entry[]取模            int i = key.threadLocalHashCode & (len-1);            //找到ThreadLocal对应的存储的下标,如果以后槽内Entry不为空,            //即以后线程曾经有ThreadLocal曾经应用过Entry[i]            for (Entry e = tab[i];                 e != null;                 e = tab[i = nextIndex(i, len)]) {                ThreadLocal<?> k = e.get();                 // 以后占据该槽的就是以后的ThreadLocal ,更新value完结                if (k == key) {                    e.value = value;                    return;                }                //以后卡槽的弱援用可能会回收了,key:null value:xxxObject ,                //需清理Entry原来的value ,便于垃圾回收value,且将新的value 放在该槽里,完结                if (k == null) {                    replaceStaleEntry(key, value, i);                    return;                }            }           //在这之前没有ThreadLocal应用Entry[i],并进行值存储            tab[i] = new Entry(key, value);            //累计Entry所占的个数            int sz = ++size;            // 清理key 为null 的Entry ,可能须要扩容,扩容长度为原来的2倍,并须要进行从新hash            if (!cleanSomeSlots(i, sz) && sz >= threshold){                rehash();            }}

从下面这个set办法,咱们就大抵能够把这三个进行一个关联了:

ThreadThreadLocalThreadLocalMap

get办法

remove办法

expungeStaleEntry办法代码里有点大,所以这里就贴了进去。

//删除古老entry的外围办法private int expungeStaleEntry(int staleSlot) {    Entry[] tab = table;    int len = tab.length;                tab[staleSlot].value = null;//删除value    tab[staleSlot] = null;//删除entry    size--;//map的size自减    // 遍历指定删除节点,所有后续节点    Entry e;    int i;    for (i = nextIndex(staleSlot, len);         (e = tab[i]) != null;         i = nextIndex(i, len)) {        ThreadLocal<?> k = e.get();        if (k == null) {//key为null,执行删除操作            e.value = null;            tab[i] = null;            size--;        } else {//key不为null,从新计算下标            int h = k.threadLocalHashCode & (len - 1);            if (h != i) {//如果不在同一个地位                tab[i] = null;//把老地位的entry置null(删除)                // 从h开始往后遍历,始终到找到空为止,插入                 while (tab[h] != null){                    h = nextIndex(h, len);                }                tab[h] = e;               }        }    }    return i;}

对象援用

在Java里万事万物皆对象,这里有个对象,那么对象援用是什么呢?

User user=new User("老田");

对于下面这段代码的解释,很大部分人会说user是个对象。

一开始培训机构什么书籍里都说user是个对象,于是也就这么叫user是对象,这里的user指向了对象"老田"。这里的User user是定义了一个对象援用,能够指向任意的User对象,比方:

User user;user = new User("张三");user = new User("李四");

一个队对象被user援用了,这里user把他叫做对象援用 。

对象援用就好比男人,对象就是男人的老婆。依据目前我国法律规定,一个男人在任何时候最多只能有一个老婆,然而一辈子能够取多个老婆。哈哈哈!!!

另外如果是上面

int a;a=1;a=100;

这里的a,咱们通常称之为变量。所以下面的user咱们也能够了解为变量。

在Java里对象的援用也是分几种类型的,分以下四种类型:

强援用

软援用

弱援用

虚援用

强援用

强援用就是咱们平时开发中用的最多的,比如说:

Person person = new Person("老田");

这个person就是强援用。

当一个对象被强援用时候,JVM垃圾回收的时候是不会回收的,宁愿执行OOM(Out Of Memory)异样也绝不回收,因为JVM垃圾回收的时候会认为这个对象是被用户正在应用,若回收了很有可能造成无奈设想的谬误。

软援用

如果一个对象具备软援用,内存空间足够,JVM垃圾回收器就不会回收它;如果内存空间有余了,就会回收这些对象的内存。只有垃圾回收器没有回收它,该对象就能够被程序应用。软援用可用来实现内存敏感的高速缓存,比方网页缓存、图片缓存等。

应用软援用能避免内存泄露,加强程序的健壮性。

java.lang.ref.SoftReference的特点是它的一个实例保留对一个Java对象的软援用, 该软援用的存在不障碍垃圾收集线程对该Java对象的回收。

也就是说,一旦SoftReference保留了对一个Java对象的软援用后,在垃圾线程对这个Java对象回收前,SoftReference类所提供的get()办法返回Java对象的强援用。

 /**     * Returns this reference object's referent.  If this reference object has     * been cleared, either by the program or by the garbage collector, then     * this method returns <code>null</code>.     *     * @return   The object to which this reference refers, or     *           <code>null</code> if this reference object has been cleared     */    public T get() {        T o = super.get();        if (o != null && this.timestamp != clock)            this.timestamp = clock;        return o;    }

如果援用对象被分明或者被GC回收,这个get办法就返回null

弱援用

弱援用也是用来形容非必须对象的,当JVM下一次进行垃圾回收时,无论内存是否短缺,都会回收被弱援用关联的对象。在java中,用java.lang.ref.WeakReference类来示意。

与软援用不同的是,不论是否内存不足,弱援用都会被回收。

弱援用能够联合 来应用,当因为零碎触发gc,导致软援用的对象被回收了,JVM会把这个弱援用退出到与之相关联的ReferenceQueue中,不过因为垃圾收集器线程的优先级很低,所以弱援用不肯定会被很快回收。

虚援用

虚援用和后面的软援用、弱援用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类示意。如果一个对象与虚援用关联,则跟没有援用与之关联一样,在任何时候都可能被垃圾回收器回收。

留神:虚援用必须和援用队列关联应用,当垃圾回收器筹备回收一个对象时,如果发现它还有虚援用,就会把这个虚援用退出到与之 关联的援用队列中。程序能够通过判断援用队列中是否曾经退出了虚援用,来理解被援用的对象是否将要被垃圾回收。如果程序发现某个虚援用曾经被退出到援用队列,那么就能够在所援用的对象的内存被回收之前采取必要的口头。

好了下面就大略说了一下对象的四大援用,次要本文前面须要用到弱援用。

ThreadLocal 内存透露

讲到内存透露,那咱们还是把内存溢出和内存透露大抵说一下。

内存溢出

在JVM如果产生内存溢出,阐明内存不够实用,撑爆了,也就是咱们说的OOM。大量内存得不到开释,又一直申请内存空间。

零碎内存应用200M,曾经应用了180M,可是你说你还想应用50M,于是零碎就受不了。

就想气球一样,本来曾经到极限了,你还是使劲打气,很容易就导致气球爆炸了。

就想你只能扛100斤的货色,当初给你200斤,必定受不了。

内存透露

强援用所指向的对象不会被回收,可能导致内存透露,虚拟机宁愿抛出OOM也不会去回收他指向的对象。后面说到强援用的时候,如果对象始终被援用,JVM是不会回收他的,直到最初零碎OOM

看过《树学生》电影的人都晓得,树学生家里的地被他人占用了,然而树学生不敢把人家怎么样。如果是很多人都去占用树学生家的地和财产,到最初树学生不就要饿死么。树学生这部电影的确难看,看完一遍基本上不晓得在说什么,次要是树学生空想的太多,很多人看了两遍也不是很懂。扯远了。。。

ThreadLocal内存透露

内存透露案例

模仿了一个线程数为THREAD_LOOP_SIZE的线程池,所有线程共享一个ThreadLocal 变量,每一个线程执行的时候插入一个大的 List 汇合,这里因为执行了500 次循环,也就是产生了500个线程,每一个线程都会附丽一个 ThreadLocal变量:

public class ThreadLocalOOMDemo {    private static final int THREAD_LOOP_SIZE = 500;    private static final int MOCK_BIG_DATA_LOOP_SIZE = 10000;    private static ThreadLocal<List<User>> threadLocal = new ThreadLocal<>();    public static void main(String[] args) throws InterruptedException {        ExecutorService executorService = Executors.newFixedThreadPool(THREAD_LOOP_SIZE);        for (int i = 0; i < THREAD_LOOP_SIZE; i++) {            executorService.execute(() -> {                threadLocal.set(new ThreadLocalOOMDemo().addBigList());                Thread t = Thread.currentThread();                System.out.println(Thread.currentThread().getName());                //threadLocal.remove(); //不勾销正文的话就可能呈现OOM            });            try {                Thread.sleep(1000L);            } catch (InterruptedException e) {                e.printStackTrace();            }        }        //executorService.shutdown();    }    private List<User> addBigList() {        List<User> params = new ArrayList<>(MOCK_BIG_DATA_LOOP_SIZE);        for (int i = 0; i < MOCK_BIG_DATA_LOOP_SIZE; i++) {            params.add(new User("Java后端技术全栈", "123456" + i, "man", i));        }        return params;    }    class User {        private String userName;        private String password;        private String sex;        private int age;        public User(String userName, String password, String sex, int age) {            this.userName = userName;            this.password = password;            this.sex = sex;            this.age = age;        }    }}

在设置IDEA或者eclipse中,设置 JVM 参数设置最大内存为 -Xmx64m,以便模拟出 OOM:

而后,运行下面的案例

从下面的案例中咱们看到:线程池中的每一个线程应用完 ThreadLocal 对象之后再也不必,因为线程池中的线程不会退出,线程池中的线程的存在,同时 ThreadLocal 变量也会存在,占用内存!造成 OOM 溢出!

后面咱们剖析了Thread、ThreadLocal、ThreadLocalMap三者的关系

一个 Thread 中只有一个 ThreadLocalMap,一个 ThreadLocalMap 中能够有多个 ThreadLocal 对象,其中一个 ThreadLocal 对象对应一个 ThreadLocalMap 中一个的 Entry(也就是说:一个 Thread 能够附丽有多个 ThreadLocal 对象)。

总结

每个 Thread 保护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例自身,value 是真正须要存储的 Object。

ThreadLocal自身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。

值得注意的是图中的虚线,示意 ThreadLocalMap 是应用 ThreadLocal 的弱援用作为 Key 的,弱援用的对象在 GC 时会被回收。

ThreadLocalMap应用 ThreadLocal的弱援用作为 key,如果一个 ThreadLocal没有内部强援用来援用它,那么零碎 GC 的时候,这个 ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会呈现 key 为 null 的 Entry,就没有方法拜访这些 key 为 null 的 Entry 的 value。

如果以后线程再迟迟不完结的话,这些 key 为 null 的 Entry 的 value 就会始终存在一条强援用链:

Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

永远无奈回收,造成内存透露。

留神:其实在T`hreadLocalMap 的设计中曾经思考到这种状况,也加上了一些防护措施:**在****ThreadLocal 的get(),set(),remove()的时候都会革除线程 ThreadLocalMap` 里所有 key 为 null 的 value**。

然而如果上述代码中的这行代码

threadLocal.remove(); 

把正文放开,这不会抛出OOM

另外,网上很多文章都说这是因为弱援用导致的,集体认为不能把锅扔给弱援用,这和使用者有间接关系。如果应用切当是不会呈现OOM的。

因为Thread中蕴含变量ThreadLocalMap,因而ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存透露。

然而应用弱援用能够多一层保障:弱援用ThreadLocal不会内存透露,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被革除。

因而,ThreadLocal内存透露的本源是:因为ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存透露,而不是因为弱援用。

那为什么应用弱援用而不是强援用??

key 应用强援用

ThreadLocalMap的key为强援用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强援用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存透露。

key 应用弱援用

ThreadLocalMap的key为弱援用回收ThreadLocal时,因为ThreadLocalMap持有ThreadLocal的弱援用,即便没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()办法的时候会被革除value值。

上面是 福利