JAVA线程基础知识-ThreadLocal

1、ThreadLocal的用途

经典场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)

经典场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦

2、经典场景1

  • 每个Thread内有自己的实例副本,不共享
  • 比喻:教材只有一本,一起做笔记有线程安全问题。复印后没问题

我们使用线程池来帮助我们创建线程

/**
 * @Classname ThreadLocalNormalUsage02
 * @Description 使用线程池来优化
 * @Date 2021/4/11 12:19
 * @Created by WangXiong
 */
public class ThreadLocalNormalUsage02 {

    public String date(int seconds){
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            final int finalI = i;
            executorService.submit(new Runnable() {
                public void run() {
                    //我们创建了1000个工具类对象,能否优化只创建一次?
                    String date = new ThreadLocalNormalUsage02().date(finalI);
                    System.out.println(date);
                }
            });
        }
        executorService.shutdown();

    }
}

 我们将创建1000次的工具类进行优化,将他提取出来

/**
 * @Classname ThreadLocalNormalUsage03
 * @Description 使用线程池优化,将工具类静态提取出来,防止创建1000次
 * @Date 2021/4/11 12:19
 * @Created by WangXiong
 */
public class ThreadLocalNormalUsage01 {

    public String date(int seconds){
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return sdf.format(date);
    }

    public static void main(String[] args) {

        for (int i = 0; i < 30; i++) {
            final int finalI = i;
            new Thread(new Runnable() {
                public void run() {
                    String date = new ThreadLocalNormalUsage01().date(finalI);
                    System.out.println(date);
                }
            }).start();
        }
    }
}

这个时候,我们发现,出现了问题。这是为啥呢?这是因为两个任务指向了同一个SimpleDateFormat对象,这个对象也不是线程安全导致的。

我们可以采用加锁(synchronize)来解决线程安全问题

但是我们使用了synchronize加锁,那1000个线程执行这个方法,他们是一个一个排队执行的,在高并发中,这显然是不可取的。

所以我们在这种情况下,应该使用ThreadLocal,下面我们来看下如何操作

/**
 * @Classname ThreadLocalNormalUsage05
 * @Description 利用ThreadLocal给每个线程分配自己的SimpleDateFormat对象,
 * 同时保证了线程安全,高效利用内存
 * @Date 2021/4/11 12:19
 * @Created by WangXiong
 */
public class ThreadLocalNormalUsage05 {

    public String date(int seconds){
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        // SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        SimpleDateFormat sdf = ThreadSafeFormatter.simpleDateFormatThreadLocal.get();
        return sdf.format(date);
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            final int finalI = i;
            executorService.submit(new Runnable() {
                public void run() {
                    //我们创建了1000个工具类对象,能否优化只创建一次?
                    String date = new ThreadLocalNormalUsage05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        executorService.shutdown();

    }
}

class ThreadSafeFormatter{
    public static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
}

使用ThreadLocal是线程安全的,且不会有性能问题

3、经典场景2

在我们的业务需求中,很多时候,都是需要用户信息的,这就导致我们在调用方法传递参数的时候,需要层层传递我们的user信息。

我们希望 每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦

那么我们是否可以设置一个全局静态变量,存储用户信息呢?

这是不行的,因为我们一个请求对应一个用户信息,第一个和第二个线程之间不能使用相同的用户对象

那我们是否可以顶一个map集合来存储呢?也是不行的,因为他不是map不是线程安全的,如果使用线程安全集合,那么或多或少还是会影响我们的性能

更好的方案是使用ThreadLocal,在线程生命周期内,都可以通过静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦

/**
 * @Classname ThreadLocalNormalUsage06
 * @Description ThreadLocal的用法2,避免传递参数的麻烦
 * @Date 2021/4/11 15:48
 * @Created by WangXiong
 */
public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        new Service1().process();
    }
}

class Service1 {
    public void process(){
        User user = new User("超哥");
        //将我们的user信息传递到ThreadLocal中
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}
class Service2 {
    public void process(){
        User user = UserContextHolder.holder.get();
        System.out.println("Service2:" + user.name);
        new Service3().process();
    }
}
class Service3 {
    public void process(){
        User user = UserContextHolder.holder.get();
        System.out.println("Service3:" + user.name);
    }
}

/**
 * @Description: 定义我们的ThradLocal持有的类,需要我们直接获取即可
 * @Param:
 * @returns:
 * @Author: WangXiong
 * @Date: 2021/4/11 15:52
 */
class UserContextHolder {
    public static ThreadLocal<User> holder = new ThreadLocal<User>();
}

class User {
    public String name;

    public User(String name) {
        this.name = name;
    }
}

4、ThreadLocal总结

  • 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
  • 在任何方法中都可以轻松获取到对象

根据共享对象的生成时机不同,选择initiaValue或set来保存对象

  • initiaValue:在ThreadLocal第一次get的时候把对象给初始化出阿里,对象的初始化时机可以由我们控制
  • set:如果我们需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set()进去,以便我们后续使用

使用ThreadLocal带来的好处

  • 达到线程安全
  • 不需要加锁,提高执行效率
  • 更高效的利用内存、节省开销:相比于每个任务都需要新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销
  • 免去传参的繁琐

5、ThreadLocal原理

我们一个Thread线程对应一个ThreadLocalMap,一个ThreadLocalMap里面有多个TreadLocal对象

5.1、initialValue()方法

  • 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发
  • 当线程第一个使用get()方法访问变量时,将调用此方法,除非线程当前调用了set()方法,在这种情况下,不会为线程调用initialValue()方法
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //如果线程调用了set方法,那么这个map就不为空
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    private T setInitialValue() {
        //调用我们重写的initialValue方法
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

    //如果不重写这个方法,默认返回null
    protected T initialValue() {
        return null;
    }
  • 每个线程只会调用一次initialValue方法,如果已经调用了remove后,在调用get,则可以再次调用此方法
  • 如果不重写这个方法,返回的就是null

5.2、set方法

为线程设置一个新值

5.3、get方法

得到这个线程对应的value。如果是首次调用get(),则会调用initialValue方法

5.4、remove方法

删除线程中这个对象

5.5、ThreadLocalMap

类似于HashMap,可以当成HashMap来理解

  • 键:这个ThreaLocal
  • 值:实际需要的成员变量,比如user对象

6、内存溢出

我们的ThreadLocalMap中的entry,我们可以看下他的构造方法

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                //使用弱引用进行赋值了
                super(k);
                //强引用,不会被垃圾回收
                value = v;
            }
        }

        public WeakReference(T referent) {
            super(referent);
        }

如果是弱引用,是可以被垃圾回收器回收的,但是我们的value是一个强引用,他是不会被回收的。

使用线程池,线程不终止,那么它的key对应的value就不能被回收,因为有以下的引用链

Thread -> ThreadLocalMap -> Entry(key为null) -> value

所以我们要在使用完value后手动remove,删除对应的entry对象

7、TreadLocal空指针

/**
 * @Classname ThreadLocalNPE
 * @Description 线程空指针
 * @Date 2021/4/12 21:41
 * @Created by WangXiong
 */
public class ThreadLocalNPE {

    ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>();

    public void set(){
        longThreadLocal.set(Thread.currentThread().getId());
    }

    public Long get(){
        return longThreadLocal.get();
    }

    public static void main(String[] args) {
        final ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        System.out.println(threadLocalNPE.get());
        new Thread(new Runnable() {
            @Override
            public void run() {
                threadLocalNPE.set();
                System.out.println(threadLocalNPE.get());
            }
        }).start();

    }
}

空指针异常是装箱拆箱导致的

8、小结

ThreadLocal中不要使用static对象,因为对象使静态的,如果不是线程安全的,那么我们使用ThreadLocal包装,还是无法保证线程安全。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值