threadLocal 本地线程浅谈

threadLocal

  • 背景
  • 原理
    • set()方法
    • get()方法
    • setInitialValue()方法
    • 总结
  • 优点
  • 缺陷:内存泄漏
    • 概念理解
    • 图片理解
    • 解决方案
  • 使用场景
    • 典型用例
  • 参考链接
  • 测试用例
    • demo
      • 目的
      • 代码
      • 运行结果

背景

首先如果多线程访问一个共享变量的时候,会出现并发的情况,导致线程不安全,为了解决这一情况,有一种方法是采用加锁的方式(如synchronized等),这样保证每次访问变量的时候只有一个线程,其他线程都在等待。但是这样的话,会使得执行效率降低。因此有了第二种方法:采用threadlocal。

从名字我们就可以看到ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。

简单来说就是threadlocal会给要访问共享变量的每一个线程创建一个共享变量的本地副本,线程直接访问自己的本地副本即可,就不会出现不安全的情况了。
threadLocal 本地线程浅谈_第1张图片

原理

ThreadLocal的作用是给每一个线程创建一个副本,请看set方法

set()方法

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);//这里的this是当前调用set方法的threadlocal实例
        else
            createMap(t, value);
    }
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

调用当期线程t,返回当前线程t中的成员变量threadLocals。而threadLocals其实就是ThreadLocalMap

向ThreadLocal进行set值的操作,实际上是向当前线程对象中的****ThreadLocalMap****存入值,ThreadLocalMap我们可以简单的理解成一个Map,而向这个Map存值的key就是ThreadLocal实例本身。

get()方法

public T get() {    
    Thread t = Thread.currentThread();    
    ThreadLocalMap map = getMap(t);    
    if (map != null) {        
        ThreadLocalMap.Entry e = map.getEntry(this);        
        if (e != null) {                 
            T result = (T)e.value;           
            return result;        
        }   
    }    
    return setInitialValue();
}

那么我们再看看ThreadLocalMap做了什么?

当createMap的时候

void createMap(Thread t, T firstValue) {    
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];//16
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

当new Entry的时候

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

它将ThreadLocal作为key值存了进去。

ThreadLoalMap的Entry是继承WeakReference,和HashMap很大的区别是,Entry中没有next字段,所以就不存在链表的情况了

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;
}
void createMap(Thread t, T firstValue) {   
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

总结

首先,在每个线程Thread内部有一个ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。

然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里查找。

(1)每个Thread维护着一个ThreadLocalMap的引用

(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储

(3)使用ThreadLocal变量创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。
    理解:
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
该处的threadLocals来自:当前线程的threadLocals变量,但是该变量是ThreadLocal的。如下图的第二个图

(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中

(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。

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

threadLocal 本地线程浅谈_第2张图片
threadLocal 本地线程浅谈_第3张图片

优点

解决线程不安全的问题

实现在线程级别传递变量

缺陷:内存泄漏

存在的问题:容易导致内存泄漏

概念理解

①Memory overflow:内存溢出,没有足够的内存提供申请者使用。
②Memory leak:内存泄漏,程序申请内存后,无法释放已申请的内存空间,内存泄漏的堆积终将导致内存溢出。
    
类型	         回收时间	                              应用场景
强引用	    一直存活,除非GC Roots不可达	          所有程序的场景,基本对象,自定义对象等
软引用	    内存不足时会被回收	                 一般用在对内存非常敏感的资源上,用作缓存的场景比较                                                 多,例如:网页缓存、图片缓存
弱引用	    只能存活到下一次GC前	                  生命周期很短的对象,例如ThreadLocal中的Key 
虚引用	    随时会被回收,创建了可能很快就会被回收	   可能被JVM团队内部用来跟踪JVM的垃圾回收活动

图片理解

threadLocal 本地线程浅谈_第4张图片
上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。

1、Thread中有一个map,就是ThreadLocalMap

2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。

3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收

4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

从上图中可以看出,hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部**强引用**时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。

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

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

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


线程迟迟不结束的情况:

线程执行结束之后,被放在了线程池中没有被销毁。

解决方案

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。
ThreadLocal自身并不储存值,而是作为一个key来让线程从ThreadLocal获取value。Entry是中的key是弱引用,所以jvm在垃圾回收时如果外部没有强引用来引用它,ThreadLocal必然会被回收。但是,作为ThreadLocalMap的key,ThreadLocal被回收后,ThreadLocalMap就会存在null,但value不为null的Entry。若当前线程一直不结束,可能是作为线程池中的一员,线程结束后不被销毁,或者分配(当前线程又创建了ThreadLocal对象)使用了又不再调用get/set方法,就可能引发内存泄漏。其次,就算线程结束了,操作系统在回收线程或进程的时候不是一定杀死线程或进程的,在繁忙的时候,只会清除线程或进程数据的操作,重复使用线程或进程(线程id可能不变导致内存泄漏)。因此,key弱引用并不是导致内存泄漏的原因,而是因为ThreadLocalMap的生命周期与当前线程一样长,并且没有手动删除对应value。

那么,为什么要将Entry中的key设为弱引用?相反,设置为弱引用的key能预防大多数内存泄漏的情况。如果key 使用强引用,引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。如果key为弱引用,引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被GC回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

使用场景

总:使用在多线程场景下!

  • 1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
  • 2、线程间数据隔离
  • 3、进行事务操作,用于存储线程事务信息。
  • https://blog.csdn.net/java_raylu/article/details/73729162
  • 4、数据库连接,Session会话管理。

1.针对线程不安全的变量进行处理(SimpleDateFormat)保证线程安全

2.解决共享参数问题(数据隔离)

ThreadLocal 也常用在多线程环境中,某个方法处理一个业务,需要递归依赖其他方法时,而要在这些方法中共享参数的问题。

例如有方法 a(),在该方法中调用了方法b(),而在方法 b() 中又调用了方法 c(),即 a–>b—>c。如果 a,b,c 现在都需要使用一个字符串参数 args。

常用的做法是 a(String args)–>b(String args)—c(String args)。但是使用ThreadLocal,可以用另外一种方式解决:

在某个接口中定义一个ThreadLocal 对象
方法 a()、b()、c() 所在的类实现该接口
在方法 a()中,使用 threadLocal.set(String args) 把 args 参数放入 ThreadLocal 中
方法 b()、c() 可以在不用传参数的前提下,在方法体中使用 threadLocal.get() 方法就可以得到 args 参数

典型用例

1.HibernateUtil工具类

2.不同的线程对象设置Bean属性,保证各个线程Bean对象的独立性

3.spring的事务隔离源码

参考链接

  • https://blog.csdn.net/zzti_erlie/article/details/90731844?utm_medium=distribute.pc_relevant.none-task-blog-2defaultBlogCommendFromMachineLearnPai2default-4.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2defaultBlogCommendFromMachineLearnPai2default-4.control
  • https://www.zhihu.com/question/341005993

测试用例

demo

目的

用于验证threadlocal的set值,是给每一个线程的副本进行了set操作

代码

public class ThreadLocalTest {

    private static ThreadLocal<Integer> num = new ThreadLocal<Integer>() {
        // 重写这个方法,可以修改“num”的初始值,默认是null
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {

        Thread one=new Thread(new Runnable() {
            @Override
            public void run() {
                num.set(1);
                System.out.println("one.num="+num.get());
            }
        });
        one.start();

        Thread two=new Thread(new Runnable() {
            @Override
            public void run() {
                num.set(2);
                System.out.println("two.num="+num.get());
            }
        });
        two.start();

        try{
            Thread.sleep(500);
            System.out.println("num="+num.get());
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 程序结果重点看的是主线程输出的是0,如果是一个普通变量,在一号线程和二号线程中将普通变量设置为1和2,那么在一二号线程执行完毕后在打印这个变量,输出的值肯定是1或者2(到底输出哪一个由操作系统的线程调度逻辑有关)。但使用ThreadLocal变量通过两个线程赋值后,在主线程程中输出的却是初始值0。在这也就是为什么“一个ThreadLocal在一个线程中是共享的,在不同线程之间又是隔离的”,每个线程都只能看到自己线程的值,这也就是ThreadLocal的核心作用:实现线程范围的局部变量。
     *
     *
     *
     * 也就是说,想要存入的ThreadLocal中的数据实际上并没有存到ThreadLocal对象中去,而是以这个ThreadLocal实例作为key存到了当前线程中的一个Map中去了,获取ThreadLocal的值时同样也是这个道理。这也就是为什么ThreadLocal可以实现线程之间隔离的原因了。
     *
     */
}

运行结果

one.num=1
two.num=2
num=0

你可能感兴趣的:(线程,java)