Java中的ThreadLocal线程变量详解

 更新时间:2024年01月17日 09:41:03   作者:java-zh  
这篇文章主要介绍了Java中的ThreadLocal线程变量详解,ThreadLocal叫做线程变量,意思是在ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,它是用来提供线程内部的局部变量,需要的朋友可以参考下

ThreadLocal 简介

介绍

ThreadLocal叫做线程变量,意思是在ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,它是用来提供线程内部的局部变量。这种变量在多线程环境下访问时能保证各个线程的变量相对独立于其他线程内的变量。

用一句话来概括:提供线程局部变量,一个局部变量,在多线程中,分别有独立的值。

特点

  • 每个线程都有自己的线程局部变量,且只能访问自己的,不能访问其他线程的
  • ThreadLocal变量通常被private static修饰,当一个线程销毁(结束)时,其所有的ThreadLocal变量都会被回收

图形解析

  1. 每个Thread线程内部都有一个Map(ThreadLocalMap)
  2.  ThreadLocalMap里面存储ThreadLocal对象(key)和线程的变量值(value)
  3. Thread内部的ThreadLocalMap是由ThreadLocalMap维护的,由ThreadLocal负责向ThreadLocalMap获取和设置线程值
  4. 对于不同的线程,每次获取值时,别的线程并不能获取到当前线程值,拥有线程隔离,互不干扰

ThreadLocal实现线程隔离基本原理

  1. ThreadLocal本身不存放数据,使用线程Thread中的threadLocals属性,(ThreadLocal是Thread中属性threadLocals的管理者),threadLocals属性对应在ThreadLocal中定义的ThreadLocalMap对象。
  2. 在调用ThreadLocal的set()方法时,会将自身的引用this作为key,用户传入的值作为value存入ThreadLocalMap中。这样每个线程的读写操作都是基于线程本身的一个私有值,线程之间的数据是相互隔离的,互不影响。

ThreadLoca常用的API方法

  • get():返回当前线程的ThreadLoca变量的值,如果当前线程没有该 变量就初始化为null返回,如果ThreadLocals属性没有就被创建,就新建一个ThreadLocalMap,并创建一个ThreadLocal变量存入map中 
  • set(T value):将当前线程的ThreadLocal变量副本中的值指定为value
  • remove():移除当前线程的ThreadLocal变量

ThreadLocal源码解析

get()

public T get() {
        // 获取到当前线程
        Thread t = Thread.currentThread();
        // 从当前线程t中获取thredLocals属性
        ThreadLocalMap map = getMap(t);
        //如果不为null,就说明已经初始化过,存在值
        if (map != null) {
            // 获取当前ThreadLocal对象为key值(如果不存在,就创建一个ThreadLocal变量)
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                //如果值不为null,就直接返回
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果当前线程的threadlocals没有被创建,就调用setInitialValue()创建
        return setInitialValue();
    }
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
private T setInitialValue() {
        // value赋值为初始化方法值,默认为null
        T value = initialValue();
        // 获取到当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程t的threadlocals属性,也就是map集合
        ThreadLocalMap map = getMap(t);
        // 如果map不为null,进行赋值
        if (map != null)
            map.set(this, value);
        else
            //map为null,创建当前线程t的map并赋值
            createMap(t, value);
        return value;
    }
 

set()

public void set(T value) {
        // 获取到当前线程
        Thread t = Thread.currentThread();
        // 获取到当前线程的threadLocals值,即map值
        ThreadLocalMap map = getMap(t);
        // 如果map不等于null,添加值
        if (map != null)
            map.set(this, value);
        // 如果等于null,创建当前线程t的map,并添加值
        else
            createMap(t, value);
    }

 remove()

public void remove() {
        // 获取当前线程的threadLocals值,即map集合
         ThreadLocalMap m = getMap(Thread.currentThread());
         // map不为null就将ThreadLocal变量(this)移除
         if (m != null)
             m.remove(this);
     }
}

适用场景

  1. 每个线程需要有自己的独立实例
  2. 实例需要在多个方法中共享,但不希望被多线程共享

场景举例:用来存储用户信息,数据库连接,数据夸层传递(由service1传到service2),解决线程安全问题等等

存在问题

ThreadLoal可能会造成内存泄漏,主要原因是线程复用。

由于ThreadLocal对象是弱引用,如果没有外部的强引用指向,会导致ThreadLoca对象被回收,Entry中的key变成空,此时如果value没有外部引用指向,value就永远访问不到了,(按理应该被GC回收,但是由于Entry对象table数组还在强引用value)此时会发生内存泄漏,value成为无法访问但又永远无法回收的对象(除非线程被销毁,但是处于系统性能考虑,线程不宜频繁的创建和销毁,经常适用线程池,这样线程的生命周期变大,内存泄漏的影响也就会变大)

总结:threadLocals对象中的Entry对象不再使用后,如果没有及时清除Entry对象,而程序自身也无法通过垃圾回收机制自动清除,那么就可能会造成内存泄漏。

解决内存泄漏方案

  • 使用完ThreadLocal变量以后,应该尽快调用remove方法,将其从ThreadLocalMap中清除,以便让垃圾回收器回收他们。
  • 将ThreadLocal变量定义成private static类型,并且咋使用完以后手动清除,以避免线程重用时引起的内存泄漏问题
  • 不要在线程池中使用ThreadLocal变量,如果必须使用,应该在使用完以后手动清理。

内存泄漏

内存泄漏指得是程序中存在某些对象或资源没有妥善的释放。

导致这些对象或资源一直占用着内存,从而无法被回收,随着时间推移,这些未释放的对象或资源会越来越多,最终耗尽系统内存资源,导致系统崩溃。

常见的内存泄漏

  • 对象被创建以后,没有及时被销毁,成为垃圾对象
  • 没有正确关闭IO资源
  • 缓存没有被清空
  • 形态集合类对象未删除引用
  • 单列模式下对象未及时释放

内存溢出

内存溢出是指程序在申请内存时,无法获得足够的内存空间,导致程序无法正常运行。

通常情况下,当程序需要使用的内存超过系统能提供的内存时,就会发生内存溢出的情况。

常见的内存溢出

  • 堆内存溢出:由于创建了过多的对象或者某些对象太大,导致堆内存不足。
  • 栈内存溢出:由于方法调用过多或者某些方法的递归调用层数过多,导致栈内存不足
  • 永久代内存溢出:由于创建了过多的类或者字符串,导致永久代内存不足。

Java中四种引用类型

引用关系的强弱关系:强引用>软引用>弱引用>虚引用

强引用

如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不回回收它,而是抛出OutOfMemoryError错误,使程序终止异常,如果想中断强引用和某个对象的关联,可以显式的将引用类型赋值为null,这样一来,JVM在合适的时间就会回收改对象。

软引用

在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。(软引用可以用来实现内存敏感的高速缓存,比如网页缓存,图片缓存等。使用软引用能防止内存泄漏,增强程序的健壮性)

弱引用

具有弱引用的对象拥有的生命周期更短。当jvm进行垃圾回收器回收,一旦发现弱引用对象,无论当前内存是否充足,都会将弱引用进行回收

虚引用

虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

注意点

虚引用必须和引用队列联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象内存之前,把这个虚引用加入到与之关联的引用队列之中。(其它引用是被JVM回收后才被加入引用队列中的,由于这个机制,所以虚引用大多被用于引用销毁前工作。可以使用在对象销毁前的一些操作,比如资源释放)

ThreadLocal示例

示例1

public class ThreadLocalDemo1 {
    private static ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
    private static ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>();
    /**
     * 运行 count个线程,每个线程持有自己独有的 String类型编号
     */
    public void startThreadArray(int count) {
        Thread[] runs = new Thread[count];
        for (int i = 0; i < runs.length; i++) {
            // 赋值编号id
            new ThreadDemo1(i).start();
        }
    }
    /**
     * 线程类:
     */
    public static class ThreadDemo1 extends Thread {
        /**
         * 编号id
         */
        private int codeId;
        public ThreadDemo1(int codeId) {
            this.codeId = codeId;
        }
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            threadLocal1.set("threadLocal1赋值,线程_" + codeId);
            if (codeId == 2) {
                //如果是线程2,设置 threadLocal2变量,值乘以5
                threadLocal2.set(codeId * 5);
            }
            System.out.println(threadName + " -》 threadLocal1 " + threadLocal1.get());
            System.out.println(threadName + " -》 threadLocal2 " + threadLocal2.get());
            // 使用完移除,help GC
            threadLocal1.remove();
            threadLocal2.remove();
        }
    }
    public static void main(String[] args) {
        ThreadLocalDemo1 useDemo = new ThreadLocalDemo1();
        // 启动3个线程
        useDemo.startThreadArray(3);
    }
}

 每个线程会获取到属于自己的线程值,不会有任何的错乱

示例2

public class ThreadLocalDemo2 {
    /**
     * 初始化 num值。使用时,先通过get方法获取。
     */
    public static ThreadLocal<ThreadLocalDemo2.Number> threadLocalValue = new ThreadLocal<ThreadLocalDemo2.Number>() {
        @Override
        protected Number initialValue() {
            return new Number(0);
        }
    };
    /**
     * 数据类
     */
    private static class Number {
        public Number(int num) {
            this.num = num;
        }
        private int num;
        public int getNum() {
            return num;
        }
        public void setNum(int num) {
            this.num = num;
        }
        @Override
        public String toString() {
            return "Number [num=" + num + "]";
        }
    }
    /**
     * 线程类:
     */
    public static class ThreadDemo2 extends Thread {
        @Override
        public void run() {
            // 如果没有初始化,注意NPE。
            // static修饰的 number时,注释掉这句
            Number number = threadLocalValue.get();
            //每个线程计数加随机数
            Random r = new Random();
            number.setNum(number.getNum() + r.nextInt(100));
            //将其存储到ThreadLocal中
            threadLocalValue.set(number);
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //打印保存的随机值
            System.out.println(Thread.currentThread().getName() + " -》 " + threadLocalValue.get().getNum());
            threadLocalValue.remove();
            System.out.println(Thread.currentThread().getName() + " remove方法之后 -》 " + threadLocalValue.get().getNum());
        }
    }
    public static void main(String[] args) {
        // 启动5个线程
        for (int i = 0; i < 5; i++) {
            new ThreadDemo2().start();
        }
    }
}

每个线程可以通过initialValue方法初始化变量值。

到此这篇关于Java中的ThreadLocal线程变量详解的文章就介绍到这了,更多相关Java中的ThreadLocal内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java代理模式与动态代理之间的关系以及概念

    Java代理模式与动态代理之间的关系以及概念

    代理模式是开发中常见的一种设计模式,使用代理模式可以很好的对程序进行横向扩展。动态代理:代理类在程序运行时被创建的代理方式。关键在于动态,程序具有了动态特性,可以在运行期间根据不同的目标对象生成动态代理对象
    2023-02-02
  • SpringBoot整合Log4j2实现自定义日志打印失效的原因及解决

    SpringBoot整合Log4j2实现自定义日志打印失效的原因及解决

    本文给大家介绍了关于SpringBoot项目整合Log4j2实现自定义日志打印失效原因及解决办法,主要的原因是因为SpringBoot的logback包的存在,文中通过图文给大家了详细解决方法,需要的朋友可以参考下
    2024-01-01
  • mybatis中<choose>标签的用法说明

    mybatis中<choose>标签的用法说明

    这篇文章主要介绍了mybatis中<choose>标签的用法说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-06-06
  • Spring整合Struts2的两种方法小结

    Spring整合Struts2的两种方法小结

    下面小编就为大家带来一篇Spring整合Struts2的两种方法小结。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-07-07
  • Java 11 正式发布,这 8 个逆天新特性教你写出更牛的代码

    Java 11 正式发布,这 8 个逆天新特性教你写出更牛的代码

    美国当地时间9月25日,Oracle 官方宣布 Java 11 (18.9 LTS) 正式发布,可在生产环境中使用!这是自 Java 8 后的首个长期支持版本
    2018-09-09
  • IDEA的TODO的使用方式

    IDEA的TODO的使用方式

    这篇文章主要介绍了IDEA的TODO的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • Spring Boot通过Junit实现单元测试过程解析

    Spring Boot通过Junit实现单元测试过程解析

    这篇文章主要介绍了Spring Boot通过Junit实现单元测试过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-01-01
  • Java跳台阶实现思路和代码

    Java跳台阶实现思路和代码

    今天小编就为大家分享一篇关于Java跳台阶实现思路和代码,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-01-01
  • Java实战房屋租赁网的实现流程

    Java实战房屋租赁网的实现流程

    读万卷书不如行万里路,只学书上的理论是远远不够的,只有在实战中才能获得能力的提升,本篇文章手把手带你用java+SSM+jsp+mysql+maven实现一个房屋租赁网站,大家可以在过程中查缺补漏,提升水平
    2021-11-11
  • java 数据结构之堆排序(HeapSort)详解及实例

    java 数据结构之堆排序(HeapSort)详解及实例

    这篇文章主要介绍了java 数据结构之堆排序(HeapSort)详解及实例的相关资料,需要的朋友可以参考下
    2017-03-03

最新评论