Java多线程——ThreadLocal类的原理和使用

摘要:本文主要学习了ThreadLocal类的原理和使用。

概述

是什么

ThreadLocal可以用来维护一个变量,提供了一个ThreadLocalMap内部类,用来对变量进行设置、获取、删除等操作,原理类似于集合的Map,在Thread类里也提供了一个ThreadLocalMap类型的变量。

在使用ThreadLocal维护变量时,实际上是通过ThreadLocalMap进行维护的,使用的是当前线程里的ThreadLocalMap对象,保存的key是ThreadLocal的实例本身。

ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

使用场景

在一个线程中,需要共享某个资源,希望不管是在哪个类中使用该资源,都能保证该资源都是同一个,只会被创建一次,这就需要使用TheadLocal来实现。当然,也可以是用饿汉式,或者懒汉式的线程安全方式实现。

使用static修饰

由于ThreadlocalMap中的key是this引用,this引用也就是ThreadLocal的实例对象,也就是说,即便是在同一个线程里,ThreadLocal实例对象如果不是同一个,那么通过get()方法得到的就不是同一个值。

所以,要想在同一个线程里,每次得到的值都是同一个,就必须使this指针指向同一个对象,这就解释了为什么Threadlocal都使用静态变量来保存。

但是需要注意的是,如果使用了static关键字,有可能导致形成内存泄漏所需要的条件。

常用方法

set()方法

设置当前线程对应的线程局部变量的值。先取出当前线程对应的ThreadLocalMap对象。如果存在将当前ThreadLocal对象作为key,传入的值作为value,放到map里。如果不存在则通过线程创建一个ThreadLocalMap对象。

 public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

get()方法

返回当前线程所对应的线程局部变量。和set类似,也是先取出当前线程对应的ThreadLocalMap对象。如果存在就直接从map取出ThreadLocal对应的value返回。如果不存在则调用setInitialValue()方法,该方法会创建一个ThreadLocalMap对象,并且调用initialValue()方法设置初始值并返回initialValue。

 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();
}

remove()方法

将当前线程局部变量的值删除,目的是为了减少内存的占用。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。需要注意的是,如果remove之后又调用了get()方法,会重新初始化一次,即再次调用initialValue()方法,除非在这之前调用set()方法设置过值。

 public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}

initialValue()方法

返回该线程局部变量的初始值。该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程首次调用get()方法或set()方法时才执行,并且仅执行一次。ThreadLocal中的缺省实现直接返回一个null。

 protected T initialValue() {
return null;
}

使用实例

使用static关键字修饰资源操作类里的ThreadLocal类型的成员变量

以下代码模拟了三个线程,每个线程分别表示一个User用户资源,每个用户分别进行了Login操作和Logout操作,代码如下:

 public class Demo {
public static void main(String[] args) {
// 模拟三个线程使用。
for (int i = 0; i < 3; i++) {
new Thread() {
@Override
public void run() {
LoginService.login(new UserHandler().getUser());
LogoutService.logout(new UserHandler().getUser());
}
}.start();
}
}
} // 业务操作类,模拟Login操作。
class LoginService {
public static void login(User user) {
System.out.println(Thread.currentThread().getName() + " User " + user.hashCode() + " login ...");
}
} // 业务操作类,模拟Logout操作。
class LogoutService {
public static void logout(User user) {
System.out.println(Thread.currentThread().getName() + " User " + user.hashCode() + " logout ...");
}
} // 资源操作类,提供获取资源的方法。
class UserHandler {
// 私有,其他类不能直接操作ThreadLocal类,避免内存泄露。
private static ThreadLocal<User> tl = new ThreadLocal<User>() { // 重写初始化方法,首次使用时调用。
protected User initialValue() {
return new User();
};
}; // 公有,获取资源的方法。
public User getUser() {
return tl.get();
}
} // 资源类,不允许除了资源操作类以外的类使用。
class User {
private String name; public String getName() {
return name;
} public void setName(String name) {
this.name = name;
}
}

运行结果如下:

 Thread-0 User 103821930 login ...
Thread-2 User 412303685 login ...
Thread-1 User 443387063 login ...
Thread-0 User 103821930 logout ...
Thread-2 User 412303685 logout ...
Thread-1 User 443387063 logout ...

结果说明:

可以发现,在一个线程里,进行操作的始终是一个用户资源类。

不使用static关键字修饰资源操作类里的ThreadLocal类型的成员变量

将资源操作类里的修饰ThreadLocal类型的成员变量的static关键字去掉,其他部分不变,部分代码如下:

 // 资源操作类,提供获取资源的方法。
class UserHandler {
// 私有,其他类不能直接操作ThreadLocal类,避免内存泄露。
private ThreadLocal<User> tl = new ThreadLocal<User>() { // 重写初始化方法,首次使用时调用。
protected User initialValue() {
return new User();
};
}; // 公有,获取资源的方法。
public User getUser() {
return tl.get();
}
}

运行结果如下:

 Thread-1 User 1127398600 login ...
Thread-0 User 1988907679 login ...
Thread-2 User 1532003980 login ...
Thread-2 User 1601112382 logout ...
Thread-1 User 1916516959 logout ...
Thread-0 User 1924384918 logout ...

结果说明:

因为没有使用static关键字修饰,导致set()方法设置资源对象的时候,key不是同一个,从而导致在一个线程里调用get()方法的时候得到了不同的资源对象。

内存泄漏

产生原因

1)ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在

如果ThreadLocal没有外部强引用,当发生垃圾回收时,这个ThreadLocal一定会被回收(弱引用的特点是不管当前内存空间足够与否,GC时都会被回收),这样就会导致ThreadLocalMap中出现key为null的Entry,外部将不能获取这些key为null的Entry的value,并且如果当前线程一直存活,那么就会存在一条由Thread到ThreaLocalMap,再到Entry里的value的强引用链,导致value对应的Object一直无法被回收,产生内存泄露。

查看源码会发现,ThreadLocal的get、set和remove方法都实现了对所有key为null的value的清除,但仍可能会发生内存泄露,因为可能使用了ThreadLocal的get或set方法后发生GC,此后不调用get、set或remove方法,为null的value就不会被清除。

2)线程结束了,ThreadLocal一直存在

用static修饰的ThreadLocal,导致ThreadLocal的生命周期和持有它的类一样长,意味着这个ThreadLocal不会被GC。这种情况下,如果不手动删除,Entry的key永远不为null,弱引用就失去了意义,理所当然的无法通过调用set(),get(),remove()方法清除value,如果当前线程结束了,就导致了Entry的内存泄漏。

解决办法

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

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

Java多线程——ThreadLocal类的原理和使用的更多相关文章

  1. Java多线程——ThreadLocal类

    一.概述   ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量).也许把它命名 ...

  2. java的ThreadLocal类的使用方法

    java的ThreadLocal类的使用方法,ThreadLocal是一个支持泛型的类,用在多线程中用于防止并发冲突问题. 比如以下的一个样例,就是用于线程添加1,可是相互不冲突 package co ...

  3. 深入研究java.lang.ThreadLocal类 (转)

    深入研究java.lang.ThreadLocal类     一.概述   ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是thr ...

  4. Java 多线程与并发【原理第二部分笔记】

    Java 多线程与并发[原理第二部分笔记] 什么是Java内存模型中的happens-before Java内存模型,即JMM,本身是一种抽象的概念,并不是真实存在的,他描述的是一组规则或者说是一种规 ...

  5. Java 多线程与并发【原理第一部分笔记】

    Java 多线程与并发[原理第一部分笔记] Synchronized synchronized的基本含义以及使用方式 在Java中线程安全问题的主要诱因就是存在共享数据(也称为临界资源)以及存在多条线 ...

  6. 多线程--ThreadLocal类

    一.ThreadLocal类简介--此类是在整个开发过程中至关重要的类,他主要是在开发过程中解决了核心资源和多线程并发访问的处理情况--在真正去了解ThreadLocal类作用的时候,我们可以先编写一 ...

  7. java多线程系类:JUC线程池:03之线程池原理(二)(转)

    概要 在前面一章"Java多线程系列--"JUC线程池"02之 线程池原理(一)"中介绍了线程池的数据结构,本章会通过分析线程池的源码,对线程池进行说明.内容包 ...

  8. java多线程系类:JUC线程池:02之线程池原理(一)

    在上一章"Java多线程系列--"JUC线程池"01之 线程池架构"中,我们了解了线程池的架构.线程池的实现类是ThreadPoolExecutor类.本章,我 ...

  9. [Java多线程]-ThreadLocal源码及原理的深入分析

    ThreadLocal<T>类:以空间换时间提供一种多线程更快捷访问变量的方式.这种方式不存在竞争,所以也不存在并发的安全性问题. //-------------------------- ...

随机推荐

  1. Javase之集合体系(2)之List及其子类ArrayList,LinkedList与Vector及其迭代器知识

    集合体系之List及其子类ArrayList,LinkedList与Vector及其迭代器知识 List(接口) 特点:有序(存储与取出顺序相同),可重复 List子类特点: ​ ArrayList: ...

  2. NDK简介

    一.NDK简介: C/C++的动态库.Dalvik通过JNI编程方式调用C/C++代码. NDK编程提高软件性能,加密保护APK文件 ndk-build        NDK编译生成脚本 Java编译 ...

  3. 究竟是什么毁了我的impl实现

    Impl模式早就有过接触(本文特指通过指针完成impl),我晓得它具有以下优点: 减少头文件暴露出来的非必要内部类(提供静态库,动态库时尤其重要): 减小文件间的编译依存关系,大型代码库的编译时间就不 ...

  4. linux下搭建jenkins

    为了配合上一篇的ant+jenkins做持续集成,需要在linux环境下搭建一个jenkins平台.网上有很多安装的例子,我主要记录一下自己遇到的问题,真真的是特别惆怅的,每次我遇到的问题都格外多. ...

  5. KETTLE入门教程-单表读取

    kettle初探 Kettle简介:Kettle是一款国外开源的ETL工具,纯java编写,可以在Window.Linux.Unix上运行,数据抽取高效稳定.Kettle 中文名称叫水壶,该项目的主程 ...

  6. 16.Java基础_对象内存图

    单个对象 1.进入main函数,生成main函数的栈空间 2.先在mian函数的栈空间里产生一个Student变量(以后会用来存储Studnet对象的首地址),然后进入Student类,在堆内存创建S ...

  7. 2. Linux文件与目录管理

    一.目录与路径 1. 相对路径与绝对路径 绝对路径:路径写法[一定由根目录 / 写起],如:/usr/share/doc 相对路径:路径写法[不由 / 写起], /usr/share/doc 要到 / ...

  8. c# 第41节 异常处理

    本节内容: 1:捕获异常两种方式 2:处理异常的两种方式 3:实例处理异常 4:手动触发异常 5:预定义异常类有哪些 6:自定义异常 1:捕获异常 第一种 捕获:只能知道发生了异常 第二种捕获: 可以 ...

  9. [C12] 大规模机器学习(Large Scale Machine Learning)

    大规模机器学习(Large Scale Machine Learning) 大型数据集的学习(Learning With Large Datasets) 如果你回顾一下最近5年或10年的机器学习历史. ...

  10. lua 3 循环

    while() do ... end i=10 while(i>0) do print(i) i=i-1 end repeat ... until() i=10 repeat print(i) ...