一、概念
1.1 什么是ThreadLocal?
ThreadLocal是线程局部变量,所谓的线程局部变量,就是仅仅只能被本线程访问,不能在线程之间进行共享访问的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
1.2 问题引入
假如语文老师有一本书,但是班上有30名学生,老师将这本书送给学生们去阅读,30名学生都想阅读这本书。
为保证每个学生都能阅读到书籍,那么基本可以有两种方案,一是按照某种排序(例如姓名首字母排序),让每个学生依次阅读。
二是让30名学生同时争抢,谁抢到谁就去阅读,读完放回原处,剩下的29名学生再次争抢。
显然第一种方案,基本表现为串行阅读,时间成本较大,第二种方案为多个学生争抢,容易发生安全问题(学生发生冲突或者书籍在争抢过程中被毁坏)。
为了解决这两个问题,那么有没有更加好的方案呢?当然有,老师可以将书籍复印30本,每个学生都发一本,这样既大大提高了阅读效率,节约了阅读时间,还能保证每个学生都能有自己的书籍,这样就不会发生争抢,避免了安全问题。
1.3 类似案例
假如我们有一个需求,那就是在多线程环境下,去格式化时间为指定格式yyyy-MM-dd HH:mm:ss
,假设一开始只有两个线程需要这么做,代码如下:
public class Test {
public static void main(String[] args) {
new Thread( ()-> System.out.println( new Test().date( 10 ) ) ).start();
new Thread( ()-> System.out.println( new Test().date( 100 ) ) ).start();
}
private String date(int seconds) {
// 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return dateFormat.format(date);
}
}
在线程少的情况下是没有问题的,我们在每个线程里调用date方法,也就是在每个线程里都执行了创建SimpleDateFormat对象,每个对象在各自的线程里面执行格式化时间。但是我们是否会思考到,假如有1000个线程需要格式化时间,那么需要调用1000次date方法,也就是需要创建1000个作用一样的SimpleDateFormat对象,这样是不是太浪费内存了?也给GC带来压力?
于是我们联想到,1000个线程来共享一个SimpleDateFormat对象,这样SimpleDateFormat对象只需要创建一次即可,代码如下:
public class Test2 {
// 创建一个定长线程池,并发数量控制在10
public static ExecutorService executorService = Executors.newFixedThreadPool(10);
private static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
for (int index = 0; index < 1000 ; index++) {
int finalIndex = index;
executorService.submit( () -> System.out.println( new Test2().date(finalIndex) ) );
}
// 关闭线程池,此种关闭方式不再接受新的任务提交,等待现有队列中的任务全部执行完毕之后关闭
executorService.shutdown();
}
private String date(int seconds) {
// 参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
return DATE_FORMAT.format(date);
}
}
上述代码我们使用到了固定线程数的线程池来执行时间格式化任务,我们来执行一下,看看结果: