Threadlocal
ThreadLocal是Thread的局部变量,用于编多线程程序。Threadlocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据。
为什么要使用Threadlocal
从上面的定义可以了解到,他可以保证数据变成局部变量,这样也就保证了数据的不共享。那么使用方法是怎样的?
import sun.misc.Launcher;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.util.*;
/**
* @author 龙小虬
* @date 2021/5/6 11:17
*/
public class Test01 {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
threadLocal.set("1");
System.out.println(threadLocal.get());
}
}
从这里貌似看不出用处,他主要用于高并发状况下。
我们来看这两段代码:
import sun.misc.Launcher;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.util.*;
/**
* @author 龙小虬
* @date 2021/5/6 11:17
*/
public class Test01 {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static int count;
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
count = finalI;
try {
Thread.sleep(500);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + "," + count);
}, i + "").start();
}
}
}
这一段代码我们可以看出来,肯定是存在线程安全的。因为共享变量会在休眠的这段时间不断更改。
运行结果可想而知
很明显,这与我们的预期结果不同。
import sun.misc.Launcher;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.util.*;
/**
* @author 龙小虬
* @date 2021/5/6 11:17
*/
public class Test01 {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
private static int count;
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
int finalI = i;
new Thread(() -> {
threadLocal.set(finalI + "");
try {
Thread.sleep(500);
} catch (Exception e) {
}
System.out.println(Thread.currentThread().getName() + "," + threadLocal.get());
}, i + "").start();
}
}
}
反观这段代码的运行结果并不会发生变化
这样就保证了,在高并发的情况下,各个线程不会阻塞,正常的运行,而且还不会发生线程不安全问题。这句话提到了不会阻塞,在前面学习多线程的时候,为了保证全局变量的安全问题,是采取了加锁(synchronized)的方法。
这里就提及一下,synchronized与Threadlocal的区别,在上面的案例可以看出来,Threadlocal是将数据存储了,所以可以看出差别
两者均可以解决线程安全问题
Threadlocal占用内存多,因为需要缓存数据
Threadlocal以空间换时间,来保证线程安全
synchronized以时间换空间,来保证线程安全
现在就来看看他的主要方法。
Threadlocal的主要api
set()
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;
}
这里可以看到他的存储方式是利用的ThreadLocalMap,首先获取到当前线程的ThreadLocalMap ,之后在使用Threadlocal对象作为key,存入数据。
remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
其实这些都不是重点。重点在于Threadlocal存在内存溢出的问题。
Threadlocal内存溢出问题
在了解为什么会内存溢出的问题之前,要了解几个概念。
- 弱引用
调用方法WeakReference来创建,具体请查看可达性分析算法与四种引用 - 内存泄露
指程序在申请内存后,无法释放已申请的内存空间,内存泄露堆积会导致内存被占光,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
了解了这个之后,我们可以看看内部的ThreadLocalMap源码:
可以看到,Entry继承的是采用弱引用的Threadlocal,那么一旦内存不足,java中的弱引用在内存不足的时候会被回收掉,回收之后变成(null,value)的形式,key被收回掉了。后面获取的时候,也不会有Thread.currentThread()
为null,那么这个key-value一直都不会被回收。可能就会造成内存溢出。那么这个问题是怎么解决的呢?
- 可以自己调用remove方法将不要的数据移除避免内存泄漏的问题
- 每次在做set方法的时候会清理之前 key为null的键值对
Threadlocal为什么采用弱引用而不是强引用
-
强引用:
将ThreadLocal 的引用指向为null,但是每个线程中有自己独立ThreadLocalMap还一直在继续持有该对象,但是ThreadLocal 对象不会被回收,就会发生ThreadLocal内存泄漏的问题。 -
弱引用:
将ThreadLocal 的引用指向为null,Entry 中的key指向为null,但是下次调用set()方法的时候,会根据判断如果key空的情况下,直接删除,有可能会发生Entry内存泄漏的问题。
所以说不管是用强引用还是弱引用都是会发生内存泄漏的问题,使用弱引用可以实行手动回收部分键值对,减少内存泄露的概率。
但是最终根本的原因Threadlocal内存泄漏的问题,产生于ThreadLocalMap与当前线程的生命周期一样,如果没有手动的删除的情况下,就有可能会发生内存泄漏的问题。