深入理解Java并发编程之ThreadLocal

目录
  • ThreadLocal简介
  • ThreadLocal源码解析
  • 实现原理
  • ThreadLocalMap源码分析
  • InheritableThreadLocal
  • 参考资料

ThreadLocal简介

变量值的共享可以使用public static的形式,所有线程都使用同一个变量,如果想实现每一个线程都有自己的共享变量该如何实现呢?JDK中的ThreadLocal类正是为了解决这样的问题。

ThreadLocal类并不是用来解决多线程环境下的共享变量问题,而是用来提供线程内部的共享变量,在多线程环境下,可以保证各个线程之间的变量互相隔离、相互独立。在线程中,可以通过get()/set()方法来访问变量。ThreadLocal实例通常来说都是private static类型的,它们希望将状态与线程进行关联。这种变量在线程的生命周期内起作用,可以减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

我们先通过一个例子来看一下ThreadLocal的基本用法:

public class ThreadLocalTest {
	static class MyThread extends Thread {
		private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

		@Override
		public void run() {
			super.run();
			for (int i = 0; i < 3; i++) {
				threadLocal.set(i);
				System.out.println(getName() + " threadLocal.get() = " + threadLocal.get());
			}
		}
	}

	public static void main(String[] args) {
		MyThread myThreadA = new MyThread();
		myThreadA.setName("ThreadA");

		MyThread myThreadB = new MyThread();
		myThreadB.setName("ThreadB");

		myThreadA.start();
		myThreadB.start();
	}
}

运行结果(不唯一):

ThreadA threadLocal.get() = 0
ThreadB threadLocal.get() = 0
ThreadA threadLocal.get() = 1
ThreadA threadLocal.get() = 2
ThreadB threadLocal.get() = 1
ThreadB threadLocal.get() = 2

虽然两个线程都在向threadLocal对象中set()数据值,但每个线程都还是能取出自己设置的数据,确实可以达到隔离线程变量的效果。

ThreadLocal源码解析

ThreadLocal常用方法介绍

  • get()方法:获取与当前线程关联的ThreadLocal值。
  • set(T value)方法:设置与当前线程关联的ThreadLocal值。
  • initialValue()方法:设置与当前线程关联的ThreadLocal初始值。

当调用get()方法的时候,若是与当前线程关联的ThreadLocal值已经被设置过,则不会调用initialValue()方法;否则,会调用initialValue()方法来进行初始值的设置。通常initialValue()方法只会被调用一次,除非调用了remove()方法之后又调用get()方法,此时,与当前线程关联的ThreadLocal值处于没有设置过的状态(其状态体现在源码中,就是线程的ThreadLocalMap对象是否为null),initialValue()方法仍会被调用。

initialValue()方法是protected类型的,很显然是建议在子类重载该函数的,所以通常该方法都会以匿名内部类的形式被重载,以指定初始值,例如:

public class ThreadLocalTest {
	public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return Integer.valueOf(1);
		}
	};
}

remove()方法:将与当前线程关联的ThreadLocal值删除。

实现原理

ThreadLocal最简单的实现方式就是ThreadLocal类内部有一个线程安全的Map,然后用线程的ID作为Map的key,实例对象作为Map的value,这样就能达到各个线程的值隔离的效果。

JDK最早期的ThreadLocal就是这样设计的,但是,之后ThreadLocal的设计换了一种方式,我们先看get()方法的源码,然后进一步介绍ThreadLocal的实现方式:

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

get()方法主要做了以下事情:

1、调用Thread.currentThread()获取当前线程对象t;

2、根据当前线程对象,调用getMap(Thread)获取线程对应的ThreadLocalMap对象:

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

threadLocals是Thread类的成员变量,初始化为null:

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

3、如果获取的map不为空,则在map中以ThreadLocal的引用作为key来在map中获取对应的value e,否则转到步骤5;

4、若e不为null,则返回e中存储的value值,否则转到步骤5;

5、调用setInitialValue()方法,对线程的ThreadLocalMap对象进行初始化操作,ThreadLocalMap对象的key为ThreadLocal对象,value为initialValue()方法的返回值。

从上面的分析中,可以看到,ThreadLocal的实现离不开ThreadLocalMap类,ThreadLocalMap类是ThreadLocal的静态内部类。每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。这样的设计主要有以下几点优势:

  • 这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能;
  • 当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。

ThreadLocalMap源码分析

ThreadLocalMap是用来存储与线程关联的value的哈希表,它具有HashMap的部分特性,比如容量、扩容阈值等,它内部通过Entry类来存储key和value,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继承自WeakReference,通过上述源码super(k);可以知道,ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。

分析到这里,我们可以得到下面这个对象之间的引用结构图(其中,实线为强引用,虚线为弱引用):

我们知道,弱引用对象在Java虚拟机进行垃圾回收时,就会被释放,那我们考虑这样一个问题:

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部关联的强引用,那么在虚拟机进行垃圾回收时,这个ThreadLocal会被回收,这样,ThreadLocalMap中就会出现key为null的Entry,这些key对应的value也就再无妨访问,但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。

该强引用链如下:

CurrentThread Ref -> Thread -> ThreadLocalMap -> Entry -> value

因此,只要这个线程对象被gc回收,那些key为null对应的value也会被回收,这样也没什么问题,但在线程对象不被回收的情况下,比如使用线程池的时候,核心线程是一直在运行的,线程对象不会回收,若是在这样的线程中存在上述现象,就可能出现内存泄露的问题。

那在ThreadLocalMap中是如何解决这个问题的呢?

在获取key对应的value时,会调用ThreadLocalMap的getEntry(ThreadLocal<?> key)方法,该方法源码如下:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

通过key.threadLocalHashCode & (table.length - 1)来计算存储key的Entry的索引位置,然后判断对应的key是否存在,若存在,则返回其对应的value,否则,调用getEntryAfterMiss(ThreadLocal<?>, int, Entry)方法,源码如下:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

ThreadLocalMap采用线性探查的方式来处理哈希冲突,所以会有一个while循环去查找对应的key,在查找过程中,若发现key为null,即通过弱引用的key被回收了,会调用expungeStaleEntry(int)方法,其源码如下:

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
            (e = tab[i]) != null;
            i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

通过上述代码可以发现,若key为null,则该方法通过下述代码来清理与key对应的value以及Entry:

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;

此时,CurrentThread Ref不存在一条到Entry对象的强引用链,Entry到value对象也不存在强引用,那在程序运行期间,它们自然也就会被回收。expungeStaleEntry(int)方法的后续代码就是以线性探查的方式,调整后续Entry的位置,同时检查key的有效性。

在ThreadLocalMap中的set()/getEntry()方法中,都会调用expungeStaleEntry(int)方法,但是如果我们既不需要添加value,也不需要获取value,那还是有可能产生内存泄漏的。所以很多情况下需要使用者手动调用ThreadLocal的remove()函数,手动删除不再需要的ThreadLocal,防止内存泄露。若对应的key存在,remove()方法也会调用expungeStaleEntry(int)方法,来删除对应的Entry和value。

其实,最好的方式就是将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,可以防止内存泄露。

InheritableThreadLocal

InheritableThreadLocal继承自ThreadLocal,使用InheritableThreadLocal类可以使子线程继承父线程的值,来看一段示例代码:

public class ThreadLocalTest {
	private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() {
		@Override
		protected Integer initialValue() {
			return Integer.valueOf(10);
		}
	};

	static class MyThread extends Thread {
		@Override
		public void run() {
			super.run();
			System.out.println(getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get());
		}
	}

	public static void main(String[] args) {
		System.out.println(Thread.currentThread().getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get());

		MyThread myThread = new MyThread();
		myThread.setName("线程A");
		myThread.start();
	}
}

运行结果:

main inheritableThreadLocal.get() = 10

线程A inheritableThreadLocal.get() = 10

可以看到子线程成功继承了父线程的值。

父线程还可以设置子线程的初始值,只需要重写InheritableThreadLocal类的childValue(T)方法即可,将上述代码的inheritableThreadLocal 定义修改为如下方式:

private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() {
    @Override
    protected Integer initialValue() {
        return Integer.valueOf(10);
    }

    @Override
    protected Integer childValue(Integer parentValue) {
        return Integer.valueOf(5);
    }
};

运行结果为:

main inheritableThreadLocal.get() = 10

线程A inheritableThreadLocal.get() = 5

可以看到,子进程成功获取到了父进程设置的初始值。

使用InheritableThreadLocal类需要注意的一点是,如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,那子线程获取的还是旧值。

线程中用来实现上述功能的ThreadLocalMap类变量为

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

InheritableThreadLocal类的实现很简单,主要是重写了ThreadLocal类的getMap(Thread)方法和createMap(Thread, T)方法,将其中操作的ThreadLocalMap变量修改为了inheritableThreadLocals,这里不再进一步叙述。

参考资料

高洪岩:《Java多线程编程核心技术》

ThreadLocal和synchronized的区别

到此这篇关于深入理解Java并发编程之ThreadLocal 的文章就介绍到这了,更多相关Java ThreadLocal 内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • java 中ThreadLocal 的正确用法

    java 中ThreadLocal 的正确用法 用法一:在关联数据类中创建private static ThreadLocalThreaLocal的JDK文档中说明:ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread.如果我们希望通过某个类将状态(例如用户ID.事务ID)与线程关联起来,那么通常在这个类中定义private s

  • Java ThreadLocal 线程安全问题解决方案

    一.线程安全问题产生的原因 线程安全问题都是由全局变量及静态变量引起的 二.线程安全问题 SimpleDateFormate sdf = new SimpleDateFormat();使用sdf.parse(dateStr);sdf.format(date);在sdf内有一个对Caleadar对象的引用,在源码sdf.parse(dateStr);源码中calendar.clear();和calendar.getTime(); // 获取calendar的时间 如果 线程A 调用了 sdf.pa

  • Java ThreadLocal用法实例详解

    本文实例讲述了Java ThreadLocal用法.分享给大家供大家参考,具体如下: 目录 ThreadLocal的基本使用 ThreadLocal实现原理 源码分析(基于openjdk11) get方法: setInitialValue方法 getEntry方法 set方法 ThreadLocalMap的set方法 replaceStaleEntry方法 cleanSomeSlots方法 rehash方法 expungeStaleEntries方法 resize方法 ThreadLocal实现

  • java线程本地变量ThreadLocal详解

    介绍 ThreadLocal作为JDK1.2以来的一个java.lang包下的一个类,在面试和工程中都非常重要,这个类的主要目的是提供线程本地的变量,所以也有很多地方把这个类叫做线程本地变量 从字面理解,这个类为每个线程都创建了一个本地变量,实际上是ThreadLocal为变量在每个线程中都创建了一个副本,使得每个线程都可以访问自己内部的副本变量 通常提到多线程,都会考虑变量同步的问题,但是ThreadLocal并不是为了解决多线程共享变量同步的问题,而是为了让每个线程的变量不互相影响,相当于线

  • java ThreadLocal使用案例详解

    本文借由并发环境下使用线程不安全的SimpleDateFormat优化案例,帮助大家理解ThreadLocal. 最近整理公司项目,发现不少写的比较糟糕的地方,比如下面这个: public class DateUtil { private final static SimpleDateFormat sdfyhm = new SimpleDateFormat( "yyyyMMdd"); public synchronized static Date parseymdhms(String

  • java多线程编程之InheritableThreadLocal

    InheritableThreadLocal的作用: 当我们需要在子线程中使用父线程中的值得时候我们就可以像使用ThreadLocal那样来使用InheritableThreadLocal了. 首先我们来看一下InheritableThreadLocal的jdk源码: package java.lang; import java.lang.ref.*; public class InheritableThreadLocal<T> extends ThreadLocal<T> { p

  • 简单分析Java线程编程中ThreadLocal类的使用

    一.概述   ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量).也许把它命名为ThreadLocalVar更加合适.线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突.   从线程的角度看,每个线程都保持一个对

  • 深入解析Java中ThreadLocal线程类的作用和用法

    ThreadLocal与线程成员变量还有区别,ThreadLocal该类提供了线程局部变量.这个局部变量与一般的成员变量不一样,ThreadLocal的变量在被多个线程使用时候,每个线程只能拿到该变量的一个副本,这是Java API中的描述,通过阅读API源码,发现并非副本,副本什么概念?克隆品? 或者是别的样子,太模糊.   准确的说,应该是ThreadLocal类型的变量内部的注册表(Map<Thread,T>)发生了变化,但ThreadLocal类型的变量本身的确是一个,这才是本质!  

  • 详解Java中的ThreadLocal

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

  • 深入理解Java并发编程之ThreadLocal

    目录 ThreadLocal简介 ThreadLocal源码解析 实现原理 ThreadLocalMap源码分析 InheritableThreadLocal 参考资料 ThreadLocal简介 变量值的共享可以使用public static的形式,所有线程都使用同一个变量,如果想实现每一个线程都有自己的共享变量该如何实现呢?JDK中的ThreadLocal类正是为了解决这样的问题. ThreadLocal类并不是用来解决多线程环境下的共享变量问题,而是用来提供线程内部的共享变量,在多线程环境

  • Java并发编程之ThreadLocal详解

    目录 一.什么是ThreadLocal? 二.ThreadLocal的使用场景 三.如何使用ThreadLocal 四.数据库连接时的使用 五.ThreadLocal工作原理 六.小结 七.注意点 一.什么是ThreadLocal? ThreadLocal叫做线程本地变量,ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的.ThreadLocal为变量在每个线程中都创建了一个副本,则每个线程都可以访问自己内部的副本变量. 二.ThreadLocal的使用场景 1.当对象

  • 深入理解Java并发编程之LinkedBlockingQueue队列

    前面一篇文章我们介绍了使用CAS算法实现的非阻塞队列ConcurrentLinedQueue, 下面我们来介绍使用独占锁实现的阻塞队列LinkedBlockingQueue. LinkedBlockingQueue也是使用单向链表实现的,其也有两个Node,分别用来存放首.尾节点,并且还有一个初始值为0的原子变量count,用来记录队列元素个数.另外还有两个ReentrantLock的实例,分别用来控制元素入队和出队的原子性,其中takeLock用来控制同时只有一个线程可以从队列头获取元素,其他

  • Java 并发编程之ThreadLocal详解及实例

    Java 理解 ThreadLocal 摘要: ThreadLocal 又名线程局部变量,是 Java 中一种较为特殊的线程绑定机制,用于保证变量在不同线程间的隔离性,以方便每个线程处理自己的状态.进一步地,本文以ThreadLocal类的源码为切入点,深入分析了ThreadLocal类的作用原理,并给出应用场景和一般使用步骤. 一. 对 ThreadLocal 的理解 1). ThreadLocal 概述 ThreadLocal 又名 线程局部变量,是 Java 中一种较为特殊的 线程绑定机制

  • Java并发编程之threadLocal

    目录 1.ThreadLocal介绍 2.ThreadLocal使用实例 3.ThreadLocal实现原理 1.ThreadLocal介绍 多个线程访问同一个共享变量时特别容易出现并发问题,特别是多线程需要对共享变量进行写入时.为了保证线程安全,一般使用者在访问共享变量的时候需要进行适当的同步,如图 同步的一般措施是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的负担,那么有没有一种方法可以做到,当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢?其实ThreadL

  • 实例讲解Java并发编程之ThreadLocal类

    ThreadLocal类可以理解为ThreadLocalVariable(线程局部变量),提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回当前执行线程在调用set时设置的最新值.可以将ThreadLocal<T>视为 包含了Map<Thread,T>对象,保存了特定于该线程的值. 概括起来说,对于多线程资源共享的问题,同步机制采用了"以时间换空间"的方式,而ThreadLocal采用了"以空间

  • Java并发编程之Fork/Join框架的理解

    一.Fork/Join框架的理解 ForkJoinTask类属于java.util.concurrent 包下: ForkJoinTask类下有2个子类,分别为RecursiveTask和RecursiveAction类:(lz示例中使用RecursiveTask类进行重写compute()方法进行实现数值的累加计算) ForkJoinTask类 将一个大的任务拆分成多个子任务进行并行处理,最后将子任务结果合并成最后的计算结果,并进行输出. 二.Fork/Join框架使用示例 示例场景:对数值进

  • Java并发编程之Semaphore(信号量)详解及实例

    Java并发编程之Semaphore(信号量)详解及实例 概述 通常情况下,可能有多个线程同时访问数目很少的资源,如客户端建立了若干个线程同时访问同一数据库,这势必会造成服务端资源被耗尽的地步,那么怎样能够有效的来控制不可预知的接入量呢?及在同一时刻只能获得指定数目的数据库连接,在JDK1.5 java.util.concurrent 包中引入了Semaphore(信号量),信号量是在简单上锁的基础上实现的,相当于能令线程安全执行,并初始化为可用资源个数的计数器,通常用于限制可以访问某些资源(物

  • java并发编程之cas详解

    CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值.这听起来可能有一点复杂但是实际上你理解之后发现很简单,接下来,让我们跟深入的了解一下这项技术. CAS的使用场景 在程序和算法中一个经常出现的模式就是"check and act"模式.先检查后操作模式发生在代码中首先检查一个变量的值,然后再基于这个值做一些操作.下面是一个

  • Java并发编程之ConcurrentLinkedQueue源码详解

    一.ConcurrentLinkedQueue介绍 并编程中,一般需要用到安全的队列,如果要自己实现安全队列,可以使用2种方式: 方式1:加锁,这种实现方式就是我们常说的阻塞队列. 方式2:使用循环CAS算法实现,这种方式实现队列称之为非阻塞队列. 从点到面, 下面我们来看下非阻塞队列经典实现类:ConcurrentLinkedQueue (JDK1.8版) ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全的队列.当我们添加一个元素的时候,它会添加到队列的尾部,当我们

随机推荐