使用场景
首先看以下的程序:
public class Main {
public static class MyRunnable implements Runnable {
int val = 0;
@Override
public void run() {
val = (int) (Math.random()*100);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(val);
}
}
public static void main(String[] args) {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);
thread1.start();
thread2.start();
}
}
/*
可能的输出:
65
65
*/
可知,变量var是被thread1和thread2两个线程共享的,当其中一个线程修改了var以后,另一个线程再修改var,就会造成var值被覆盖掉。
这是一个典型的多个线程跑同一份代码。但是,如果我们不想这些线程共享某些变量,或者说var在每个线程内都有一个独立备份,这时候该怎么办呢?TreadLocal就是用来解决这个问题的。
例子1:
public class Main {
public static class MyRunnable implements Runnable {
private ThreadLocal threadLocal = new ThreadLocal();
@Override
public void run() {
threadLocal.set((int) (Math.random() * 100D));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(threadLocal.get());
}
}
public static void main(String[] args) {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);
thread1.start();
thread2.start();
}
}
/*
可能的输出:
68
34
*/
如果我需要两个不共享的变量呢?很简单,再添加一个ThreadLocal对象用来存储数据就行了。也就是说一个ThreadLocal只代表一个变量的独立备份,多个变量需要独立备份,就要有多个ThreadLocal对象来存放独立备份。
例子2:
public class Main {
public static class MyRunnable implements Runnable {
private ThreadLocal threadLocal = new ThreadLocal();
private ThreadLocal threadLocal2 = new ThreadLocal();
@Override
public void run() {
threadLocal.set((int) (Math.random() * 100D));
threadLocal2.set(Math.random() * 100D);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(threadLocal.get());
System.out.println(threadLocal2.get());
}
}
public static void main(String[] args) {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);
thread1.start();
thread2.start();
}
}
/*
可能的输出:
28
34
56.049492341729
11.603018021238775
*/
实现方案
从上面的ThreadLocal的使用,可以知道ThreadLocal对象也是多个线程共享的,为什么不同的线程往里面放数据,还可以从里面获取每个线程自己存放的数据呢?
方案一:ThreadLocal维护线程与实例之间的映射
既然每个访问 ThreadLocal 变量的线程都有自己的一个“本地”实例副本。一个可能的方案就是 ThreadLocal 维护一个 Map,键是 Thread,代码大概如下:
public class ThreadLocal {
private Map mMap = new HashMap<>();
public void set(Object o) {
mMap.put(Thread.currentThread(), o);
}
public Object get() {
return mMap.get(Thread.currentThread());
}
public void remove() {
mMap.remove(Thread.currentThread());
}
}
//以下为泛型形式
public class ThreadLocal {
private Map mMap = new HashMap<>();
public void set(T o) {
mMap.put(Thread.currentThread(), o);
}
public T get() {
return mMap.get(Thread.currentThread());
}
public void remove() {
mMap.remove(Thread.currentThread());
}
}
该方案可满足上文提到的每个线程内一个独立备份的要求。每个新线程访问该 ThreadLocal 时,需要向 Map 中添加一个映射,而每个线程结束时,应该清除该映射。这里就有两个问题:
增加线程与减少线程均需要写 Map,故需保证该 Map线程安全。虽然从ConcurrentHashMap的演进看Java多线程核心技术一文介绍了几种实现线程安全 Map的方式,但它或多或少都需要锁来保证线程的安全性
线程结束时,需要保证它所访问的所有 ThreadLocal 中对应的映射均删除,否则可能会引起内存泄漏
其中锁的问题,是 JDK 未采用该方案的一个原因。
方案二:Thread维护ThreadLocal与实例的映射
上述方案中,出现锁的问题,原因在于多线程访问同一个 Map。如果该 Map 由 Thread 维护,从而使得每个 Thread 只访问自己的 Map,那就不存在多线程写的问题,也就不需要锁。代码如下:
public class Main {
public static class Mythread extends Thread {
private HashMap container = null;
public Mythread(Runnable r) {
super(r);
}
public void putData(Object key, Object data) {
if (container == null) {
container = new HashMap<>();
}
container.put(key, data);
}
public Object getData(Object key) {
if (container == null) {
return null;
}
return container.get(key);
}
}
public static class MyRunnable implements Runnable {
@Override
public void run() {
Mythread curr = (Mythread) Thread.currentThread();
curr.putData("key1", (int) (Math.random() * 100D));
curr.putData("key2", Math.random() * 100D);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
System.out.println(curr.getData("key1"));
System.out.println(curr.getData("key2"));
}
}
public static void main(String[] args) {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Mythread(sharedRunnableInstance);
Thread thread2 = new Mythread(sharedRunnableInstance);
thread1.start();
thread2.start();
}
}
/*
可能的运行结果:
45
11
4.537484778554357
27.90239993353697
*/
JDK中的实现
JDK中的实现类似于第二种方案,在Thread类中,有一个ThreadLocal.ThreadLocalMap类型的变量threadLocals,实现类似上面container的功能;其次,在ThreadLocal的get和set方法内部获取threadLocals来存放数据。threadLocals也不像上面的container,直接使用字符串做为键,而是使用ThreadLocal对象本身,不同的ThreadLocal对象刚好对应这不同的独立备份。
ThreadLocal如何防止内存泄漏
ThreadLocal中,获取到线程私有对象是通过Thread类持有的一个threadLocalMap(threadLocals),然后传入ThreadLocal当做key获取到对象的,这时候就有个问题,如果你在使用完ThreadLocal之后,将其置为null,这时候这个对象并不能被回收,因为他还有 ThreadLocalMap->entry->key的引用,直到该线程被销毁,但是这个线程很可能会被放到线程池中不会被销毁,这就产生了内存泄露,jdk是通过弱引用来解决的这个问题的,entry中对key的引用是弱引用,当你取消了ThreadLocal的强引用之后,他就只剩下一个弱引用了,所以也会被回收。
弱引用的使用,可参考这篇文章 http://www.cnblogs.com/dolphin0520/p/3784171.html
参考文章:
https://blog.csdn.net/u011983531/article/details/50833784
https://www.tuicool.com/articles/auE3A3e