当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了。数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。
一、使用场景
- 每个线程需要一个独享的对象
- 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
1. 每个线程需要一个独享的对象
以SimpleDateFormat为例,该类是一个线程不安全的类,看下面的例子: ```java public class ThreadLocalNormalUsage { static SimpleDateFormat dateFormat = new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”); public static void main(String[] args) {
} public String date(int seconds) {ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10,10,0, TimeUnit.SECONDS,new LinkedBlockingQueue<>());
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage().date(finalI);
System.out.println(date);
}
});
}
} }Date date = new Date(1000 * seconds); return dateFormat.format(date);
执行结果如下: 1970-01-01 08:00:08 1970-01-01 08:00:09 1970-01-01 08:00:08 1970-01-01 08:00:08 1970-01-01 08:00:12 …..
可以看出来,为了避免重复创建SimpleDateFormat对象,使用static的SimpleDateFormat,却发现由于SimpleDateFormat是线程不安全的,导致执行结果不符合实际结果。<br />**改进以后:**
```java
public class ThreadLocalNormalUsage02 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage02().date(finalI);
System.out.println(date);
}
});
}
threadPool.shutdown();
}
public static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal=new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public String date(int seconds) {
Date date = new Date(1000 * seconds);
return simpleDateFormatThreadLocal.get().format(date);
}
}
---------------
执行结果正常。
可以看到,使用ThreadLocal每个线程创建一个SimpleDateFormat副本,相比于每个任务创建一个SimpleDateFormat对象来讲节省了内存,同时相比使用synchronized加锁的方式提高了执行效率。
2. 每个线程内需要保存全局变量
当前用户信息需要被线程内所有方法共享:每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦。
当多线程同时工作时,我们需要保证线程安全,可以用synchronized ,也可以用ConcurrentHashMap,但无论用什么,都会对性能有所影响
使用threadLocal以后:
使用:
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process("");
}
}
class Service1 {
public void process(String name) {
User user = new User("超哥");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
ThreadSafeFormatter.dateFormatThreadLocal.get();
System.out.println("Service2拿到用户名:" + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到用户名:" + user.name);
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
二、使用场景总结
根据共享对象的生成时机不同,选择initialValue或set来保存对象:
1.在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制。
2.如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用
ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用。
三、源码分析
# Thread类的成员变量,map的原因是一个thread可以存储很多个threadLocal对象。
ThreadLocal.ThreadLocalMap threadLocals = null;
重要的事说一遍:ThreadLocalMap事Thread类的成员变量。
initialValue()和get()
该方法会返回当前线程对应的“初始值”这是一个延迟加载的方法,只有在调用get的时候,才会触发。
protected T initialValue() {
return null;
}
可以看到,该方法返回了一个null,所以要求我们在使用的时候重写该方法。在使用get()方法时调用。
//调用方式。
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
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();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
- 可以看到,首次调用get方法时,ThreadLocalMap的map为null,所以调用的时setInitialValue()方法,再该方法中的initialValue()方法,即我们重写的该方法。
- 当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法.
- 通常,每个线程最多调用一次此方法,但如果已经调用了remove(后,再调用get(),则可以再次调用此方法,因为remove过后,ThreadLocalMap又变成了null,所以就会再次调用setInitialValue()方法。
- 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。
- get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
注意,这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中
void set(T value)
想清楚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); } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
void remove()
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
注意,这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中
补充:
内存泄漏问题:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到Entry类继承了WeakReference弱引用,super(k)说明可以k被gc垃圾回收, 但是 value = v又是一个强引用.
- ThreadLocalMap的每个Entry都是一个对key的弱引用,同时,每个Entry都包含了一个对value的强引用
- 正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了
- 但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链:
因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM.
JDK已经考虑到了这个问题,所以在set,remove, rehash方法中会扫描key为null,并把对应的value设置为null。
解决方法:
阿里规约规定:调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法。