什么是Threadlocal
在Java中,ThreadLocal是一个容器,它为每个线程提供一个独立的变量副本。每个线程都可以独立地改变其副本,而不会影响其他线程的副本。
Threadlocal的使用
实例代码
public class ThreadLocalExample {
private static ThreadLocal<Integer> counter = new ThreadLocal<>();
private static ThreadLocal<Integer> counter2 = new ThreadLocal<>();
public static void main(String[] args) {
// 创建并启动多个线程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
int value1 = (int) (Math.random() * 100);
counter.set(value1);
int value2 = (int) (Math.random() * 100);
counter2.set(value2);
System.out.println(Thread.currentThread().getName() + " set counter to " + value1);
System.out.println(Thread.currentThread().getName() + " set counter2 to " + value2);
// 模拟一些业务逻辑
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " get counter: " + counter.get());
System.out.println(Thread.currentThread().getName() + " get counter2: " + counter2.get());
}).start();
}
}
}
执行结果
Thread-1 set counter to 82
Thread-4 set counter to 36
Thread-3 set counter to 98
Thread-2 set counter to 83
Thread-0 set counter to 11
Thread-1 set counter2 to 95
Thread-2 set counter2 to 17
Thread-4 set counter2 to 49
Thread-3 set counter2 to 89
Thread-0 set counter2 to 88
Thread-0 get counter: 11
Thread-2 get counter: 83
Thread-0 get counter2: 88
Thread-4 get counter: 36
Thread-4 get counter2: 49
Thread-2 get counter2: 17
Thread-1 get counter: 82
Thread-1 get counter2: 95
Thread-3 get counter: 98
Thread-3 get counter2: 89
Threadlocal的原理
一个 Thread 里面只有一个ThreadLocalMap ,而在一个 ThreadLocalMap 里面却可以有很多的 ThreadLocal,每一个 ThreadLocal 都对应一个 value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map 的数据结构来存放 ThreadLocal 和 value。
ThreadLocal -> 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();
}
ThreadLocal -> 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);
}
}
ThreadLocal为什么可能产生内存泄漏,如何避免?
如果说一个线程执行完毕,线程Thread随之被释放,那么value便不存在内存泄漏的问题。然而,我们一般会通过线程池的方式来复用Thread对象来节省资源,这就会导致一个Thread对象的生命周期会非常长,随着任务的执行,value就有可能越来越多且无法释放,最终导致内存泄漏。
因此,我们在使用完ThreadLocal变量后,要手动调用remove()方法来清理ThreadLocalMap
解决线程安全问题的途径
使用Threadlocal
- 优点: 每一个线程绑定自己的值,自己用自己的,不跟别人争抢
- 缺点: 每个线程绑定的值会额外占据内存空间
使用synchronized或者lock
- 优点: 线程相互竞争同一份资源,不会额外占据内存空间
- 缺点: 加锁会使程序执行由异步改为同步,降低执行效率
Threadlocal的应用
解决SimpleDateFormat线程不安全的问题
为什么线程不安全
一般来说,这种时间格式转换工具,我们都会做成一个公共工具类,如下所示:
public class DateUtil {
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static String formatDate(Date date)throws ParseException {
return sdf.format(date);
}
public static Date parse(String strDate) throws ParseException{
return sdf.parse(strDate);
}
}
SimpleDateFormat线程不安全的原因是因为它的实现类java.text.SimpleDateFormat在内部使用了一个共享的Calendar对象,并在多线程环境下对它进行修改和访问,导致线程竞争和数据不一致的问题。
public class SimpleDateFormat extends DateFormat {
// 省略其他代码...
private final Calendar calendar;
// 省略其他代码...
public SimpleDateFormat(String pattern) {
this(pattern, Locale.getDefault(Locale.Category.FORMAT));
}
public SimpleDateFormat(String pattern, Locale locale) {
// 省略其他代码...
this.calendar = new GregorianCalendar(locale);
// 省略其他代码...
}
// 省略其他代码...
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
// 省略其他代码...
calendar.setTime(date);
// 省略其他代码...
}
@Override
public Date parse(String source, ParsePosition pos) {
// 省略其他代码...
calendar.clear();
// 省略其他代码...
// 解析 source 并设置到 calendar 对象中
// 省略其他代码...
return calendar.getTime();
}
// 省略其他代码...
}
从源码分析可以看出以下关键点:
SimpleDateFormat内部有一个共享的Calendar对象,用于解析和格式化日期。
Calendar对象在多个方法中被修改和访问,包括format和parse等方法。
在多线程环境中,多个线程同时调用format和parse等方法会导致对Calendar对象的并发读写操作,从而引发不一致的问题。
由于SimpleDateFormat的内部使用的Calendar对象是共享的,不同线程同时修改它会导致竞争条件。这可能导致解析和格式化的结果不正确,或者抛出异常。因此,SimpleDateFormat在多线程环境中是线程不安全的,需要采取线程安全的措施来保证正确的日期处理。例如,可以使用ThreadLocal来为每个线程创建独立的SimpleDateFormat对象,以避免线程安全问题。
使用ThreadLocal来保证SimpleDateFormat类在使用过程中的线程安全
public class DateUtil {
private static ThreadLocal<DateFormat> dateFormat = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static String format(Date date) {
return dateFormat.get().format(date);
}
}