ThreadLocal全面讲解

概述

ThreadLocal无论是在平常的项目中还是面试都会比较频繁的出现,今天星期天,抽时间总结一下。首先ThreadLocal的出现是为解决多线程程序的并发问题的,ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。各个线程通过ThreadLocal的get和set方法就可以设置和得到当前线程对应的值。这样说起来比较抽象,我们下面通过实际的案例来解释说明。

目录

一、ThreadLocal的基本使用

二、ThrealLocal实现原理

三、ThreadLocal的内存泄漏问题


一、ThreadLocal的基本使用

为了说明ThreadLocal的作用,我们先看一下没有ThreadLocal的情况

public class ThreadLocalBDemo{
    static Integer num ;
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() +":" + num);
                num = 1;
                System.out.println(Thread.currentThread().getName() + ":" + num);
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() +":" +  num);
                num = 2;
                System.out.println(Thread.currentThread().getName() + ":" + num);
            }
        });

        thread1.start();
        try {
            /**
             *加入这个join方法主要是为了让线程thread1和thread2顺序执行
             * 我们在main线程中调用了thread1的join方法,那么main线程就会被阻塞
             * 等到thread1线程执行完后,main线程才会继续执行,所以在main线程中的
             * thread2.start()才会执行,这样就保证了thread1和thread2顺序执行
             */
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
    }
}

我们在ThreadLocalBDemo定义了一个全局变量 num,并且开启两个线程thread1和thread2,在两个线程中分别对num进行赋值处理,看一下输出结果:

Thread-0:null
Thread-0:1
Thread-1:1
Thread-1:2

通过输出结果我们可以看出,我们在thread1中给num赋值为1后,在thread2中的第一个输出语句中也拿到num=1的结果。在一些业务需求中我们不希望这个变量线程共享,而是每个线程都有属于它自己独立的一份,比如在实际业务中我们每个线程代表一个用户,这个num代表每个用户自己特有的订单数量等属性,那么像上面的程序就很明显不符合业务需求和逻辑。这个时候我们引入了ThrealLocal,ThreadLocal会为这个变量保存一个只属于它自己线程本身的变量副本,从而避免上面两个线程相互影响的情况。我们还是以上面的程序为案例,用ThreadLocal对其进行改造:

package com.zhangxudong;

public class ThreadLocalDemo {
   static ThreadLocal<Integer> threadLocal =  new ThreadLocal<Integer>();
    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(threadLocal.get());
                threadLocal.set(1);
                System.out.println(threadLocal.get());
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(threadLocal.get());
                threadLocal.set(2);
                System.out.println(threadLocal.get());
            }
        });

        thread1.start();
        try {
            thread1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        thread2.start();
    }
}

输出结果:

null
1
null
2

通过上面的输出结果我们可以看到虽然我在先执行的thread1中通过ThreadLocal的set方法赋值为1,但在后执行的thread2中的第一行输出语句中,通过get方法拿到的值确实null,这就实现了变量的隔离。那ThreadLocal是怎么实现的这种各个线程变量隔离的呢?下面我们就进入ThreadLocal源码内部了解一下。

二、ThrealLocal实现原理

首先我们看一下ThreadLocal在使用set方法赋值的时候,是怎么实现为每个线程保存一个变量副本的。我们先从set()方法入手:

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);
    }

可以看到,我们在thread1线程中利用ThreadLocal的set方法设置值的时候,在set方法内部首先是通过Thread t = Thread.currentThread()拿到当前的线程,这个线程当然就是thread1本身。紧接着调用了getMap(t)方法,这个方法传进去的参数就是当前线程thread1,那我们继续看一下getMap(t)这个方法:

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

可以看到它返回值是一个ThreadLocalMap类型的threadLocals变量,这个threadLocals其实就是线程类Thread中的一个成员变量,当前这个时候也就是thread1实例对象中的threadLocals成员变量:

//Thread类中的成员变量
ThreadLocal.ThreadLocalMap threadLocals = null;

那么这个ThreadLocalMap又是一个什么东西呢?这个你就简单的先理解为它就是一个类似我们平常用的Map结构,里面存放的是(key,value)键值对形式的数据。上面介绍的一切,总结起来其实就是一句话:在线程类中有一个成员变量threadLocals,这个成员变量的类型是ThreadLocalMap类型的,我们可以往里面放一条条“键值对”形式的数据。所以我们每new一个线程,那么在这个线程对象了里就有一个只属于这个线程对象的ThreadLocalMap类型的threadLocals变量。

继续接着上面的set源码看,通过getMap(t)方法在拿到这个属于当前线程对象(thread1)的threadLocals变量后(map),判断其是否为空,如果为空的话createMap()方法创建一个,如果不为空的话,就往里面存一个(key,value)形式的键值对,其中的这个key就是this,表示我们最开始创建的ThreadLocal对象,value就是我们最开始要设置的值。所以不管我们创建多少个线程实例,在每个线程实例中都有一个属于它自己的threadLocals变量,通过set方法往里面放值的时候,也是放到了只属于这个线程的threadLocals中去(虽然各个线程中threadLocals的key是一样的,都是我们最开始创建的ThreadLocal对象,但threadLocals本身是不一样的,它属于不同的线程,这里可能有点绕,好好理解一下),所以我们在拿值得时候,也就会只从属于当前线程得threadLocals中去拿,这就实现了变量的隔离。到此,ThreadLocal的set方法就讲解完了。

get()方法

理解了set方法其实get方法就很好理解了,我们看一下get方法得源码:

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

我们可以看到,get方法中首先也是拿到当前线程,然后也是通过getMap()方法拿到属于它得threadLocals,如果这个threadLocals不为空,则通过getEntry()方法得到存在里面的键值对Entry类型的对象,这个键值对中的键就是ThreadLocal对象,值就是我们之前在set方法中设置的值,最后的e.value拿到的就是这个值。

三、ThreadLocal的内存泄漏问题

在上面讲解THreadLocal的set()方法的时候我们知道,set方法会把ThreadLocal对象作为键,具体的value作为值,以键值对的形式存放在ThreadLocalMap类型的threadLocals变量中,而这个键值对整体是以一个Entry类型存在的。我们接着上面set源码中的map.set(this, value)继续点进去看:

private void set(ThreadLocal key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

上面的源码我们只看第8行和第28行就可以了,其他的先忽略,首先在这个set方法中的最开始有这么一行代码Entry[] tab = table,它维护了一个Entry类型的数组tab;接着看第28行:tab[i] = new Entry(key, value),可以发现是根据我们传过来的key和value值来new了一个Entry对象,并把它放到了Entry数组tab中。整体关系就是Thread中有一个ThreadLocalMap类型的成员变量threadLocals,而在ThreadLocalMap是ThreadLocal类的一个静态内部类,Entry又是ThreadLocalMap内部的一个静态内部类。  我们继续往下走,看看Entry的具体细节:

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

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

Entry 是ThreadLocalMap的一个静态内部类,并且它继承了弱引用(关于弱引用可以参考我前篇的文章java中四种引用类型),在Entry的构造方法中有这么一段:super(k),这说明它调用了其父类“弱应用WeakReference”的构造方法,并且将键值k传了进去(这个key值就是指向ThreadLocal对象的引用)。那么这个时候key就以一种弱引用的形式指向了ThreadLocal对象,我们通过一张图来说明一下这个:

                      

首先我们在最开始通过ThreadLocal t1 = new ThreadLocal()这新建一个ThreadLocal对象时,这个t1是一个强引用并且执行ThreadLocal对象。之后通过一系列上面的操作,我们知道键值对Entry中的key会以弱引用的形式也指向Thread Local对象。

那为什么Entry要使用弱引用呢?因为若使用的是强引用,即使t1=null,但key依然以强引用的形式指向ThreadLocal对象,因此ThreadLocal对象不会被释放掉,所以会有内存泄漏的问题。但如果是弱引用,当t1=null后,因为key是以弱引用的形式指向ThreadLocal的对象,那么gc碰到弱引用它是不关心的,依然会把ThreadLocal对象回收掉。但是虽然这样,ThreadLocal还是会存在内存泄漏问题,因为当ThreadLocal对象被回收后,key值就变成了null,这就导致整个Entry中的value再也无法访问到,因此依然存在内存泄漏问题。综上,ThreadLocal是存在内存泄漏的 问题的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值