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的使用及原理浅析的更多相关文章

  1. java高并发之锁的使用以及原理浅析

    锁像synchronized同步块一样,是一种线程同步机制.让自Java 5开始,java.util.concurrent.locks包提供了另一种方式实现线程同步机制——Lock.那么问题来了既然都 ...

  2. 0.Java并发包系列开篇

    在我们想要谈论Java并发包(java.util.concurrent)的时候,这是一个头疼的问题,却又是每个Java工程师不得不掌握的一项技能.一直以来都想写一个Java并发包系列,无奈迟迟没有动手 ...

  3. [Java并发包学习七]解密ThreadLocal

    概述 相信读者在网上也看了非常多关于ThreadLocal的资料,非常多博客都这样说:ThreadLocal为解决多线程程序的并发问题提供了一种新的思路:ThreadLocal的目的是为了解决多线程訪 ...

  4. java并发编程学习: ThreadLocal使用及原理

    多线程应用中,如果希望一个变量隔离在某个线程内,即:该变量只能由某个线程本身可见,其它线程无法访问,那么ThreadLocal可以很方便的帮你做到这一点. 先来看一下示例: package yjmyz ...

  5. Java 并发包中的读写锁及其实现分析

    1. 前言 在Java并发包中常用的锁(如:ReentrantLock),基本上都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时 刻可以允许多个读线程访问,但是在写线程访问时,所有 ...

  6. Java并发包同步工具之Exchanger

    前言 承接上文Java并发包同步工具之Phaser,讲述了同步工具Phaser之后,搬家博客到博客园了,接着未完成的Java并发包源码探索,接下来是Java并发包提供的最后一个同步工具Exchange ...

  7. Java并发包源码学习系列:ReentrantReadWriteLock读写锁解析

    目录 ReadWriteLock读写锁概述 读写锁案例 ReentrantReadWriteLock架构总览 Sync重要字段及内部类表示 写锁的获取 void lock() boolean writ ...

  8. Java多线程10:ThreadLocal的作用及使用

    ThreadLocal的作用 从上一篇对于ThreadLocal的分析来看,可以得出结论:ThreadLocal不是用来解决共享对象的多线程访问问题的,通过ThreadLocal的set()方法设置到 ...

  9. Java并发包源码学习之AQS框架(四)AbstractQueuedSynchronizer源码分析

    经过前面几篇文章的铺垫,今天我们终于要看看AQS的庐山真面目了,建议第一次看AbstractQueuedSynchronizer 类源码的朋友可以先看下我前面几篇文章: <Java并发包源码学习 ...

  10. Java并发包源码学习之AQS框架(三)LockSupport和interrupt

    接着上一篇文章今天我们来介绍下LockSupport和Java中线程的中断(interrupt). 其实除了LockSupport,Java之初就有Object对象的wait和notify方法可以实现 ...

随机推荐

  1. Qt5 escape spaces in path

    There are two possible ways. You can either use escaped quotes (inserting the string between quotes) ...

  2. 基于jenkins自动打包并部署Tomcat环境

    传统网站部署的流程 在运维过程中,网站部署是运维的工作之一.传统的网站部署的流程大致分为:需求分析->原型设计->开发代码->提交代码->内网部署->内网测试->确 ...

  3. 【手把手教你】win10 虚拟机 VMware Workstation Pro 15下安装redhat 8.0

    安装redhat8.0 和安装Ubuntu 差别不大 可以参考上篇文章:https://www.cnblogs.com/zero-vic/p/11593683.html 但是redhat  8.1 b ...

  4. Comparable 接口与Comparator的使用的对比

    package com.yhqtv.java; import org.junit.Test; import java.util.Arrays; import java.util.Comparator; ...

  5. Linux 用户管理和提权

    Linux ⽀持多个⼈使⽤同⼀个⽤户登录系统, Windows 在修改组策略的情况下,也可以多个⼈使⽤同⼀个⽤户登录 远程连接Linux的⽅式:SSH协议 远程连接Windows的⽅式:RDP协议 安 ...

  6. 【JAVA基础】06 面向对象

    1. 面向对象思想概述 面向过程思想概述 第一步 第二步 面向对象思想概述 找对象(第一步,第二步) 举例 买煎饼果子 洗衣服 面向对象思想特点 是一种更符合我们思想习惯的思想 可以将复杂的事情简单化 ...

  7. 【Linux常见命令】cp命令

    cp - copy files and directories 拷贝文件或目标文件夹,默认不能直接拷贝目录,通过-r参数设置递归复制目录 copy 语法: cp [OPTION]... [-T] SO ...

  8. P1459 三值的排序 Sorting a Three-Valued

    题目描述 排序是一种很频繁的计算任务.现在考虑最多只有三值的排序问题.一个实际的例子是,当我们给某项竞赛的优胜者按金银铜牌排序的时候.在这个任务中可能的值只有三种1,2和3.我们用交换的方法把他排成升 ...

  9. 集训模拟赛-1-T2

    好了不要在铺垫了直接整吧就 题目拿来!!!!!!! 倒水 (water) (256MB,1s) [问题描述] 你有一个水桶(记为 0),两个杯子(记为 1,2).水桶中的水量无限,容量也无限.1 号杯 ...

  10. MySQL 入门(2):索引

    摘要 在这篇文章中,我会先介绍一下什么是索引,索引有什么作用. 之后会介绍一下索引的数据结构是什么样的,有什么优点,又会带来什么样的问题. 在分析完数据结构后,我们可以根据这个数据结构,研究索引的用法 ...