ThreadLocal

简介

ThreadLocal提供局部线程变量,这个变量与普通的变量不同,每个线程在访问ThreadLocal实例的时候,(通过get或者set方法)都有自己的、独立初始化变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(用户ID或者事务ID)与线程关联起来。

每个线程都有自己的、独立初始化变量副本,意味着避免了线程安全问题

使用

  • ThreadLocal.withInitial():创建ThreadLocal并设置初始值
  • initialValue():返回局部线程变量的当前线程的初始化值
  • get():返回局部线程变量在当前线程副本的值
  • set():设置局部线程变量在当前线程副本的值
  • remove():删除线程的局部线程变量副本

案例

class House {
int saleCount;
public synchronized void saleHouse(int size) {
this.saleCount += size;
}
} public class SaleHouse {
public static void main(String[] args) {
House house = new House();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5) + 1;
System.out.println(size);
house.saleHouse(size);
}, String.valueOf(i)).start();
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Total: " + house.saleCount);
}
}

现在需要记录每个销售员的业绩,可以使用ThreaLocal设置每个线程变量进行存储

class House {
int saleCount;
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
public synchronized void saleHouse() {
this.saleCount++;
}
public void saleVolumeByThreadLocal() {
saleVolume.set(saleVolume.get() + 1);
}
} public class SaleHouse {
public static void main(String[] args) {
House house = new House();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5) + 1;
for (int i1 = 0; i1 < size; i1++) {
house.saleHouse();
house.saleVolumeByThreadLocal();
}
System.out.println(Thread.currentThread().getName() + " sales " + house.saleVolume.get());
}, String.valueOf(i)).start();
}
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Total: " + house.saleCount);
}
}
  • 由于是每个线程独有的一个副本变量,因此是绝对线程安全的,不需要将方法变为同步方法

清除局部变量

必须回收自定义的ThreadLocal变量,尤其是在线程池的场景下,线程经常会复用,如果不清理则会造成后续逻辑业务和内存泄漏问题,所以要在代码中使用try-catch进行回收。

ThreadLocal源码解读

Thread ThreadGroup ThreadLocal三者的区别?
  • Thread中含有ThreadLocalGroup的属性,而ThreadLocalGroup是ThreadLocal的静态内部类
  • ThreadLocalGroup中有一个继承了弱引用WeakReference的Entry内部类
  • Entry维护了一个ky键值对,键为当前线程的ThreadLocal,值为任意对象

    static class ThreadLocalMap {

        /**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
}

原话是:JVM内部维护了一个线程版的Map<ThreadLocal, value>(通过ThreadLocal对象的set方法,把ThreadLocal对象自己当作key放入到ThreadMap中),每个线程要用到的时候,用当前线程去自己的map里面以相应的ThreadLocal取,通过这样每个线程都拥有了独立的变量。

private T get(Thread t) {
ThreadLocalMap map = getMap(t); // 首先获取当前线程的ThreadLocalMap
if (map != null) {
if (map == ThreadLocalMap.NOT_SUPPORTED) {
return initialValue();
} else {
ThreadLocalMap.Entry e = map.getEntry(this); // 再获取this(ThreadLocal)的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T) e.value;
return result;
}
}
}
return setInitialValue(t);
}

ThreadLocal实现数据隔离的原理

  • ThreaLocal只是一个外壳,真正起存储作用的是ThreadLocal的ThreadLocalMap这个内部类,ThreadLocal本身只是作为一个key好从ThreadLocalMap获取value

  • 每个Thread维护了一个ThreadLocalMap引用,ThreadLocalMap使用Entry进行的存储

  • ThreadLocal调用set方法时,就是往ThreadLocalMap设置值,key为ThreadLocal,值为传递来的对象

    正因为上述原理,ThreadLocal才能实现每个线程拥有一份副本,实现数据隔离

为什么ThreadLocal 的 ThreadLocalMap要使用弱引用

内存泄漏:不会被使用的对象和变量的内存不能被回收,就是内存泄漏

内存溢出:内存泄漏达到一定程度导致无法分配内存,即OOM,就会导致内存溢出

回答这个问题,首先要看一下Java的四种引用:

对于普通的对象,如果没有其他的引用关系,只要超过了引用的作用域范围或者显式地赋值为null,就会被垃圾回收器回收

强引用 Reference
  • 强引用是常见的普通对象引用,只要还有强引用指向一个对象,就表明对象还存活,垃圾回收器就不会碰这种对象
  • 只要把一个对象赋值给一个引用变量,这个引用变量就是强引用
  • 当一个对象被强引用变量引用,它就处于可达状态,垃圾回收器就不可能回收它
  • 即使该对象以后永远也用不到,JVM也不会回收,因此强引用是造成JAVA泄漏的主要原因之一
class MyObject {
@Override
protected void finalize() throws Throwable {
System.out.println("--- invoke finalize method!");
}
}
public class ReferenceDemo {
public static void main(String[] args) {
var object = new MyObject();
System.out.println("gc before:" + object);
object = null;
System.gc();
System.out.println("gc after:" + object);
}
}
gc before:com.hikaru.juc.threadLocal.MyObject@7cd84586
gc after:null
--- invoke finalize method!

finalize是在对象被不可撤销的清理之前执行的撤销操作

System.gc() 人工gc

SoftReference 软引用
  • 软引用是相对强引用弱化了一些的引用,可以让对象豁免一些垃圾收集
  • 当内存充足的时候它不会被回收
  • 当内存不足的时候就会被回收
  • 所以经常使用在对内存敏感的程序中,如在高速缓存中
weakReference 弱引用
  • 比软引用弱化的引用,只要gc就会被回收
虚引用

​ 下面回到最初的问题,为什么ThreadLocal的ThreadLocalMap的Entry要使用弱引用:

ThreadLocal<String> t1 = new ThreadLocal<>();
t1.set("hello");
t1.get();

​ 在创建ThreadLocal的时候,会有强引用变量指向ThreadLocal对象,并且ThreadLocalMap的Entity的key会以弱引用的方式同时指向该对象,当强引用的作用域过期则只有弱引用指向ThreaLocal,而如果不是使用弱引用则ThreaLocal对象就一直不能被垃圾回收从而导致内存泄漏问题。

ThreadLocal之清除脏Entity

问题分析:当我们为ThreadLocal赋值时,实际上就是为当前线程的ThreadLocalMap中添加Entry键值对,而Entry中的key是弱引用,当ThreadLocal外部强引用被赋值为null,那么系统GC的时候,根据可达性分析,没有任何链路能够到达这个ThreadLocal实例,如此一来,ThreadLocalMap就出现了key为null的Entity

​ 接下来如果线程迟迟不能结束(线程池复用线程),这些key的对象就会由于ThreadLocalMap的强引用链导致永远无法回收,造成内存泄漏。因此,弱引用不能保证内存泄漏问题

​ remove方法会寻找脏Entity,即key==null的Entity进行删除。

JUC(八)ThreadLocal的更多相关文章

  1. 三十八 ThreadLocal

    在多线程环境下,每个线程都有自己的数据.一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁. 但是局部变量也有问题,就是在函数调用的 ...

  2. java并发编程实践——王宝令(极客时间)学习笔记

    1.并发 分工:如何高效地拆解任务并分配给线程 同步:线程之间如何协作 互斥:保证同一时刻只允许一个线程访问共享资源 Fork/Join 框架就是一种分工模式,CountDownLatch 就是一种典 ...

  3. JUC源码分析-集合篇(八)DelayQueue

    JUC源码分析-集合篇(八)DelayQueue DelayQueue 是一个支持延时获取元素的无界阻塞队列.队列使用 PriorityQueue 来实现. 队列中的元素必须实现 Delayed 接口 ...

  4. 【JUC】JDK1.8源码分析之ConcurrentSkipListSet(八)

    一.前言 分析完了CopyOnWriteArraySet后,继续分析Set集合在JUC框架下的另一个集合,ConcurrentSkipListSet,ConcurrentSkipListSet一个基于 ...

  5. (三)juc高级特性——虚假唤醒 / Condition / 按序交替 / ReadWriteLock / 线程八锁

    8. 生产者消费者案例-虚假唤醒 参考下面生产者消费者案例: /* * 生产者和消费者案例 */ public class TestProductorAndConsumer { public stat ...

  6. 多线程(八)~ThreadLocal、InheritableThreadLocal的使用

    通过前面的学习,我们了解了在多线程+成员变量等情况下会出现线程安全的问题.那么解决线程安全问题除了使用synchronize关键字之外,还有另一种常用的解决思路,那就是使用ThreadLocal类,下 ...

  7. Java多线程系列八——volatile和ThreadLocal

    参考资料: http://ifeve.com/java-memory-model-4/ http://www.infoq.com/cn/articles/java-memory-model-1 htt ...

  8. 【Java多线程系列八】volatile和ThreadLocal

    1. volatile final class Singleton { private static Singleton instance = null; private Singleton() { ...

  9. (八)Filter&ThreadLocal实现处理事务

    ConnectionContext.java package com.aff.bookstore.web; import java.sql.Connection; public class Conne ...

  10. java并发编程工具类JUC第八篇:ConcurrentHashMap

    在之前的文章中已经为大家介绍了java并发编程的工具:BlockingQueue接口.ArrayBlockingQueue.DelayQueue.LinkedBlockingQueue.Priorit ...

随机推荐

  1. maya灯光导入houdini插件开发

    加入工作室时师兄给了两道测试题,由于第一道是完善师兄的一个houdini项目管理插件,我只是开发了一些小功能,所以不好意思拿出来. 第二道题就完全是由自己开发的一个小插件,功能是把maya里的灯光导入 ...

  2. FCC 高级算法题 收银机找零钱

    Exact Change 设计一个收银程序 checkCashRegister() ,其把购买价格(price)作为第一个参数 , 付款金额 (cash)作为第二个参数, 和收银机中零钱 (cid) ...

  3. axios实现无感刷新

    前言 最近在做需求的时候,涉及到登录token,产品提出一个问题:能不能让token过期时间长一点,我频繁的要去登录. 前端:后端,你能不能把token 过期时间设置的长一点. 后端:可以,但是那样做 ...

  4. C#获取本地所有打印机名称

    引用:using System.Drawing.Printing; 实现代码: List<string> PrintS = new List<string>(); foreac ...

  5. python求列表中n个最大或最小的值

    import heapq #y为结果列表,n为所求的n个值,x为来源列表 y=heapq.nsmallest(n,x) y=heapq.nlargest(n,x)

  6. Microsoft SQL Server,错误: 3023

    对数据库的备份.文件操作(如 ALTER DATABASE ADD FILE)以及加密更改必须序列化.请在当前备份或文件操作完成后重新发出该语句.ALTER DATABASE 语句失败. (Micro ...

  7. Python第3章 流程控制语句(第2次作业)

    实例01 判断输入的是不是黄蓉所说的数 ①使用内置的print()函数输出"今有物不知其数,三三数之剩二,五五数之剩三,七七数之剩二,问几何?",代码如下: ②使用input()函 ...

  8. Serverless 架构演进与实践

    Serverless 架构演进与实践 1. 介绍 Serverless 并不仅仅是一个概念,很多地方都已经有了它的影子和思想,本文将给大家介绍最近比较火的 Serverless. 首先放出官方对 Se ...

  9. C#开发微信

    C#开发微信门户及应用教程   C#开发微信门户及应用(1)--开始使用微信接口... 6 1.微信账号... 6 2.微信菜单定义... 7 3.接入微信的链接处理... 8 4.使用开发方式创建菜 ...

  10. 看图就会-网络攻击 (xss和csrf)

    最近发现好多东西整理过后就没啥印象,但是思维导图很好用,能取其精华去其糟粕