今天我们一起探讨下ThreadLocal的实现原理和源码分析。首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两个应用场景。相信本文一定能让大家完全了解ThreadLocal。

ThreadLocal是什么?

  ThreadLocal是啥?以前面试别人时就喜欢问这个,有些伙伴喜欢把它和线程同步机制混为一谈,事实上ThreadLocal与线程同步无关。ThreadLocal虽然提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。那么ThreadLocal到底是什么呢?

  ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。

  通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。概括起来说,ThreadLocal为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

API说明

1、ThreadLocal()

创建一个线程本地变量。

2、T get()

返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。

3、protected  T initialValue()

返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。

若该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 ThreadLocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialValue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。

4、void remove()

移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。

5、void set(T value)

将此线程局部变量的当前线程副本中的值设置为指定值。

ThreadLocal使用示例

假设我们要为每个线程关联一个唯一的序号,在每个线程周期内,我们需要多次访问这个序号,这时我们就可以使用ThreadLocal了

 package concurrent;

 import java.util.concurrent.atomic.AtomicInteger;

 /**
* Created by chenhao on 2018/12/03.
*/
public class ThreadLocalDemo {
public static void main(String []args){
for(int i=0;i<5;i++){
final Thread t = new Thread(){
@Override
public void run(){
System.out.println("当前线程:"+Thread.currentThread().getName()+",已分配ID:"+ThreadId.get());
}
};
t.start();
}
}
static class ThreadId{
//一个递增的序列,使用AtomicInger原子变量保证线程安全
private static final AtomicInteger nextId = new AtomicInteger(0);
//线程本地变量,为每个线程关联一个唯一的序号
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return nextId.getAndIncrement();//相当于nextId++,由于nextId++这种操作是个复合操作而非原子操作,会有线程安全问题(可能在初始化时就获取到相同的ID,所以使用原子变量
}
}; //返回当前线程的唯一的序列,如果第一次get,会先调用initialValue,后面看源码就了解了
public static int get() {
return threadId.get();
}
}
}

运行结果:

当前线程:Thread-4,已分配ID:1
当前线程:Thread-0,已分配ID:0
当前线程:Thread-2,已分配ID:3
当前线程:Thread-1,已分配ID:4
当前线程:Thread-3,已分配ID:2

ThreadLocal源码分析

  ThreadLocal最常见的操作就是set、get、remove三个动作,下面来看看这三个动作到底做了什么事情。首先看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);
}

第 2 行代码取出了当前线程 t,然后调用getMap(t)方法时传入了当前线程,换句话说,该方法返回的ThreadLocalMap和当前线程有点关系,我们先记录下来。进一步判定如果这个map不为空,那么设置到Map中的Key就是this,值就是外部传入的参数。这个this是什么呢?就是定义的ThreadLocal对象。
代码中有两条路径需要追踪,分别是getMap(Thread)和createMap(Thread , T)。首先来看看getMap(t)操作

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

在这里,我们看到ThreadLocalMap其实就是线程里面的一个属性,它在Thread类中的定义是:

ThreadLocal.ThreadLocalMap threadLocals = null;

即:每个Thread对象都有一个ThreadLocal.ThreadLocalMap成员变量,ThreadLocal.ThreadLocalMap是一个ThreadLocal类的静态内部类(如下所示),所以Thread类可以进行引用.所以每个线程都会有一个ThreadLocal.ThreadLocalMap对象的引用

static class ThreadLocalMap {

首先获取当前线程的引用,然后获取当前线程的ThreadLocal.ThreadLocalMap对象,如果该对象为空就创建一个,如下所示:

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

这个this变量就是ThreadLocal的引用,对于同一个ThreadLocal对象每个线程都是相同的,但是每个线程各自有一个ThreadLocal.ThreadLocalMap对象保存着各自ThreadLocal引用为key的值,所以互不影响,而且:如果你新建一个ThreadLocal的对象,这个对象还是保存在每个线程同一个ThreadLocal.ThreadLocalMap对象之中,因为一个线程只有一个ThreadLocal.ThreadLocalMap对象,这个对象是在第一个ThreadLocal第一次设值的时候进行创建,如上所述的createMap方法.

ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}

至此,ThreadLocal的原理我们应该已经清楚了,简单来讲,就是每个Thread里面有一个ThreadLocal.ThreadLocalMap threadLocals作为私有的变量而存在,所以是线程安全的。ThreadLocal通过Thread.currentThread()获取当前的线程就能得到这个Map对象,同时将自身(ThreadLocal对象)作为Key发起写入和读取,由于将自身作为Key,所以一个ThreadLocal对象就能存放一个线程中对应的Java对象,通过get也自然能找到这个对象。

最后来看看get()、remove()代码,或许看到这里就可以认定我们的理论是正确的

 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();
} public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

  第一句是取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是  this,而不是当前线程t。

  如果获取成功,则返回value值。

  如果map为空,则调用setInitialValue方法返回value。

  可以看出第12行处的方法setInitialValue()只有在线程第一次使用 get() 方法访问变量的时候调用。如果线程先于 get 方法调用 set(T) 方法,则不会在线程中再调用 initialValue 方法。

protected T initialValue() {
return null;
}

该方法定义为protected级别且返回为null,很明显是要子类实现它的,所以我们在使用ThreadLocal的时候一般都应该覆盖该方法,创建匿名内部类重写此方法。该方法不能显示调用,只有在第一次调用get()或者set()方法时才会被执行,并且仅执行1次。

对于ThreadLocal需要注意的有两点:
1. ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key。
2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小伙伴会弄错他们的关系。

ThreadLocal的应用场景

  最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。如:

/**
* 数据库连接管理类
*/
public class ConnectionManager { /** 线程内共享Connection,ThreadLocal通常是全局的,支持泛型 */
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>(); public static Connection getCurrConnection() {
// 获取当前线程内共享的Connection
Connection conn = threadLocal.get();
try {
// 判断连接是否可用
if(conn == null || conn.isClosed()) {
// 创建新的Connection赋值给conn(略)
// 保存Connection
threadLocal.set(conn);
}
} catch (SQLException e) {
// 异常处理
}
return conn;
} /**
* 关闭当前数据库连接
*/
public static void close() {
// 获取当前线程内共享的Connection
Connection conn = threadLocal.get();
try {
// 判断是否已经关闭
if(conn != null && !conn.isClosed()) {
// 关闭资源
conn.close();
// 移除Connection
threadLocal.remove();
conn = null;
}
} catch (SQLException e) {
// 异常处理
}
}
}

也可以重写initialValue方法

private static ThreadLocal<Connection> connectionHolder= new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
}; public static Connection getConnection() {
return connectionHolder.get();
}

Hiberante的Session 工具类HibernateUtil

public class HibernateUtil {
private static Log log = LogFactory.getLog(HibernateUtil.class);
private static final SessionFactory sessionFactory; //定义SessionFactory
static {
try {
// 通过默认配置文件hibernate.cfg.xml创建SessionFactory
sessionFactory = new Configuration().configure().buildSessionFactory();
} catch (Throwable ex) {
log.error("初始化SessionFactory失败!", ex);
throw new ExceptionInInitializerError(ex);
}
}
//创建线程局部变量session,用来保存Hibernate的Session
public static final ThreadLocal session = new ThreadLocal();
/**
* 获取当前线程中的Session
* @return Session
* @throws HibernateException
*/
public static Session currentSession() throws HibernateException {
Session s = (Session) session.get();
// 如果Session还没有打开,则新开一个Session
if (s == null) {
s = sessionFactory.openSession();
session.set(s); //将新开的Session保存到线程局部变量中
}
return s;
}
public static void closeSession() throws HibernateException {
//获取线程局部变量,并强制转换为Session类型
Session s = (Session) session.get();
session.set(null);
if (s != null)
s.close();
}
}

在这个类中,由于没有重写ThreadLocal的initialValue()方法,则首次创建线程局部变量session其初始值为null,第一次调用currentSession()的时候,线程局部变量的get()方法也为null。因此,对session做了判断,如果为null,则新开一个Session,并保存到线程局部变量session中

ThreadLocal使用的一般步骤

1、在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。

2、在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。

3、在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

ThreadLocal为什么会内存泄漏

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,说明该map的key为一个弱引用,我们知道弱引用有利于GC回收。

  ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue,如果当前线程再迟迟不结束的话,这些keynullEntryvalue就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocalget(),set(),remove()的时候都会清除线程ThreadLocalMap里所有keynullvalue。但是这些被动的预防措施并不能保证不会内存泄漏:

  • 使用staticThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。

  • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

为什么使用弱引用

  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,getremove的时候会被清除。

  1、可以知道使用弱引用可以多一层保障:理论上弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除;但是如果分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就有可能导致内存泄漏

  2、通常,我们需要保证作为key的ThreadLocal类型能够被全局访问到,同时也必须保证其为单例,因此,在一个类中将其设为static类型便成为了惯用做法,如上面例子中都是用了Static修饰。使用static修饰ThreadLocal对象的引用后,ThreadLocal的生命周期跟Thread一样长,因此ThreadLocalMap的Key也不会被GC回收,弱引用形同虚设,此时就极容易造成ThreadLocalMap内存泄露。

关键在于threadLocal如果用Static修饰,如果是多线程操作threadlocal,当前线程结束后,ThreadLocal对象作为GCRoot还在其他线程中,这是弱引用就不能被回收,也就是当前Thread中的Map中的key还不会被回收,也就是很多线程中都有threadlocal为key的map不会被回收,那就会出现内存泄露。

ThreadLocal 最佳实践

综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

总结

  • ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。这点至关重要。
  • 每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。
  • ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要木得视为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部。
  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据,避免造成内存泄露。

并发编程(四)—— ThreadLocal源码分析及内存泄露预防的更多相关文章

  1. Java并发编程之ThreadLocal源码分析

    ## 1 一句话概括ThreadLocal<font face="微软雅黑" size=4>  什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象 ...

  2. 多线程高并发编程(3) -- ReentrantLock源码分析AQS

    背景: AbstractQueuedSynchronizer(AQS) public abstract class AbstractQueuedSynchronizer extends Abstrac ...

  3. 多线程高并发编程(10) -- ConcurrentHashMap源码分析

    一.背景 前文讲了HashMap的源码分析,从中可以看到下面的问题: HashMap的put/remove方法不是线程安全的,如果在多线程并发环境下,使用synchronized进行加锁,会导致效率低 ...

  4. ThreadLocal源码解析,内存泄露以及传递性

    我想ThreadLocal这东西,大家或多或少都了解过一点,我在接触ThreadLocal的时候,觉得这东西很神奇,在网上看了很多博客,也看了一些书,总觉得有一个坎跨不过去,所以对ThreadLoca ...

  5. 并发编程之ThreadLocal源码分析

    当访问共享的可变数据时,通常需要使用同步.一种避免同步的方式就是不共享数据,仅在单线程内部访问数据,就不需要同步.该技术称之为线程封闭. 当数据封装到线程内部,即使该数据不是线程安全的,也会实现自动线 ...

  6. 多线程高并发编程(7) -- Future源码分析

    一.概念 A Future计算的结果. 提供方法来检查计算是否完成,等待其完成,并检索计算结果. 结果只能在计算完成后使用方法get进行检索,如有必要,阻塞,直到准备就绪. 取消由cancel方法执行 ...

  7. 并发-ThreadLocal源码分析

    ThreadLocal源码分析 参考: http://www.cnblogs.com/dolphin0520/p/3920407.html https://www.cnblogs.com/coshah ...

  8. ThreadLocal源码分析-黄金分割数的使用

    前提 最近接触到的一个项目要兼容新老系统,最终采用了ThreadLocal(实际上用的是InheritableThreadLocal)用于在子线程获取父线程中共享的变量.问题是解决了,但是后来发现对T ...

  9. Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式

    在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概 ...

随机推荐

  1. hibernate-release-5.2.9.Final

    链接:https://pan.baidu.com/s/1UaoaaXb5C9gJp3Yu7sjUVQ 提取码:orgx

  2. 展开被 SpringBoot 玩的日子 《 四 》 Session 会话共享

    共享Session-spring-session-data-redis 分布式系统中,sessiong共享有很多的解决方案,其中托管到缓存中应该是最常用的方案之一. Spring Session官方说 ...

  3. python学习笔记(7)

    第七章 文件和数据格式化 文件的使用 文件是数据的抽象和集合 文件是存储在辅助存储器上的数据序列 文件是数据存储的一种形式 文件展现形态:文本文件和二进制文件 文本文件 由单一特定编码组成的文件,如U ...

  4. Rest API 操作List Items

    获取所有的List Itemsfunction getItems(url) { $.ajax({ url: url, type: "GET", headers: { "a ...

  5. 初学angular项目中遇到的一些问题

    1.当angular渲染完成后操作DOM树方法 //当数据渲染完毕 ngApp.directive('repeatFinish', function () {            return {  ...

  6. mysql_Navicat数据库破解

    Navicat Premium 12.1.16.0安装与激活 Navicat Premium 12是一套数据库开发管理工具,支持连接 MySQL.Oracle等多种数据库,可以快速轻松地创建.管理和维 ...

  7. 201771010118 马昕璐《面向对象程序设计java》第十周学习总结

    第一部分:理论知识学习部分 泛型:也称参数化类型(parameterized type)就是在定义类.接口和方法时,通过类型参数 指示将要处理的对象类型. 泛型程序设计(Generic program ...

  8. 你不知道的JS之作用域和闭包(一)什么是作用域?

    原文:你不知道的js系列 什么是作用域(Scope)? 作用域 是这样一组规则——它定义了如何存放变量,以及程序如何找到之前定义的变量. 编译器原理 JavaScript 通常被归类为动态语言或者解释 ...

  9. Spring Cloud 组件 —— feign

    feign 作为一个声明式的 Http Client 开源项目.在微服务领域,相比于传统的 apache httpclient 与在 spring 中较为活跃的 RestTemplate 更面向服务化 ...

  10. mysql数据库表的修改及删除

    一.对数据表的修改 1.重命名一张表: RENAME TABLE 原名 TO 新名字; ALTER TABLE 原名 RENAME 新名; ALTER TABLE 原名 RENAME TO 新名; 2 ...