Java ThreadLocal原理

本文详细探讨了Java中的ThreadLocal机制,包括其实现原理、ThreadLocalMap类的table结构和核心方法(set、get、remove)。此外,文章还介绍了ThreadLocal不支持继承性,以及如何通过InheritableThreadLocal实现线程间变量传递。最后,讨论了ThreadLocal可能导致的内存泄漏问题及其解决策略。

概述

threadlocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。

多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。

  ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一乐ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题

ThreadLocal实现原理

Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap类型的变量,我们通过查看内部内ThreadLocalMap可以发现实际上它类似于一个HashMap。在默认情况下,每个线程中的这两个变量都为null,只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们。除此之外,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的ThreadLocals变量里面。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的threadLocals中,所以不使用本地变量的时候需要调用remove方法将threadLocals中删除不用的本地变量。

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

/*
 * InheritableThreadLocal values pertaining to this thread. This map is
 * maintained by the InheritableThreadLocal class.
 */
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

 

ThreadLocalMap类

Thread类里面,维护了一个ThreadLocal.ThreadLocalMap的类。下面是Thread类中的ThreadLocalMap属性。

/* ThreadLocal values pertaining to this thread. This map is maintained by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

从Thread源码看出,ThreadLocalMap是ThreadLocal的一个内部类,ThreadLocalMap的关键属性介绍

table

ThreadLocalMap里面有一个Entry数组,用来保存数据;

/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
*/
private Entry[] table;

其中Entry继承了WeakReference,表示这个Entry是一个软引用类;Entry的结构可以看成类似于HashMap,由一个key(threadlocal实例)和value(保存的object)组成。WeakReference的介绍在后面的文章补充。

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

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

核心方法

set

首先看一下ThreadLocal的set源码

    public void set(T value) {
		//获取当前线程
        Thread t = Thread.currentThread();
		
		//线程里面实际存储的数据结构类型
        ThreadLocalMap map = getMap(t);
		
		//如果存在map就直接set,如果没有则创建map并set
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

其中getMap()方法获取到的是当前线程对应的threadLocals,即每个线程自己维护的一个ThreadLocalMap属性;

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

而线程的threadLocals属性刚开始都是null值,只有当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们,创建threadLocals,并且赋值(createMap)。所以实际上,threadLocal保存的值,不是保存在threadLocal实例里面,而是放在调用线程的ThreadLocals里面;

void createMap(Thread t, T firstValue) {
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}

实际的set操作,是调用ThreadLocalMap的set方法,再看一下ThreadLocalMap的set源码

private void set(ThreadLocal<?> key, Object value) {

	Entry[] tab = table;  //ThreadLocalMap里面的Entry数组
	
	//计算数组下标
	int len = tab.length; 
	int i = key.threadLocalHashCode & (len-1);

	//循环遍历entry数组
	for (Entry e = tab[i];
		 e != null;
		 e = tab[i = nextIndex(i, len)]) {
		//获取Entry的ThreadLocal引用实例
		ThreadLocal<?> k = e.get();

		//如果与要保存的是同一个线程,更新entry的值
		if (k == key) {
			e.value = value;
			return;
		}

		//如果当前的entry的key已经被回收了,则将新值更新进去
		if (k == null) {
			replaceStaleEntry(key, value, i);
			return;
		}
	}

	//如果遍历数组后没有找到,那么就新建一个entry保存
	tab[i] = new Entry(key, value);
	int sz = ++size;
	
	//对数组进行检查更新,如果长度已经达到阈值,那么重新hash,排列
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash();
}

可以看到,就是以线程里面的threadLocal属性实例作为key,来保存线程的值。

每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。

get

在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。

public T get() {
	//获取当前线程
	Thread t = Thread.currentThread();
	
	//获取线程的threadLocals属性
	ThreadLocalMap map = getMap(t);
	
	//如果当前线程已经有ThreadLocalMap属性,则从map里面获取当前的threadLocal实例做key的entry
	if (map != null) {
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null) {
			@SuppressWarnings("unchecked")
			T result = (T)e.value;
			return result;
		}
	}
	
	//如果Thread还没有初始化threadLocalMap属性,初始化null值
	return setInitialValue();
}

remove

remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量。

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

ThreadLocal不支持继承性

同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)

InheritableThreadLocal类

ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,下面是该类的源码

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

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

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。其中createMap方法在被调用(当前线程调用set方法时得到的map为null的时候需要调用该方法)的时候,创建的是inheritableThreadLocal而不是threadLocals。同理,getMap方法在当前调用者线程调用get方法的时候返回的也不是threadLocals而是inheritableThreadLocal。

什么时候创建InheritableThreadLocal类

下面用一个示例来表示InheritableThreadLocal的使用

public class InheritableThreadLocalApp extends Thread{
    public static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程开启。。。");
        inheritableThreadLocal.set("我是主线程赋值的");
        new InheritableThreadLocalApp().start();

        System.out.println("主线程休眠3s");
        Thread.sleep(3000);

        System.out.println("主线程在子线程赋值后获取本地变量值: " + inheritableThreadLocal.get());
        System.out.println("主线程结束。。。");
    }

    @Override
    public void run() {
        System.out.println("子线程开启。。。");
        System.out.println("子线程在赋值前获取本地变量值: " + inheritableThreadLocal.get());
        inheritableThreadLocal.set("我是子线程赋值的");
        System.out.println("子线程赋值后获取本地变量值: " + inheritableThreadLocal.get());
        System.out.println("子线程结束");
    }
}

输出的结果是

主线程开启。。。
主线程休眠3s
子线程开启。。。
子线程在赋值前获取本地变量值: 我是主线程赋值的
子线程赋值后获取本地变量值: 我是子线程赋值的
子线程结束
主线程在子线程赋值后获取本地变量值: 我是主线程赋值的
主线程结束。。。

可以看到,在子线程赋值前,子线程获取到的本地变量是父线程保存的。在子线程重新赋值后,获取到的是新的值,但是不会影响到原来主线程获取到自己保存的值。

下面看一下子线程创建,是如何获取到父线程的值的。

Thread的默认构造方法,会调用到

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize) {
        init(g, target, name, stackSize, null, true);
    }

其中init的  inheritThreadLocals 参数设置为true,在init的方法里面。

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }

        this.name = name;
		
		//(1)获取当前线程(父线程)
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */

            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }

            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }

        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();

        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }

        g.addUnstarted();

        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
		//(2)如果父线程的inheritableThreadLocal不为null
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
			//(3)设置子线程中的inheritableThreadLocals为父线程的inheritableThreadLocals
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;

        /* Set thread ID */
        tid = nextThreadID();
    }

在init方法中,首先(1)处获取了当前线程(父线程),然后(2)处判断当前父线程的inheritableThreadLocals是否为null,然后调用createInheritedMap将父线程的inheritableThreadLocals作为构造函数参数创建了一个新的ThreadLocalMap变量,然后赋值给子线程。

在构造函数中将父线程的inheritableThreadLocals成员变量的值赋值到新的ThreadLocalMap对象中。返回之后赋值给子线程的inheritableThreadLocals。总之,InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritableThreadLocals变量中,当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候,就会创建当前线程的inheritableThreadLocals变量。而父线程创建子线程的时候,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inheritableThreadLocals变量中。

ThreadLocal内存泄漏问题

ThreadLocalMap中的Entry,继承了WeakReference类,说明Entry是一个弱引用类。

关于Java中的四种引用类型,

  1. 强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
  2. 软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中
  3. 弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null
  4. 虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)

内存泄漏分析

因为Entry是弱引用,那么在GC的时候,会被回收掉。如果GC回收了Entry的key对ThreadLocal的弱引用。那么key为null,也就永远不可能再访问到原来key对应的value,此时便出现了泄漏。

ThreadLocal如何避免内存泄漏

在get和set方法里面,当对Entry数组进行遍历的时候,如果发现存在老的entry的key是null值,就会出发遍历table数组,清除掉所有key为null的entry;

规避方法

ThreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的value不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值