1. 什么是ThreadLocal以及它的作用
通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果实现每个线程都有自己的专属变量该如何设置?ThreadLocal类主要就是解决让每个线程绑定自己的专属变量。
ThreadLocal主要用来存储当前上下文的变量信息,他可以保障存储进去的信息只能被当前的线程读取到,并且线程之间不会受到影响。ThreadLocal为变量在每个线程都创建了一个副本,那么每个线程可以访问自己的内部的副本变量。
2. ThreadLocal的原理
2.1 源码
我们可以看到ThreadLocal中的set()、get()方法都是调用了ThreadLocalMap中的set()、get()方法。最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。
ThreadLocalMap的map,这个map就是用来存储与这个线程绑定的变量,map的key就是ThreadLocal对象,value就是线程正在执行的任务中的某个变量的包装类Entry。
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();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
3. ThreadLocal引发的新问题
3.1 内存泄露问题
ThreadLocalMap中的key是弱引用,而value是强引用。所以,如果ThreadLocal没有被外部强引用的情况下,在垃圾回收时,key会被清空掉,而value不会。ThreadLocal中就会出现key为null的Entry。假如我们不采取措施,value永远不会被GC回收,这个时候就可能会产生内存泄漏问题。
3.2 解决思路
ThreadLocalMap实现中已经考虑了这种情况,在调用set()、get()、remove()方法时会处理掉key为null的记录。使用ThreadLocal时,最好手动调用remove()方法。
4. ThreadLocal的应用场景
- 数据库事务:通过AOP的方式,对执行数据库事务的函数进行拦截。函数开始前,获取connection开启事务并存储在ThreadLocal中,任何用到connection的地方,从ThreadLocal中获取。函数执行完毕,提交事务释放connection。
- 解决线程安全的问题。比如Java7中的SimpleDateFormat不是线程安全的,可以用ThreadLocal来解决这个问题。
ackage fuxi.ThreadLocal;
import java.text.SimpleDateFormat;
import java.util.Random;
public class ThreadLocalExample implements Runnable{
private static final ThreadLocal<SimpleDateFormat>formatter=ThreadLocal.withInitial(()->
new SimpleDateFormat("yyyy-MM--dd :HH-mm"));
public static void main(String[]args){
ThreadLocalExample obj=new ThreadLocalExample();
for(int i=0;i<10;i++){
Thread t=new Thread(obj,""+i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.start();
}
}
@Override
public void run() {
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.set(new SimpleDateFormat());
System.out.println("Thread Name="+Thread.currentThread().getName()+" "+"formatter="+formatter.get().toPattern());
}
}
- 用户登录信息等:用户登录信息好多个方法上都要用到,给每个方法都添加一个User非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,User对象就传不进去了。而ThreadLocal可以在一个线程中传递同一个对象。
package fuxi.ThreadLocal;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[]args){
ExecutorService es= Executors.newFixedThreadPool(3);
String[] users = new String[] { "Bob", "Alice", "Tim", "Mike", "Lily", "Jack", "Bush" };
for (String user : users) {
es.submit(new Task(user));
}
es.shutdown();
}
}
class Task implements Runnable{
private final String name;
public Task(String name){
this.name=name;
}
@Override
public void run() {
try(UserContext ctx=new UserContext(this.name)){
new Task1().process();
new Task2().process();
new Task3().process();
}
}
}
class Task1{
public void process(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("[%s] check user %s...\n", Thread.currentThread().getName(), UserContext.getCurrentUser());
}
}
class Task2{
public void process(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("[%s] check user %s...\n", Thread.currentThread().getName(), UserContext.getCurrentUser());
}
}
class Task3{
public void process(){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("[%s] check user %s...\n", Thread.currentThread().getName(), UserContext.getCurrentUser());
}
}
class UserContext implements AutoCloseable{
private static final ThreadLocal<String> userThreadLocal = new ThreadLocal<>();
public UserContext(String name){
userThreadLocal.set(name);
System.out.printf("[%s] init user %s...\n", Thread.currentThread().getName(), UserContext.getCurrentUser());
}
public static String getCurrentUser(){
return userThreadLocal.get();
}
@Override
public void close() {
System.out.printf("[%s] cleanup for user %s...\n", Thread.currentThread().getName(),
UserContext.getCurrentUser());
userThreadLocal.remove();
}
}
5. 使用ThreadLocal应该注意什么?
在使用ThreadLocal对象,尽量使用static,不然会使线程的ThreadLocalMap产生太多Entry,从而造成内存泄露。
参考资料:
https://www.iteye.com/blog/liudeh-009-1934768
https://www.liaoxuefeng.com/wiki/1252599548343744/1306581251653666