ThreadLocal
多线程的本质就是增加任务的并发,提高效率。但是又要控制任务不错乱,可以通过锁来控制资源的访问。
除了控制资源的访问外,我们可以通过增加资源来保证所有对象的线程安全。比如100个人填写个人信息表,如果只有一支笔,那么大家都得排队,如果准备100支笔,这样人手一支笔,就可以很快完成填写信息。
如果说锁是第一种思路,ThreadLocal就是第二种思路。
ThreadLocal的简单实用
从ThreadLocal的名字上可以看到,这是一个线程的局部变量,也就是说只有当前线程可以访问,自然是线程安全的。
下面来看一个简单示例:
package main.java.study;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadLocalTest {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static class ParseDate implements Runnable {
int i = 0;public ParseDate(int i) {
this.i = i;
}public void run() {
try {
Date t = sdf.parse("2019-05-24 17:00:" + i % 60);
System.out.println(i + ":" + t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
es.execute(new ParseDate(i));
}
}}
运行结果:
结果中即有正确的,又有错误的异常。出现这种问题的原因是SimpleDateFormat.parse()方法并不是线程安全的。因此在线程池中共享这个对象必然导致错误。
一种可行的方法是在sdf.parse()方法上加锁,这是一般思路,这里我们不这么做,我们使用ThreadLocal为每个线程都产生一个SimpleDateFormat对象。
package main.java.study;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadLocalTest2 {
static ThreadLocal<SimpleDateFormat> tl = new ThreadLocal<SimpleDateFormat>();
public static class ParseDate implements Runnable {
int i = 0;public ParseDate(int i) {
this.i = i;
}public void run() {
try {
if (tl.get() == null) {
tl.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); //必须为ThreadLocal分配不同的对象,不然不能保证线程安全。
}
Date t = tl.get().parse("2019-05-24 17:00:" + i % 60);
System.out.println(i + ":" + t);
} catch (ParseException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
es.execute(new ParseDate(i));
}
}}
执行结果:
从上面可以看出,为每个线程人手分配一个对象工作并不是由ThreadLoca来完成,而是在应用层保证。如果在应用上为每个线程分配了同一个对象,则ThreadLocal也不能保证线程安全。
ThreadLocal原理
(上图来源:https://blog.****.net/aaronsimon/article/details/82711336)
- 每个Thread线程内部都有一个Map;
- Map里面存储线程本地对象(key)和线程的变量副本(value)
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
ThreadLocal源码
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);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}ThreadLocal.ThreadLocalMap threadLocals = null;
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//每个ThreadLocal对象都有一个HashCode
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
Thread.ThreadLocalMap<ThreadLocal, Object>;
1、Thread: 当前线程,可以通过Thread.currentThread()获取。
2、ThreadLocal:我们的static ThreadLocal变量。
3、Object: 当前线程共享变量。
我们调用ThreadLocal.get方法时,实际上是从当前线程中获取ThreadLocalMap<ThreadLocal, Object>,然后根据当前ThreadLocal获取当前线程共享变量Object。
ThreadLocal.set,ThreadLocal.remove实际上是同样的道理。
这种存储结构的好处:
1、线程死去的时候,线程共享变量ThreadLocalMap则销毁。
2、ThreadLocalMap<ThreadLocal,Object>键值对数量为ThreadLocal的数量,一般来说ThreadLocal数量很少,相比在ThreadLocal中用Map<Thread, Object>键值对存储线程共享变量(Thread数量一般来说比ThreadLocal数量多),性能提高很多。
关于ThreadLocalMap<ThreadLocal, Object>弱引用问题:
当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,造成内存泄露。(ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在)。
虽然ThreadLocal的get,set方法可以清除ThreadLocalMap中key为null的value,但是get,set方法在内存泄露后并不会必然调用,所以为了防止此类情况的出现,我们有两种手段。
1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;
2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。