当前位置: 代码迷 >> 综合 >> JUC-2.0-ThreadLocal-基本使用
  详细解决方案

JUC-2.0-ThreadLocal-基本使用

热度:67   发布时间:2023-12-05 18:31:04.0

了解了线程池之后,再来看一个常用的类,就是 ThreadLocal 这个类在面试中也是很常见的,下面来看一下这个类常见的使用场景

常见使用场景

ThreadLocal 比较常见的有两个地方

  1. 每个线程都需要一个独享的对象,通常是工具类,比如经常用的 SimpleDateFormatRandom ,这两个类都不是线程安全的类,使用 ThreadLocal 就可以保证线程安全 。每个线程都有一个实例副本,不共享
  2. 每个线程内需要保存一个全局变量,让不同的方法使用。这种场景可能需要把这个全局变量一级一级通过参数传递, 使用 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 的优点:

  • 线程安全
  • 不需要加锁,提高执行效率
  • 高效利用内存,节省开销
  • 免去繁琐的传参
  相关解决方案