zoukankan      html  css  js  c++  java
  • 浅析 ThreadLocal

    原文地址:浅析 ThreadLocal
    博客地址:http://www.extlight.com

    一、介绍

    根据 Java 官方文档的描述,我们可知 ThreadLocal 类用于提供线程内部的局部变量,其在多线程环境下能保证各个线程内部变量的隔离性。

    换言之,ThreadLocal 提供线程内的局部变量,不同线程之间不会相互干扰,该变量作用范围贯穿线程的生命周期,减少同一线程内多个方法或组件之间一些公共变量传递的复杂度。

    二、使用

    2.1 常用方法

    返回值 方法名 描述
    T get() 返回此线程局部变量的当前线程副本中的值
    void remove() 移除此线程局部变量当前线程的值
    void set(T value) 将此线程局部变量的当前线程副本中的值设置为指定值

    2.2 案例演示

    需求:用 3 名画家在一个画布上各自绘制一种颜色,并打印出其绘制的颜色。

    /**
     * 画布类
     */
    public class Canvas {
    	
    	private String content;
    
    	public String getContent() {
    		return content;
    	}
    
    	public void setContent(String content) {
    		this.content = content;
    	}
    }
    
    /**
     * 画家类
     */
    public class Painter extends Thread {
    
    	private String name;
    	
    	private Canvas canvas;
    	
    	private String color;
    	
    	public Painter(String name, Canvas canvas, String color) {
    		this.name = name;
    		this.canvas = canvas;
    		this.color = color;
    	}
    
    	@Override
    	public void run() {
    		canvas.setContent(color);
    		System.out.println(this.name + "在画板绘制" + canvas.getContent());
    		
    	}
    }
    
    /**
     * 启动类
     */
    public class Demo {
    
    	public static void main(String[] args) {
    		
    		// 创建画布
    		Canvas canvas = new Canvas();
    		
    		Painter painter1 = new Painter("小强", canvas, "红色");
    		Painter painter2 = new Painter("旺财", canvas, "黄色");
    		Painter painter3 = new Painter("狗蛋", canvas, "蓝色");
    		
    		painter1.start();
    		painter2.start();
    		painter3.start();
    		
    	}
    }
    

    执行结果如下:

    小强在画板绘制蓝色
    旺财在画板绘制黄色
    狗蛋在画板绘制黄色
    

    显然,在多线程访问同一个资源(画布)的情况下,输出结果出现并发问题。

    现有 2 种解决方案:一种是在 run 方法中加入 synchronized 同步代码块,另一种是使用 ThreadLocal 改造 Canvas 类型。

    由于本篇着重介绍 ThreadLocal, 故下边我们通过第二种方式解决上述问题。

    修改 Canvas 类为如下:

    public class Canvas {
    	
    	private ThreadLocal<String> map = new ThreadLocal();
    
    	public String getContent() {
    		return map.get();
    	}
    
    	public void setContent(String content) {
    		map.set(content);
    	}
    }
    

    启动执行类,运行结果如下:

    小强在画板绘制红色
    狗蛋在画板绘制蓝色
    旺财在画板绘制黄色
    

    结果正常输出。

    2.3 ThreadLocal 与 synchronized 区别

    名称 原理 侧重点
    ThreadLocal 空间换时间,每个线程都都提供一份变量副本,从而实现同时访问而不相互干扰 多线程之间资源相互隔离
    synchronized 时间换空间,只提供一个变量,让线程排队访问 多线程之间共享资源,同步访问

    三、ThreadLocal 内部结构

    在看源码之前,我们可以试着猜测 ThreadLocal 内部结构是怎样的。

    比如,ThreadLocal 内部定义了一个 Map 容器。当调用 ThreadLocal 实例的 set 方法时,以当前线程名/当前线程实例作为 key, 需要保存的内容作为 value 进行操作。当调用 get 方式时,以当前线程名/当前线程实例作为 key 获取数据。

    上述方案看似可以正常实现功能,实则存在一些问题:

    1) 由 ThreadLocal 维护 key-value 容器,当线程增多并调用 ThreadLocal 实例 的set 方法时,key-value 容器也随之增大,即内存占用也随之增大。
    
    2) 当调用 ThreadLocal 实例方法的对象为线程池中的线程时,无法区分线程是否被循环使用,即当前线程之前已从线程池中被拿出调用 ThreadLocal 实例的 set 方法,如果当前调用 get 方法就会取出之前的数据造成数据污染等问题。
    

    那么,ThreadLocal 内部到底是怎么实现线程间内部变量的隔离性的呢?

    如上图,由 Thread 实例内部维护名为 ThreadLocalMap 的容器,其元素是以 ThreadLocal 实例为 key ,保存对象作为 value 的数据结构,与我们猜测的实现方式相反。

    对比我们之前设想的方案,JDK 实现方案有 2 个好处:

    1) Map 存储的 Entry 数量变少
    
    2) 当线程销毁时,ThreadLocalMap 也随之销毁,减少内存使用
    

    四、源码分析

    4.1 ThreadLocal 源码

    我们针对常用的 setgetremove 方法进行源码剖析。

    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取当前线程对象维护的 ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 如果 map 存在设置 entry
            map.set(this, value);
        else
            // 如果 map 不存在,由于 threadLocal 实例帮忙创建并绑定数据
            createMap(t, value);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
        
    

    set 方法执行流程:

    1) 获取当前线程对象
    2) 通过当前线程对象获取 ThreadLocalMap 对象
    3) 如果 ThreadLocalMap 对象存在,则将入参设置进 ThreadLocalMap 对象中
    4) 如果 ThreadLocalMap 对象不存在,则给当前线程创建 ThreadLocalMap 对象并设置入参
    

    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取当前线程对象维护的 ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 如果 map 不为空,以当前的 ThreadLocal 实例为 key, 获取数据
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果 map 为空,初始化值,通常为 null
        return setInitialValue();
    }
    
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
    
    protected T initialValue() {
        return null;
    }
    

    get 方法执行流程:

    1) 获取当前线程对象
    2) 通过当前线程对象获取 ThreadLocalMap 对象
    3) 如果 ThreadLocalMap 对象存在,则以当前的 ThreadLocal 实例为 key, 获取数据
    4) 如果 ThreadLocalMap 对象不存在,则通过 initialValue 方法初始化 value 值。
    

    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
    

    remove 方法执行流程:

    1) 通过当前线程对象获取 ThreadLocalMap 对象
    2) 如果 ThreadLocalMap 对象存在,则以当前的 ThreadLocal 实例为 key, 进行数据删除
    

    4.2 ThreadLocalMap 源码

    ThreadLocalMapThreadLocal 的内部类,其没有实现 Map 接口,单独实现了 Map 的功能。

    成员变量:

    /**
     * 初始容量,必须是 2 的整次幂
     */
    private static final int INITIAL_CAPACITY = 16;
    
    /**
     * 存放数据的 table,数据长度也是 2 的整次幂
     */
    private Entry[] table;
    
    /**
     * 数组中 entry 的个数
     */
    private int size = 0;
    
    /**
     * 进行扩展的阀值
     */
    private int threshold; // Default to 0
    

    Entry 内部类:

    static class Entry extends WeakReference<ThreadLocal<?>> {
    
        Object value;
    
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    

    Entry 继承 WeakReference 类,也就是 key 是弱引用,其目的是将 ThreadLocal 对象的生命周期与线程的生命周期解绑。

    五、内存泄漏

    虽然 ThreadLocal 作为弱引用 key 来使用,但是在某些情况下还是会造成内存泄漏问题。 在分析内存泄漏之前,我们先补充几个概念:

    内存溢出:没有足够的内存供申请者使用
    
    内存泄漏:程序中已动态分配的堆内存由于某种原因未释放或无法释放,造成系统内存浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,该问题最终会导致内存溢出
    
    强引用:常见的对象引用,只要还有强引用指向一个对象,表明对象还“活着”,垃圾回收器就不会回收该对象
    
    弱引用:垃圾回收期一旦发现只具有弱引用指向的对象,不管当前内存空间是否足够,都会回收该对象
    

    了解了基本概念,接下来我们分析使用 ThreadLocal 出现内存泄漏的情况:

    上图为一个线程使用 ThreacLocal 时的内存结构图,实线箭头表示强引用,虚线箭头表示弱引用。

    当 ThreadLocal 使用结束,栈内存的 ThreadLocal 引用被回收,即引用 1 不再指向 ThreadLocal 对象。
    
    由于引用 2 是弱引用,没有任何强引用指向 ThreadLocal 对象,因此 ThreadLocal 对象会被 GC 回收,此时 Entry 的 key = null
    
    如果我们没有会手动删除 Entry 对象,且当前线程一直在运行中,会存在一个强引用链 Thread 引用-> Thread 对象-> ThreadLocal 对象-> Entry 对象 -> Value,由于 value 不会被回收,而 key 又为 null, value 这块内存就永远无法被访问,这就造成了内存泄漏,
    
    

    既然使用弱引用作为 ThreadLocalMap 的 key 会造成内存泄漏,那为什么还要使用它呢?

    其实,在 ThreadLocalMapsetgetEntry 方法中,会对 key 为 null 进行判断,如果为 null, 那么会将 value 也设置为 null。

    换言之,在使用 ThreadLocal 的线程依然运行的情况下,我们忘记调用 remove 方法,弱引用比强引用多一层保障。弱引用指向的 ThreadLocal 对象被回收,对应的 value 在 TheadLocalMap 调用 setgetEntryremove 任一方法时被设置为 null, 避免内存泄漏。

    六、总结

    适用于多线程并发场景
    
    使用 ThreadLocal 在同一线程,不同组件中可传递公共变量
    
    每个线程的变量都是相互独立,互不影响
    

    注意:为防止内存泄漏,养成良好开发习惯,使用完 ThreadLocal 务必手动调用 remove 方法。

  • 相关阅读:
    【IDEA】(4)---很好用的DEBUG功能
    【IDEA】(3)---非常实用提高开发效率和水平的插件
    【IDEA】(2)---MAC代码模版
    【IDEA】(1)---MAC下常用快捷键
    Shell(2)—数组
    MySQL(12)---纪录一次left join一对多关系而引起的BUG
    MySQL(11)---约束
    Shell(1)---变量
    算法(2)---算法复杂度理论
    MySQL(10)---自定义函数
  • 原文地址:https://www.cnblogs.com/moonlightL/p/13947296.html
Copyright © 2011-2022 走看看