首页 > 编程语言 >快速掌握并发编程---深入学习ThreadLocal

快速掌握并发编程---深入学习ThreadLocal

时间:2023-04-25 21:01:24浏览次数:54  
标签:Thread ThreadLocalMap 编程 value --- ThreadLocal 线程 引用


快速掌握并发编程---深入学习ThreadLocal_java

生活中的ThreadLocal

快速掌握并发编程---深入学习ThreadLocal_jvm_02

考试题只有一套,老师把考试题打印出多份,发给每位考生,然后考生各自写各自的试卷。考生之间不能相互交头接耳(会当做作弊)。各自写出来的答案不会影响他人的分数。

注意:考试题、考生、试卷。

快速掌握并发编程---深入学习ThreadLocal_jvm_03


用代码来实现:

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_jvm_02

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

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

上面考试场景中的几个关键点我们这么可以这么理解:

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

试卷-----考试题的副本

考试----线程

ThreadLocal可以理解为每个线程想绑定自己的东西,相互不受干扰。比如上面的考试场景,考试题大家都是一样的。但是考试题进行复印出来后,每人一份,各自写写各自的,相互不受影响,这就正是ThreadLocal想要实现的功能。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

可以想想生活中还有没有类似的例子。肯定非常多,只要我们用心去体会。

下面我们就来看看ThreadLocal到底是如何实现的。

ThreadLocal设计原理

ThreadLocal名字中第一个单词Thread表示线程,Local表示本地,我们就理解为线程本地变量了。想了解更多Thread,可看:快速掌握并发编程---Thread常用方法

先看看ThreadLocal的整体

快速掌握并发编程---深入学习ThreadLocal_jvm_05


最关心的三个公有方法: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是个什么东东

快速掌握并发编程---深入学习ThreadLocal_python_06


ThreadLocalMapThreadLocal的静态内部类。

set方法整体为

快速掌握并发编程---深入学习ThreadLocal_python_07

快速掌握并发编程---深入学习ThreadLocal_java_08

快速掌握并发编程---深入学习ThreadLocal_内存泄漏_09


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

快速掌握并发编程---深入学习ThreadLocal_多线程_10


get方法

快速掌握并发编程---深入学习ThreadLocal_java_11

快速掌握并发编程---深入学习ThreadLocal_多线程_12


remove方法

快速掌握并发编程---深入学习ThreadLocal_python_13

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_多线程_14


然后,运行上面的案例

快速掌握并发编程---深入学习ThreadLocal_jvm_15


从上面的案例中我们看到:线程池中的每一个线程使用完 ThreadLocal 对象之后再也不用,由于线程池中的线程不会退出,线程池中的线程的存在,同时 ThreadLocal 变量也会存在,占用内存!造成 OOM 溢出!

前面我们分析了Thread、ThreadLocal、ThreadLocalMap三者的关系

快速掌握并发编程---深入学习ThreadLocal_jvm_16

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

快速掌握并发编程---深入学习ThreadLocal_python_17


总结

每个 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。

快速掌握并发编程---深入学习ThreadLocal_python_18

如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:

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

永远无法回收,造成内存泄漏。

注意:其实在ThreadLocalMap 的设计中已经考虑到这种情况,也加上了一些防护措施: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值。

下面是 福利

快速掌握并发编程---深入学习ThreadLocal_java_19

 

标签:Thread,ThreadLocalMap,编程,value,---,ThreadLocal,线程,引用
From: https://blog.51cto.com/u_11702014/6225178

相关文章

  • 考研408操作系统-缓冲区管理
    缓冲技术分类:单缓冲双缓冲循环缓冲缓冲池一、单缓冲单缓冲是操作系统提供的一种最简单的缓冲形式,当用户进程发出一个IO请求时,操作系统便在内存中为它分配一个缓冲区。由于只设置一个缓冲区,设备和处理器交换数据时,应该先把要交换的数据写入缓冲区,然后放入工作区,在工作区的......
  • C++第四章课后习题4-12
    定义一个datatype类,能处理包含字符型,整形,浮点型3种类型的数据,给出其构造函数。1#include<iostream>2usingnamespacestd;34classDataType{5private:6chara;7intn;8floatx;9enum{10character,11intege......
  • 2023.4.25编程一小时打卡
    一、问题描述:格式输出:输入一个整数,以八进制形式输入,分别以十进制和十六进制显示;输出字符串“Iamastudent!”,设置输出位宽为20,使用符号“*”填充;输出浮点数3.1415926,分别以浮点数和二进制形式进行输出,并分别设置小数点后的位数为8,6,4位。 二、解题思路:首先,根据题意定......
  • Python面向切面编程-语法层面和functools模块
    1,Python语法层面对面向切面编程的支持(方法名装饰后改变为log)__author__='Administrator'importtimedeflog(func):defwrapper(*args):start=time.time()func(args)end=time.time()print'funcusedtimeis:',end-st......
  • 编程实现可靠数据传输原理 Go-Back-N
    1.编写接收端代码接收端模拟网络环境较差时情况,每次生成一个随机数,小于0.8时不丢包,大于0.8时丢包。接收数据格式:编号+空格+内容返回数据格式:丢包:Loss+空格+编号未丢包:ACK+空格+编号接收包非累计计数时不做处理。2.编写发送端代码发送端较为复杂,分为两个线程:发送线程:设......
  • CF1822G2 - Magic Triples
    比较好的题目,别的不说,G1对G2有着不错的启发性。首先,因为\(b>0,a_k\le10^9\),所以\(b\)不可能超过\(\sqrt{a}\)考虑对\(b\)分类讨论,设置一个阈值\(B\),先处理\(b=1\)的情况,其实就是取三个相同的数然后排列,可以比较简单的排序之后做到\(O(n)\)。接着手写一个哈希表用......
  • ES6-特性
    1.let特性1:同作用域下变量不能重复声明(var可以,前者覆盖后者)特性2:块级作用域内有效也就是说:ES6后作用域,全局作用域,块级作用域(if(){},while(){},for(){},else{}),函数作用域。查找变量由内向外查找。特性3:不存在变量提升例如:var声明变量,在代码......
  • Period UVA - 1371
     题意:给两个串A,B。现在把B串分为若干个部分,对每一个部分进行操作将其变为一个A串,代价为每部分操作次数的最大值求最小代价 #include<iostream>#include<algorithm>#include<cstring>usingnamespacestd;constintN=5003,M=N;#defineintlonglongconstinti......
  • 03-1 燃烧系统分析方法与着火理论:热自燃基本模型、影响热自燃的因素
    热自燃条件第一步:作出假设以封闭容器内可燃物质的着火过程为例,来分析热自燃问题。作出以下假设:只有热反应,不存在链式反应;容器的体积V和表面积F为定值;容器内的参数(如温度浓度等)处处相同;容器与环境之间仅存在对流换热,α为定值;可燃物质的反应热Q为定值;在整个着火过程中,可燃......
  • 深度学习--RNN实战与存在问题
    深度学习--RNN实战与存在问题时间序列预测importnumpyasnpimporttorchimporttorch.nnasnnimporttorch.optimasoptimfrommatplotlibimportpyplotasplt#数量num_time_steps=50#输入的维度input_size=1#隐藏层大小hidden_size=16#输出的维......