Java并发包2--ThreadLocal的使用及原理浅析
ThreadLocal 是本地线程变量,是一个以ThreadLocal对象为key,任意对象为value的存储结构。
一、使用案例
1.定义线程类MyThread,代码如下:
public class MyThread extends Thread{
private User user;
public MyThread(User user){
this.user = user;
}
public void run() {
System.out.println("线程:"+Thread.currentThread().getName()+"设置ThreadLocal的user="+user.getUserName());
ThreadLocalTest.LOCAL.set(user);
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
User user = ThreadLocalTest.LOCAL.get();
System.out.println("线程:"+Thread.currentThread().getName()+"从ThreadLocal获取的user="+user.getUserName());
}
}
2.测试方法Main方法
public class ThreadLocalTest {
//全局ThraedLocal变量
public static ThreadLocal<User> LOCAL = new ThreadLocal<User>();
public static void main(String[] args){
User user1 = new User();
user1.setUserName("Jack");
User user2 = new User();
user2.setUserName("Bob");
//定义两个线程变量
Thread t1 = new MyThread(user1);
Thread t2 = new MyThread(user2);
t1.start();
t2.start();
//从ThreadLocal变量中获取数据
User user = LOCAL.get();
System.out.println(user==null);//当前线程为Main线程,而Main线程没有设置过TheadLocal的值,所以获取不到
LOCAL.set(user2);
System.out.println(LOCAL.get().getUserName());//从Main线程设置ThreadLocal,则可以获取
}
}
定义两个线程,线程的run方法执行了ThreadLocal变量的set操作,然后再执行get操作,可以获取到本线程设置的值
而直接从Main线程中执行ThreadLocal的get方法,返回的数据为null,只有在自己的线程中执行了set操作,才可以获取到值,
上例的执行结果如下:
true
Bob
线程:Thread-0设置ThreadLocal的user=Jack
线程:Thread-1设置ThreadLocal的user=Bob
线程:Thread-0从ThreadLocal获取的user=Jack
线程:Thread-1从ThreadLocal获取的user=Bob
二、源码解析
ThreadLocal主要有三个方法,
set (T value) 给ThreadLocal变量设置数据,ThreadLocal会存储当前线程存储的值
get ( ) 返回ThreadLocal当前线程设置的值
remove() 删除当前线程设置的ThreadLocal的值
2.1、set方法解析
源码如下:
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果当前线程的ThreadLocalMap存在,则直接将当前的value设置到map中,map的key就是当前的ThreadLocal对象
map.set(this, value);
else
// 如果当前线程的ThreadLocalMap不存在,则先创建ThreadLocalMap,则进行赋值
createMap(t, value);
}
首先是通过getMap (Thread t) 方法获取当前线程的ThreadLocalMap对象,代码如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
每个线程Thread对象内部都有一个ThreadLocalMap对象threadLocals
ThreadLocal.ThreadLocalMap threadLocals = null; //Thread类中持有一个ThreadLocalMap对象
如果当前线程的ThreadLocalMap不存在,则只需createMap方法初始化,代码如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
直接初始化一个ThreadLocalMap,然后赋值给Thread的threadLocals对象
可以发现ThreadLocal的set方法逻辑其实很简单,就是获取一个ThreadLocalMap对象,然后将需要set的值保存在ThreadLocalMap中
ThreadLocalMap是ThreadLocal的一个内部类,如下:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocal的set方法实际是执行了ThreadLocalMap的set方法
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();
}
可以发现是ThreadLocalMap是将当前的ThreadLocal当做一个key,需要存储的对象为value,存储在ThreadLocalMap内部的Entry数组中。
ThreadLocalMap也会存储hash冲突的问题,只是解决冲突的方式比较简单,指定尝试获取下一个位置用于存放,直到能够放入位置(ThreadLocal不建议一个线程有太多ThreadLocal,所以没必要花费大力气解决冲突问题)
同理既然ThreadLocal的set方法是执行了ThreadLocalMap的set方法,那么可以猜想ThreadLocal的get方法也是执行了ThreadLocalMap的get方法。
如下:
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();
}
ThreadLocalMap的getEntry方法如下:
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);
}
为什么ThreadLocalMap是采用数组存储的呢?因为ThreadLocalMap是和Thread绑定的,一个Thread只有一个ThreadLocalMap对象,但是每个Thread可以存储多个ThreadLocal对象
所以ThreadLocalMap中的数组就是存储了多个ThreadLocal对象,数组的下标是通过 threadLocal对象的hashCode和数组长度进行取模算法得到的数组下标值。
分析到这里可以总结出ThreadLocal的原理:
每个线程Thread 内部有一个 ThreadLocalMap对象,每个ThreadLocalMap 存储了多个以 ThreadLocal对象为key,存储的数据为value的值。ThreadLocalMap内部采用数组存储ThreadLocal的值
通过ThreadLocal的hashCode和数组长度进行取模算法得到数组的下标位置,在指定的位置存储ThreadLocal的值。
分析到这里涉及到的对象 包含了 Thread、ThreadLocal 和 ThreadLocalMap,这三者的关系为:
Thread 内部有一个 ThreadLocalMap对象实例
ThreadLocalMap 是 ThreadLocal的一个内部类
ThreadLocalMap 存储的是以 ThreadLocal 为key,ThreadLocal的值为value 的结构
光靠文字不好理解,使用图形的话很更直观,以上面的代码为例,三者关系如下图示:

每个Thread中的ThreadLocalMap可以存储多个ThreadLocal的值,但是同一个ThreadLocal变量在同一个Thread中只能保存一个值,后面set的值会把前面set的值覆盖。
如果需要保存多个值就需要使用多个ThreadLocal来存储
三、ThreadLocal内存泄露浅析
ThreadLocal很好的解决了线程之间数据隔离,但是大量的ThreadLocal在大量的线程中就有了空间的问题,内存中存储的ThreadLocal值的个数等于 ThreadLocal变量个数 * 线程个数,显然随着线程的变多,ThreadLocal占据的空间也是不容小觑的。
所以使用ThreadLocal的时候就需要做到不用对时候及时回收,防止没有被回收导致内存泄露问题。如下图示:

栈中有一个ThreadLocal的变量和一个Thread的变量 分别强引用堆中对应的实例,堆中的ThreadLocalMap实例也是被Thread变量引用
ThreadLocalMap又持有 Entry实例的强引用,而Entry分别持有key的弱引用,value的强引用,key是 ThreadLocal实例,value是实际存的数据
上图中实现表示强引用,虚线表示弱引用。
当ThreadLocal 不用的时候,ThreadLocal变量会被回收,此时ThreadLocal 变量和ThreadLocal实例的强引用断开,但是此时ThreadLocal实例还不能够被回收,引用还有 Entry的key对ThreadLocal实例持有着弱引用
所以当下一次执行GC的时候,ThreadLocal实例就会被回收,因为GC的时候会直接回收弱引用实例。此时Entry的key已经被回收了,所以Entry就变成了<null, value >的结构,这就会导致这个value是无法被获取了。
如果这个Thread 一直活跃,那么Thread 强引用 ThreadLocalMap;ThreadLocalMap强引用Entry,Entry强引用value就都不能被回收,所以一旦ThreadLocal被回收,而Thread还继续工作的话,就会导致value无法被访问,
从而就造成了 内存泄露问题。
既然Entry的 key是弱引用,那么为什么value不是弱引用呢?
如果value是弱引用的话,那么在GC的时候就会被回收,而ThreadLocal除了有弱引用,还有一个强引用,所以在GC的时候ThreadLocal并不会被强制回收,而value会被回收,就会出现通过 key无法获取到数据的情况,
如果GC频繁,那么ThreadLocal的get方法就会频繁获取不到数据,那么这样的ThreadLocal还有什么意义呢?
所以ThreadLocalMap的实现仅仅将key作为了弱引用,value不会出现弱引用,这样虽然有内存泄露问题,但是至少可以保证只要 ThreadLocal 还在存活状态,就可以获取到value,显然是高可用的。
那么既然有内存泄露问题,ThreadLocal就不管了么?显然不可能!
ThreadLocal的get方法、set方法、remove方法的内部都做了判断,如果存储key=null都情况,就将value设置为null,一旦value设置为null之后,那么就将value和实际的数据之前的强引用断开了,那么数据在GC的时候就会被回收
但是这样的设计就导致ThreadLocal 如果再也不会执行get、set 或 remove方法了,那么还是会存在内存泄露问题,所以在使用的时候需要养成好习惯,当不用ThreadLocal的时候,手动执行remove方法回收数据。
Java并发包2--ThreadLocal的使用及原理浅析的更多相关文章
- java高并发之锁的使用以及原理浅析
锁像synchronized同步块一样,是一种线程同步机制.让自Java 5开始,java.util.concurrent.locks包提供了另一种方式实现线程同步机制——Lock.那么问题来了既然都 ...
- 0.Java并发包系列开篇
在我们想要谈论Java并发包(java.util.concurrent)的时候,这是一个头疼的问题,却又是每个Java工程师不得不掌握的一项技能.一直以来都想写一个Java并发包系列,无奈迟迟没有动手 ...
- [Java并发包学习七]解密ThreadLocal
概述 相信读者在网上也看了非常多关于ThreadLocal的资料,非常多博客都这样说:ThreadLocal为解决多线程程序的并发问题提供了一种新的思路:ThreadLocal的目的是为了解决多线程訪 ...
- java并发编程学习: ThreadLocal使用及原理
多线程应用中,如果希望一个变量隔离在某个线程内,即:该变量只能由某个线程本身可见,其它线程无法访问,那么ThreadLocal可以很方便的帮你做到这一点. 先来看一下示例: package yjmyz ...
- Java 并发包中的读写锁及其实现分析
1. 前言 在Java并发包中常用的锁(如:ReentrantLock),基本上都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时 刻可以允许多个读线程访问,但是在写线程访问时,所有 ...
- Java并发包同步工具之Exchanger
前言 承接上文Java并发包同步工具之Phaser,讲述了同步工具Phaser之后,搬家博客到博客园了,接着未完成的Java并发包源码探索,接下来是Java并发包提供的最后一个同步工具Exchange ...
- Java并发包源码学习系列:ReentrantReadWriteLock读写锁解析
目录 ReadWriteLock读写锁概述 读写锁案例 ReentrantReadWriteLock架构总览 Sync重要字段及内部类表示 写锁的获取 void lock() boolean writ ...
- Java多线程10:ThreadLocal的作用及使用
ThreadLocal的作用 从上一篇对于ThreadLocal的分析来看,可以得出结论:ThreadLocal不是用来解决共享对象的多线程访问问题的,通过ThreadLocal的set()方法设置到 ...
- Java并发包源码学习之AQS框架(四)AbstractQueuedSynchronizer源码分析
经过前面几篇文章的铺垫,今天我们终于要看看AQS的庐山真面目了,建议第一次看AbstractQueuedSynchronizer 类源码的朋友可以先看下我前面几篇文章: <Java并发包源码学习 ...
- Java并发包源码学习之AQS框架(三)LockSupport和interrupt
接着上一篇文章今天我们来介绍下LockSupport和Java中线程的中断(interrupt). 其实除了LockSupport,Java之初就有Object对象的wait和notify方法可以实现 ...
随机推荐
- Qt5 escape spaces in path
There are two possible ways. You can either use escaped quotes (inserting the string between quotes) ...
- 基于jenkins自动打包并部署Tomcat环境
传统网站部署的流程 在运维过程中,网站部署是运维的工作之一.传统的网站部署的流程大致分为:需求分析->原型设计->开发代码->提交代码->内网部署->内网测试->确 ...
- 【手把手教你】win10 虚拟机 VMware Workstation Pro 15下安装redhat 8.0
安装redhat8.0 和安装Ubuntu 差别不大 可以参考上篇文章:https://www.cnblogs.com/zero-vic/p/11593683.html 但是redhat 8.1 b ...
- Comparable 接口与Comparator的使用的对比
package com.yhqtv.java; import org.junit.Test; import java.util.Arrays; import java.util.Comparator; ...
- Linux 用户管理和提权
Linux ⽀持多个⼈使⽤同⼀个⽤户登录系统, Windows 在修改组策略的情况下,也可以多个⼈使⽤同⼀个⽤户登录 远程连接Linux的⽅式:SSH协议 远程连接Windows的⽅式:RDP协议 安 ...
- 【JAVA基础】06 面向对象
1. 面向对象思想概述 面向过程思想概述 第一步 第二步 面向对象思想概述 找对象(第一步,第二步) 举例 买煎饼果子 洗衣服 面向对象思想特点 是一种更符合我们思想习惯的思想 可以将复杂的事情简单化 ...
- 【Linux常见命令】cp命令
cp - copy files and directories 拷贝文件或目标文件夹,默认不能直接拷贝目录,通过-r参数设置递归复制目录 copy 语法: cp [OPTION]... [-T] SO ...
- P1459 三值的排序 Sorting a Three-Valued
题目描述 排序是一种很频繁的计算任务.现在考虑最多只有三值的排序问题.一个实际的例子是,当我们给某项竞赛的优胜者按金银铜牌排序的时候.在这个任务中可能的值只有三种1,2和3.我们用交换的方法把他排成升 ...
- 集训模拟赛-1-T2
好了不要在铺垫了直接整吧就 题目拿来!!!!!!! 倒水 (water) (256MB,1s) [问题描述] 你有一个水桶(记为 0),两个杯子(记为 1,2).水桶中的水量无限,容量也无限.1 号杯 ...
- MySQL 入门(2):索引
摘要 在这篇文章中,我会先介绍一下什么是索引,索引有什么作用. 之后会介绍一下索引的数据结构是什么样的,有什么优点,又会带来什么样的问题. 在分析完数据结构后,我们可以根据这个数据结构,研究索引的用法 ...