乐趣区

关于java:快速掌握并发编程深入学习ThreadLocal

生存中 的 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 值。

上面是 福利

退出移动版