本篇博客将为大家介绍一下ThreadLocal。从用途、使用方法、原理、及常见问题四个方面来介绍。
1.ThreadLocal用途
ThreadLocal用途可以理解成一个“储物间”,这个“储物间”当中拥有大量的“储物柜”,每个“储物柜”实际上就是每个线程,当中存放的是Thread线程中参数,针对于ThreadLocal的set方法其实就是将参数放入到当前线程对应的“储物柜”当中(根据Thread.currentThread()进行区分线程),同样get()和remove()方法也是一样。
2.ThreadLocal使用方法
下面我们编写一个程序,每个线程保存一个自己的ID数据,然后多次调用进行修改ID数据并将其打印。
public class ThreadLocalIds {
private static final ThreadLocal<AtomicInteger> THREAD_LOCAL = new ThreadLocal<AtomicInteger>() {
protected AtomicInteger initialValue() {
return new AtomicInteger(0);// threadlocal中初始化值,每个线程的ID都从0开始增加
};
};
public static int getIntValue() {
AtomicInteger atomicInteger=THREAD_LOCAL.get();
return atomicInteger.getAndIncrement();// 获取并增加
}
public static void remove() {
THREAD_LOCAL.remove();// 移除
}
}
public class ThreadLocalTest {
public static void main(String[] args) {
increment();//主线程
new Thread(new Runnable() {//依次启动并创建三个线程
@Override
public void run() {
increment();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
increment();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
increment();
}
}).start();
}
private static void increment() {
try {
for (int i = 0; i < 6; i++) {// 每个线程增加6次
System.out.println("当前线程名:" + Thread.currentThread().getName() + " ,No." + i + ", intValue="
+ ThreadLocalIds.getIntValue());
}
} finally {
ThreadLocalIds.remove();
}
}
}
最后代码的执行结果如下,可以看到每个线程的ID都是依次从0开始增加。
当前线程名:main ,No.0, intValue=0
当前线程名:main ,No.1, intValue=1
当前线程名:main ,No.2, intValue=2
当前线程名:main ,No.3, intValue=3
当前线程名:main ,No.4, intValue=4
当前线程名:main ,No.5, intValue=5
当前线程名:Thread-0 ,No.0, intValue=0
当前线程名:Thread-0 ,No.1, intValue=1
当前线程名:Thread-0 ,No.2, intValue=2
当前线程名:Thread-0 ,No.3, intValue=3
当前线程名:Thread-0 ,No.4, intValue=4
当前线程名:Thread-0 ,No.5, intValue=5
当前线程名:Thread-1 ,No.0, intValue=0
当前线程名:Thread-1 ,No.1, intValue=1
当前线程名:Thread-1 ,No.2, intValue=2
当前线程名:Thread-1 ,No.3, intValue=3
当前线程名:Thread-1 ,No.4, intValue=4
当前线程名:Thread-1 ,No.5, intValue=5
当前线程名:Thread-2 ,No.0, intValue=0
当前线程名:Thread-2 ,No.1, intValue=1
当前线程名:Thread-2 ,No.2, intValue=2
当前线程名:Thread-2 ,No.3, intValue=3
当前线程名:Thread-2 ,No.4, intValue=4
当前线程名:Thread-2 ,No.5, intValue=5
3.ThreadLocal原理
1.ThreadLocal类结构:
ThreadLocal拥有三个方法,set(),get(),remove()以及内部类ThreadLocalMap
2.ThreadLocal及Thread之间的关系
从上面这张图可以看到Thread中属性threadLocal是作为一个特殊的Map,它的key值就是我们ThreadLocal
实例,而value值就是我们设置的值。
然后我们看一下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();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
第二行代码getMap就是当前的线程中获取我们设置的ID值。如果是第一次来调用get方法,会执行初始值的操作,也就是我们赋值的0.
4.ThreadLocal常见的使用问题
关于ThreadLocal使用不当造成的内存泄漏问题:
上面ThreadLocal的源码中有一行是从ThreadLocalMap.Entry进行获取,Entry类这个是弱引用,弱引用的回收是在JVM的下一次GC回收之前。
那么我们可以总结一下ThreadLocal出现内存泄漏的条件:
- ThreadLocal的引用设置为null,且后续没有任何操作。
- 线程池中使用,线程一直运行没有停止。
- 触发了Minor GC或者是Full GC(这一条是根据其他大牛的博客得来)
平常我们在使用ThreadLocal时可以尽量申明为private static final或者使用之后进行remove(),这两种方法都可以避免出现内存泄漏的问题。