ThreadLocal的作用
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?
JDK 中自带的ThreadLocal
类正是为了解决这样的问题。 ThreadLocal
类主要解决的就是让每个线程绑定自己的值,可以将ThreadLocal
类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。
ThreadLocal类型的变量,每个线程访问的时候,都会有这个变量的本地副本。线程可以使用get和set方法来获取默认值或将其值更改为当前线程所存的副本值,避免线程安全问题。
使用案例
使用ThreadLocal存放SimpleDateFormat类型的变量
java
import java.text.SimpleDateFormat;
import java.util.Random;
public class ThreadLocalExample implements Runnable{
// SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
public static void main(String[] args) throws InterruptedException {
ThreadLocalExample obj = new ThreadLocalExample();
for(int i=0 ; i<10; i++){
Thread t = new Thread(obj, ""+i);
Thread.sleep(new Random().nextInt(1000));
t.start();
}
}
@Override
public void run() {
//初始格式是:yyyyMMdd HHmm
System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
//formatter pattern is changed here by thread, but it won't reflect to other threads
// 默认构造方法,格式是yy-M-d ah:mm
formatter.set(new SimpleDateFormat());
System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
}
}
执行结果
java
Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm
结果分析
formatter初始格式是:yyyyMMdd HHmm,在线程执行过程中,会将其修改为yy-M-d ah:mm。线程0将其修改为yy-M-d ah:mm格式之后,线程1获取到的formatter仍然是:yyyyMMdd HHmm,说明线程0修改了格式,不影响线程1,线程0只是修改其本地副本。
不使用ThreadLocal
java
import java.text.SimpleDateFormat;
import java.util.Random;
public class ThreadLocalExamlple implements Runnable {
//private static final ThreadLocal<SimpleDateFormat> formatter =
// ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
private static SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd HHmm");
public void run() {
System.out.println("Thread Name= " + Thread.currentThread().getName()+
" default Formatter = " +formatter.toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
formatter = new SimpleDateFormat();
System.out.println("Thread Name= " + Thread.currentThread().getName() + " formatter " + formatter.toPattern());
}
public static void main(String[] args) throws InterruptedException {
ThreadLocalExamlple obj = new ThreadLocalExamlple();
for (int i = 0; i < 10; i++) {
Thread t = new Thread(obj, "" + i);
Thread.sleep(new Random().nextInt(1000));
t.start();
}
}
}
执行结果
ini
Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter yy-M-d ah:mm
Thread Name= 1 default Formatter = yy-M-d ah:mm
Thread Name= 2 default Formatter = yy-M-d ah:mm
Thread Name= 1 formatter yy-M-d ah:mm
Thread Name= 3 default Formatter = yy-M-d ah:mm
Thread Name= 2 formatter yy-M-d ah:mm
Thread Name= 4 default Formatter = yy-M-d ah:mm
Thread Name= 3 formatter yy-M-d ah:mm
Thread Name= 5 default Formatter = yy-M-d ah:mm
Thread Name= 4 formatter yy-M-d ah:mm
Thread Name= 6 default Formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yy-M-d ah:mm
Thread Name= 6 formatter yy-M-d ah:mm
Thread Name= 5 formatter yy-M-d ah:mm
Thread Name= 7 formatter yy-M-d ah:mm
Thread Name= 8 default Formatter = yy-M-d ah:mm
Thread Name= 8 formatter yy-M-d ah:mm
Thread Name= 9 default Formatter = yy-M-d ah:mm
Thread Name= 9 formatter yy-M-d ah:mm
结果分析
formatter格式是:yyyyMMdd HHmm,在线程执行过程中,会将其修改为yy-M-d ah:mm。线程0将其修改为yy-M-d ah:mm格式之后,后面线程获取到的初始formatter是:yy-M-d ah:mm,说明线程0修改了格式,不是在其本地修改的,其他线程能获取到线程0修改之后的值。
ThreadLocal的原理
ThreadLocal的set方法源码
java
public void set(T value) {
//获取当前请求的线程
Thread t = Thread.currentThread();
//取出 Thread 类内部的 threadLocals 变量(哈希表结构)
ThreadLocalMap map = getMap(t);
if (map != null)
// 将需要存储的值放入这个哈希表中
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
根据上述代码可以看到:最终的变量其实是存放在当前线程的ThreadLocalMap
(ThreadLocal
中的静态内部类)中,而不是存在
ThreadLocal`上。
ThreadLocal
可以理解为只是ThreaLocalMap
的封装,传递了变量值。ThreadLocal
类中可以通过Thread.currentThread()
获取到当前线程对象后,直接通过getMap(Thread t)
访问到该线程的ThreadLocalMap
对象。
Thread类
java
public class Thread implements Runnable {
//......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......
}
每个Thread对象中都只有一个ThreadLocalMap
,ThreadLocalMap
可以存储以ThreadLocal
为key,Object对象为value的键值对。
比如我们在同一个线程中声明了两个ThreaddLocal
对象的话,Thread
内部都是使用仅有的那个ThreadLocalMap
存放数据的,ThreaLocalMap
的key就是ThreadLocal
对象,value就是ThreadLocal
对象调用set方法设置的值。
ThreadLocal
数据结构如下图所示
ThreadLocal
内部类
ThreadLocal的内存泄漏问题
ThreadLocalMap
中使用的 key 为 ThreadLocal
的弱引用,而 value 是强引用。所以,如果 ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。
这样一来,ThreadLocalMap
中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap
实现中已经考虑了这种情况,在调用 set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal
方法后最好手动调用remove()
方法
java
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
弱引用介绍
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。