JUC-2.0-ThreadLocal-基本使用

了解了线程池之后,再来看一个常用的类,就是 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>(){
        @Override
        protected 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 的优点:

  • 线程安全
  • 不需要加锁,提高执行效率
  • 高效利用内存,节省开销
  • 免去繁琐的传参
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值