深入了解ThreadLocal

/ 技术 / 2 条站内评论 / 616浏览

ThreadLocal类为每一个线程都维护了自己独有的变量拷贝。每个线程都拥有了自己独立的一个变量,竞争条件被彻底消除了,那就没有任何必要对这些线程进行同步,它们也能最大限度的由CPU调度,并发执行。并且由于每个线程在访问该变量时,读取和修改的,都是自己独有的那一份变量拷贝,变量被彻底封闭在每个访问的线程中,并发错误出现的可能也完全消除了

适用场景

如上文所述,ThreadLocal 适用于如下两种场景

对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLoca 可以以非常方便的形式满足该需求。

对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。ThreadLocal 使得代码耦合度更低,且实现更优雅。

不恰当的理解

写这篇文章的一个原因在于,网上很多博客关于 ThreadLocal 的适用场景以及解决的问题,描述的并不清楚,甚至是错的。下面是常见的对于 ThreadLocal的介绍

ThreadLocal为解决多线程程序的并发问题提供了一种新的思路
ThreadLocal的目的是为了解决多线程访问资源时的共享问题

还有很多文章在对比 ThreadLocal 与 synchronize 的异同。既然是作比较,那应该是认为这两者解决相同或类似的问题。

我们先代码实现下Thread间同步的情况

什么是同步?

  java允许多线程并发控制,当多个线程同时操作一个可共享的资源变量时(如数据的增删改查)

  将会导致数据不准确,相互之间产生冲突,因此加入同步锁以避免在该线程没有完成操作之前,被其他线程的调用, 

  从而保证了该变量的唯一性和准确性。

以下代码实现开两个线程对bank类的金额变量叠加

同步

Bank类

public class Bank {

private int account = 100;


public int getAccount() {
return account;
}

/**
* 用同步方法实现
*
* @param money
*/
public synchronized void save(int money) {
account += money;
}
}

主代码



/**
* 线程同步的运用
*
* @author sani
*
*/
public class SynchronizedThread {


class NewThread implements Runnable {
private Bank bank;

public NewThread(Bank bank) {
this.bank = bank;
}

@Override
public void run() {
for (int i = 0; i < 10; i++) {
// bank.save1(10);
bank.save(10);
System.out.println(Thread.currentThread().getName()+" : "+ i + "账户余额为:" + bank.getAccount()+" hashcode: "+bank.hashCode());
}
}

}

/**
* 建立线程,调用内部类
*/
public void useThread() {
Bank bank = new Bank();
NewThread new_thread = new NewThread(bank);
System.out.println("线程1");
Thread thread1 = new Thread(new_thread);
thread1.start();
System.out.println("线程2");
Thread thread2 = new Thread(new_thread);
thread2.start();
}

public static void main(String[] args) {
SynchronizedThread st = new SynchronizedThread();
st.useThread();
}

}

输出结果


我们对tBank对象方法进行加锁,在对同一个对象多线程操作,多线程间会同步。

我们再试试不同对象

输出结果


因为多线程间加锁的不是同一个对象,因此多线程间不会同步。

我们再看看在同一个对象,使用ThreadLocal作为金额,看多线程间能不能共享。

ThreadLocal金额变量

public class Bank {

// private int account = 100;

//使用ThreadLocal类管理共享变量account
private static ThreadLocal<Integer> account = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue(){
return 100;
}
};

public int getAccount() {
return account.get();
}

/**
* 用同步方法实现
*
* @param money
*/
public synchronized void save(int money) {
Integer integer = account.get();
account.set( integer+= money);
}
}

输出结果

实验证明:

ThreadLocal 并不解决多线程 共享 变量的问题。既然变量不共享,那就更谈不上同步的问题。

我们这里再科普下

Java线程安全和非线程安全

https://blog.csdn.net/xiao__gui/article/details/8934832

https://www.cnblogs.com/chy2055/p/5175969.html

总结

  若多个线程同时修改同一个对象的成员变量,很容易就会出现错误,我们称之为线程不安全。(该类的这个方法是线程不安全的。若要线程安全,用synchronized关键字修饰即可)

合理的理解

ThreadLoal 变量,它的基本原理是,同一个 ThreadLocal 所包含的对象(对ThreadLocal< String >而言即为 String 类型变量),在不同的 Thread 中有不同的副本(实际是不同的实例,后文会详细阐述)。这里有几点需要注意

那 ThreadLocal 到底解决了什么问题,又适用于什么样的场景?

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

核心意思是

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

1.定义private 作用: private为了安全,同时使用private修饰类的一个属性是一个普遍的问题。

2.static修饰符 作用:

1.ThreadLocal要使用static的 ,在其他地方可以直接用get 和 set方法,方便。

2.static 防止无意义多实例

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。让每个线程都保存一份变量的副本,该副本只会被隶属的线程操作,这也就不存在线程安全问题了。

ThreadLocal和Thread的联系

在上面提到了数据副本,那么线程如何保存该副本的呢?其实,Thread类中有一个ThreadLocalMap类型的变量threadLocals

在ThreadLocal的set方法中,可以看到获取ThreadLocalMap的方法。



我们发现,threadlocals是Thread线程类的一个成员变量。


ThreadLocalMap是ThreadLocal的一个内部类,其作用相当于一个HashMap,用于保存隶属于该线程的变量副本。下面需要考虑一个问题:ThreadLocalMap的key和value该如何设计呢?

ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal 实例本身,value 是真正需要存储的 Object

也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

弱引用解释:https://www.cnblogs.com/absfree/p/5555687.html

原理图

ThreadLocal类中一共有4个方法

下面我们分析这四个方法的底层实现

set操作


1.首先先获取当前线程,从当先线程维护的ThreadLocalMap

2.判断map是否为null,非空则set,注意这里。put的key是this!也就是当前ThreadLocal本身,value就是我们传进去的value。

3.获取到的 ThreadLocalMap 为 null,则先创建该 ThreadLocalMap 对象。入参是当前线程对象(即代码中的 this),以及我们要设置的value。

 

get操作

首先获取到当前线程,从Thread中获取到维护的ThreadLocalMap,key为当前ThreadLocal(即代码中的 this)

initialValue

设置初始值方法如下


首先,通过initialValue()方法获取初始值,且默认返回 null。

然后拿到该线程对应的 ThreadLocalMap 对象,若该对象不为 null,则直接将该 ThreadLocal 对象(即代码中的 this)与对应实例初始值的映射添加进该线程的 ThreadLocalMap中。若为 null,则先创建该 ThreadLocalMap 对象再将映射添加其中。

remove操作


通过当前ThreadLocal获取到Thread线程维护的ThreadLocalMap,通过当前ThreadLocal(即代码中的 this)作为key从map中移除。

子线程继承父线程的值?

InheritableThreadLocal

InheritableThreadLocal是ThreadLocal的子类。该类扩展了 ThreadLocal,为子线程提供从父线程那里继承的值:在创建子线程时,子线程会接收所有可继承的线程局部变量的初始值,以获得父线程所具有的值。

子线程

如果线程A创建了线程B,那么B就是A的子线程。

示例代码

用法和ThreadLocal几乎一样,但是效果不一样。

ThreadLocal类

import java.util.Random;

/**
* @Auther: shouliang.wang
* @Date: 2018/7/9 11:01
* @Description:
*/
public class ThreadLocalBean {
private static ThreadLocal<Integer> threadLocal=new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
//生成随机数
return new Random().nextInt(1000);
}
};

public static int get(){
return threadLocal.get();
}

public static void set(Integer integer){
threadLocal.set(integer);
}
}

InheritableThreadLocal类

import java.util.Random;

/**
* @Auther: shouliang.wang
* @Date: 2018/7/9 11:01
* @Description:
*/
public class InheritableThreadLocalBean {
private static InheritableThreadLocal<Integer> threadLocal = new InheritableThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return new Random().nextInt(1000);
}
};
public static int get() {
return threadLocal.get();
}
public static void set(Integer integer) {
threadLocal.set(integer);
}
}


Thread类

/**
* @Auther: shouliang.wang
* @Date: 2018/7/9 11:06
* @Description:
*/
public class MyThread extends Thread{
public MyThread(String name) {
super(name);
}

@Override
public void run() {
super.run();
System.out.printf("%s ThreadLocal 取数据:%d\n", Thread.currentThread().getName(), ThreadLocalBean.get());
System.out.printf("%s InheritableThreadLocal 取数据:%d\n", Thread.currentThread().getName(),
InheritableThreadLocalBean.get());
}

public static void main(String[] args) {
System.out.printf("%s ThreadLocal 取数据:%d\n", Thread.currentThread().getName(), ThreadLocalBean.get());
System.out.printf("%s InheritableThreadLocal 取数据:%d\n", Thread.currentThread().getName(),
InheritableThreadLocalBean.get());
MyThread t1 = new MyThread("Child");
t1.start();
}
}

输出结果


结果可以看到,看到ThreadLocal里的值,子线程里不能获得;InheritableThreadLocal里的值,子线程可以获得。


在Thread的init()方法中可以看到

if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);


在子线程初始化时,会调用这个方法,然后以父线程的map做为参数,创建一个map,这样,子线程就能访问到父线程的数据了

官方例子

slf4j日志

在MDC里面有个MDCAdapter接口存储日志变量数据,在实现类BasicMDCAdapter中可以看到使用了InheritableThreadLocal。

我们来看看slf4j底层源码是怎么实现的

我们再看看mdcAdaoter的实现类BasicMDCAdapter源码实现

我们发现MDC是将一个日志数据存入map放在InheritableThreadLocal中。

再看一下MDC的get方法

首先从InheritableThreadLocal取出map实例,再通过key得到对应的value。

内存泄露


ThreadLocal为什么会内存泄漏

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 内存泄露的实例分析)。
  • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

防止内存泄漏

对于已经不再被使用且已被回收的 ThreadLocal 对象,它在每个线程内对应的实例由于被线程的 ThreadLocalMap 的 Entry 强引用,无法被回收,可能会造成内存泄漏。

针对该问题,ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏。


为什么使用弱引用

从表面上看内存泄漏的根源在于使用了弱引用。网上的文章大多着重分析ThreadLocal使用了弱引用会导致内存泄漏,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?

我们先来看看官方文档的说法:

To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了应对非常大和长时间的用途,哈希表使用弱引用的 key。

下面我们分两种情况讨论:

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

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

ThreadLocal 最佳实践

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

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

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

总结

  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
  • ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景


代码

点我下载

参考以下资料

Java进阶(七)正确理解Thread Local的原理与适用场景 http://www.jasongj.com/java/threadlocal/

深入分析 ThreadLocal 内存泄漏问题 http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/

ThreadLocal案例分析 https://www.jianshu.com/p/3ab5f9145ca2

ThreadLocal、ThreadLocalMap弱引用key https://blog.csdn.net/enetor1/article/details/39006727

聊一聊ThreadLocal https://blog.csdn.net/u013256816/article/details/51776846

理解Java中的弱引用(Weak Reference) https://www.cnblogs.com/absfree/p/5555687.html



  1. san

    直接从IDEA复制,卡是指什么呢?我用着不觉得啊

    回复
  2. 写的很好呀,问一下,代码块的背景颜色是怎么弄得呀,还有就是你的网站很卡很卡

    回复
召唤蕾姆
琼ICP备18000156号

鄂公网安备 42011502000211号