述
了解了线程池之后,再来看一个常用的类,就是 ThreadLocal
这个类在面试中也是很常见的,下面来看一下这个类常见的使用场景
常见使用场景
ThreadLocal
比较常见的有两个地方
- 每个线程都需要一个独享的对象,通常是工具类,比如经常用的
SimpleDateFormat
和Random
,这两个类都不是线程安全的类,使用ThreadLocal
就可以保证线程安全 。每个线程都有一个实例副本,不共享 - 每个线程内需要保存一个全局变量,让不同的方法使用。这种场景可能需要把这个全局变量一级一级通过参数传递, 使用
ThreadLocal
可以避免这种参数传递的麻烦 。同一个请求内(同一个线程内)不同方法间的共享
下面分别看以下这两个场景
第一种场景
每个Thread
都有自己的实例副本,不共享
以常见的时间转换工具类 SimpleDateFormat
为例,写一个测试的示例,代码如下:
/**
* 两个线程打印日期并没有问题
*/
public class ThreadLocalNormalUsage00 {public String date(int seconds){Date date=new Date(1000*seconds);SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");return dateFormat.format(date);}public static void main(String[] args) {new Thread(()-> System.out.println(new ThreadLocalNormalUsage00().date(7))).start();new Thread(()-> System.out.println(new ThreadLocalNormalUsage00().date(1007))).start();}
}
代码很简单,就是创建一个 SimpleDateFormat
做时间转换,上面这段有两个线程,但是 SimpleDateFormat
是局部变量,所以是没有线程安全的问题的
但是当任务数量很大的时候,每个线程都会去执行创建 SimpleDateFormat
的实例,这就可能造成资源的浪费,我们可以把这个工具类转成共享变量去处理,如下:
public class ThreadLocalNormalUsage01{private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");public String date(int seconds){Date date=new Date(1000*seconds);return dateFormat.format(date);}public static void main(String[] args) {Thread thread = new Thread(() -> new ThreadLocalNormalUsage01().date(System.currentTimeMillis()));thread.start();Thread thread1 = new Thread(() -> new ThreadLocalNormalUsage01().date(System.currentTimeMillis()));thread1.start();Thread thread2 = new Thread(() -> new ThreadLocalNormalUsage01().date(System.currentTimeMillis()));thread2.start();}
}
这样就只会创建一个对象,但是这里是会有线程安全的问题的, 这里可以去通过加锁同步的方式去实现,但是同步的方式效率太低了,所以这种情况下就可以去使用 ThreadLocal
,使每个线程都有自己的实例副本,不共享,然后我们对上面这个类做一个改造,代码如下
/*** 利用ThreadLocal,给每个线程分配自己的ateFormat对象,保证了线程安全,高效利用内存*/
public class ThreadLocalNormalUsage02 {public static ExecutorService threadPool=Executors.newFixedThreadPool(10);public String date(int seconds){Date date=new Date(1000*seconds);//SimpleDateFormat dateFormat=new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");SimpleDateFormat dateFormat= ThreadSafeFormatter.dateFormatThreadLocal2.get();return dateFormat.format(date);}public static void main(String[] args) {for (int i =0;i<1000;i++) {int finalI=i;threadPool.submit(new Thread(() -> System.out.println(new ThreadLocalNormalUsage02().date(finalI))));}threadPool.shutdown();}}
class ThreadSafeFormatter{//第一种写法public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal=new ThreadLocal<SimpleDateFormat>(){@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");}};//第二种使用 lombda表达式public static ThreadLocal<SimpleDateFormat>dateFormatThreadLocal2=ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}
这里首先创建一个类,ThreadSafeFormatter
里面就存放了 ThreadLocal<SimpleDateFormat>
需要的时候通过这个类去拿
ThreadLocal
初始化有两种方法,一种是直接重写 initialValue()
方法,另一种是 ThreadLocal.withInitial()
用 lambda 实现,第二种更加简洁,但是效果一样的
以上就是 ThreadLocal
的第一种用法,然后看第二种场景
第二种方式
举个例子,假如有一个web请求,这个请求,会调用依次调用 service1()
service2()
service3()
这三个方法,这三个方法里面都需要一个user参数,通常情况就是在最上面一层获取到user对象,然后一级一级通过参数传递下去,这样就有点繁琐,这种情况下也可以通过 ThreadLocal
实现
代码如下:
/*** 演示 ThreadLocal用法2:避免传递参数麻烦*/
public class ThreadLocalNormalUsage03 {public static void main(String[] args) {new Service1().process();}
}class Service1 {public void process(){User user=new User("xx");UserContextHolder.holder.set(user);new Service2().process();}
}class Service2 {public void process(){User user=UserContextHolder.holder.get();System.out.println("Service2拿到用户名"+user.getUser());new Service3().process();}
}class Service3 {public void process(){User user=UserContextHolder.holder.get();System.out.println("Service3拿到用户名"+user.getUser());}
}class UserContextHolder {public static ThreadLocal<User> holder=new ThreadLocal<>();
}@Data //get,set
@NoArgsConstructor //无参构造
@AllArgsConstructor //有参构造
class User{private String user;
}
这里主要看一下 UserHolder
,这里直接使用 new 来创建对象,并没有做初始化,之后在 Service1
中通过 set()
方法把对象传过去
按上面这种实现方式就可以实现不同的请求(线程)各自存储自己对应的 User
对象,这里主要强调的是同一个线程内的不同方法之间的数据共享。
总结
通过两个案例对 ThreadLocal
的用法有一个基本的了解
总结一下 ThreadLocal
的两个作用
- 让某个需要用到的对象在线程间隔离,每个线程都有自己独立的对象
- 在同一个线程的任何方法中都可以轻松获取到该对象
然后是 ThreadLocal
设置对象的两种方式:
- 通过 initialValue 设置对象,然后这个是会懒加载的,在调用 get() 方法的时候才会去初始化
- 第二种就是通过
set()
方法设置
使用 ThreadLocal
的优点:
- 线程安全
- 不需要加锁,提高执行效率
- 高效利用内存,节省开销
- 免去繁琐的传参