Java并发工具学习(二)——简单说说ThreadLocal

前言

上一篇博客梳理了线程池的一些常用知识点,这一篇博客开始絮叨一下ThreadLocal,其实从整体上来说,ThreadLocal有两大使用场景:**1、每个线程之间为了避免出现线程安全,需要独享一个对象。2、每个线程内,不同的业务方法之前需要做到数据共享。**这两个使用场景将是我们总结ThreadLocal的切入点。

ThreadLocal——线程间独享一个对象

从SimpleDateFormat开始

如果要问SimpleDateFormat是不是线程安全的,可能大部分程序员都知道,其是线程不安全的。这个类通常用于我们进行日期格式的转换。其内部的parser函数,由于用到calender对象进行日期转换,相关源码并不是线程安全的,这个可以参考相关大牛的总结。我们这篇博客从SimpleDateFormat的使用开始,总结一下ThreadLocal的第一种使用场景

1、基础使用实例

/**
 * autor:liman
 * createtime:2021/11/7
 * comment:简单的多线程下的SimpleDateFormat实例
 * 两个线程打印,并没有出现问题
 */
@Slf4j
public class SimpleDateFormatDemo {
    
    

    public static void main(String[] args) {
    
    
        new Thread(()->{
    
    
            String date = new SimpleDateFormatDemo().dateFormat(10);
            System.out.println(date);
        }).start();
        new Thread(()->{
    
    
            String date = new SimpleDateFormatDemo().dateFormat(1000);
            System.out.println(date);
        }).start();
    }

    public String dateFormat(int seconds){
    
    
        //从1970-01-01 00:00:00开始计算
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }
}

两个线程打印日期格式,并没有啥问题。

如果这个时候,有10个线程都想用这个SimpleDateFormat的格式,指定日期输出,那我们该如何做?

2、十个线程使用同一个格式的SimpleDateFormat

/**
 * autor:liman
 * createtime:2021/11/8
 * comment:多个线程打印SimpleDateFormat转换的日期格式
 */
@Slf4j
public class SimpleDateFormatMultiThread {
    
    

    public static void main(String[] args) {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            int finalI = i;
            new Thread(()->{
    
    
                String date = new SimpleDateFormatMultiThread().dateFormat(finalI);
                System.out.println(date);
            }).start();
        }
    }

    public String dateFormat(int seconds) {
    
    
        //从1970-01-01 00:00:00开始计算
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);
    }

}

运行一下,好像也没有什么问题,因为每个线程都有自己的SimpleDateFormat对象

3、如果有1000个线程?

这个时候我们就要利用线程池了

/**
 * autor:liman
 * createtime:2021/11/8
 * comment:线程池运行SimpleDateFormat的内容
 */
@Slf4j
public class SimpleDateFormatThreadPool {
    
    
    private static ExecutorService threadPool = Executors.newFixedThreadPool(10);

    public static void main(String[] args) {
    
    
        for (int i = 0; i < 1000; i++) {
    
    
            int finalI = i;
            //每个线程都会创建SimpleDateFormat,相当于创建了1000个SimpleDateFormat对象
            threadPool.submit(new Thread(() -> {
    
    
                String date = new SimpleDateFormatThreadPool().dateFormat(finalI);
                System.out.println(date);
            }));
        }
        threadPool.shutdown();
    }
}

public String dateFormat(int seconds) {
    
    
    //从1970-01-01 00:00:00开始计算
    Date date = new Date(1000 * seconds);
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    return dateFormat.format(date);
}

运行一下,好像依旧没有问题,但是,1000个线程,创建了1000个SimpleDateFormat对象,这……也太暴力了。

共享的SimpleDateFormat

上面几个实例下来,每个线程创建SimpleDateFormat对象,并不会出现一些异常,但是SimpleDateFormat并没有必要每个线程都去创建,指定的格式都是一样,为什么不通过静态变量来实现这个逻辑?

/**
 * autor:liman
 * createtime:2021/11/8
 * comment:线程池共享 SimpleDateFormat
 */
@Slf4j
public class SimpleDateFormatStaticThreadPool {
    
    
    private static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    //共享SimpleDateFormat
    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    //记录重复数据
    private static Set<String> resultSet = new HashSet<>();

    public static void main(String[] args) {
    
    
        for (int i = 0; i < 10000; i++) {
    
    
            int finalI = i;
            threadPool.submit(new Thread(() -> {
    
    
                String date = new SimpleDateFormatStaticThreadPool().dateFormat(finalI);
                System.out.println(date);
            }));
        }
        threadPool.shutdown();
    }

    public String dateFormat(int seconds) {
    
    
        //从1970-01-01 00:00:00开始计算
        Date date = new Date(1000 * seconds);
        String result = dateFormat.format(date);
        if(resultSet.contains(result)){
    
    
            log.warn("{},出现重复数据",result);
        }
        resultSet.add(result);
        return result;
    }
}

可以看到,程序中我加入了一个集合,用于记录是否出现重复数据,运行结果如下

在这里插入图片描述

一堆重复数据,因为多个线程共享SimpleDateFormat,出现了线程安全问题。

加锁?

/**
 * autor:liman
 * createtime:2021/11/8
 * comment:加锁解决SimpleDateFormat的线程安全问题
 */
@Slf4j
public class SimpleDateFormatLockSlove {
    
    
    private static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    //共享SimpleDateFormat
    private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    //记录重复数据
    private static Set<String> resultSet = new HashSet<>();

    public static void main(String[] args) throws InterruptedException {
    
    
        for (int i = 0; i < 100; i++) {
    
    
            int finalI = i;
            threadPool.submit(new Thread(() -> {
    
    
                String date = new SimpleDateFormatLockSlove().dateFormat(finalI);
                System.out.println(date);
            }));
        }
    }

    public String dateFormat(int seconds) {
    
    
        //从1970-01-01 00:00:00开始计算
        Date date = new Date(1000 * seconds);
        String result = "";
        //加锁,锁住关键代码
        synchronized (SimpleDateFormatLockSlove.class) {
    
    
            result = dateFormat.format(date);
            resultSet.add(result);
        }
        return result;
    }
}

运行结果就正常了,很丝滑

用ThreadLocal优化一下

synchronized自然能解决,但是……这种排队运行的方式,是不是太慢了?如果高并发情况下,ThreadLocal就可以更好的解决这类问题,如果通过线程共享会有线程安全的问题,则ThreadLocal是一个不错的容器,可以保证每个线程内部有一个指定对象的副本。线程间的修改和操作就是隔离的。

/**
 * autor:liman
 * createtime:2021/11/8
 * comment:用ThreadLocal解决SimpleDateFormat的线程安全问题
 */
@Slf4j
public class SimpleDateFormatThreadLocal {
    
    
    private static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    //记录重复数据
    private static Set<String> resultSet = new HashSet<>();

    public static void main(String[] args) {
    
    
        for (int i = 0; i < 10000; i++) {
    
    
            int finalI = i;
            threadPool.submit(new Thread(() -> {
    
    
                String date = dateFormat(finalI);
                System.out.println(date);
            }));
        }
        threadPool.shutdown();
    }

    public String dateFormat(int seconds) {
    
    
        //从1970-01-01 00:00:00开始计算
        Date date = new Date(1000 * seconds);
        //直接调用ThreadLocal的get方法,获得其中的SimpleDateFormat对象
        String result = ThreadSafeSimpleDateFormat.dateFormatThreadLocal.get().format(date);
        if(resultSet.contains(result)){
    
    
            log.warn("{},出现重复数据",result);
        }
        resultSet.add(result);
        return result;
    }
}

//用一个内部类构造ThreadLocal
class ThreadSafeSimpleDateFormat{
    
    
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
    
    
        //复写ThreadLocal中的initialValue的方法,放入SimpleDateFormat对象
        @Override
        protected SimpleDateFormat initialValue() {
    
    
            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        }
    };
}

丝滑,没有线程安全问题。

支持lambda表达式

public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal
            = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));

ThreadLocal——同一个线程内部数据共享

在实际的web开发中,其实很多如下图的场景,当前请求有个很长的请求链,这个请求链上的方法,偶尔都会去操作用户数据,则需要一个公共的地方共享数据,并能做到同步,这个时候ThreadLocal就能完美的满足这个需求。

在这里插入图片描述

无需synchronized,无需ConcurrentHashMap,效率还很高,ThreadLocal就是这种场景的最优解。

实例代码

/**
 * autor:liman
 * createtime:2021/11/8
 * comment: 不断传递用户信息的问题
 */
@Slf4j
public class UserInfoProblem {
    
    

    public static void main(String[] args) {
    
    
        log.info("开始调用");
        ServiceOne serviceOne = new ServiceOne();
        serviceOne.process();
        log.info("调用结束");
    }
    
    
}

class ServiceOne{
    
    
    public void process(){
    
    
        User user = new User("牛哥");
        //这里手动setThreadLocal中的内容,没有调用initValue方法
        UserContextHolder.holder.set(user);
        System.out.println("service one 设置用户"+user);
        ServiceTwo serviceTwo = new ServiceTwo();
        serviceTwo.process();
    }
}

class ServiceTwo{
    
    
    public void process(){
    
    
        User user = UserContextHolder.holder.get();
        System.out.println("server two 中拿到的对象"+user);
        ServiceThree serviceThree = new ServiceThree();
        serviceThree.process();
    }
}

class ServiceThree{
    
    
    public void process(){
    
    
        User user = UserContextHolder.holder.get();
        System.out.println("server three 中拿到的对象"+user);
        //不用了要记得remove
        UserContextHolder.holder.remove();
    }
}

//Holder是持有者的意思,一些源码中经常看到Holder结尾的类,就是持有者的意思
class UserContextHolder{
    
    
    public static ThreadLocal<User> holder =
            new ThreadLocal<>();
}

class User{
    
    
    String name;

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

    public String getName() {
    
    
        return name;
    }

    public void setName(String name) {
    
    
        this.name = name;
    }

    @Override
    public String toString() {
    
    
        return "User{" +
                "name='" + name + '\'' +
                '}';
    }
}

走到这里,ThreadLocal的两个使用场景,我们已经总结完毕。但是……这不是结束

ThreadLocal的好处

1、可以达到线程安全目的

2、不需要加锁,执行效率比较高

3、更高效地利用内存,节省开销。至少不用每个线程都new一个SimpleDateFormat对象了

4、可以避免线程间共享数据的麻烦。

相关源码

其实在Thread类中,有一个threadLocals的属性,这个属性是ThreadLocalMap类型的

在这里插入图片描述

ThreadLocalMap中其实是一个Map类型的对象,这个Map的key值是ThreadLocal,value就是ThreadLocal中的值。也就是说,同一个线程中,其实允许存在多个ThreadLocal,如下图所示

在这里插入图片描述

ThreadLocal的get方法和setInitialValue方法

public T get() {
    
    
    //获取当前线程
    Thread t = Thread.currentThread();
    //获取线程中的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    
    //如果ThreadLocalMap不为空,则取出调用这个方法的ThreadLocal,并返回其value值
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
    
    
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //如果ThreadLocalMap为空,则调用setInitialValue方法
    return setInitialValue();
}

private T setInitialValue() {
    
    
    //这里就会去调用initialValue方法,这方法源码中直接返回null,因此子类要复写这个方法,达到初始化ThreadLocal中的值的目的。
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    
    
        map.set(this, value);
    } else {
    
    //如果当前ThreadLocalMap为空,则创建一个ThreadLocalMap
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
    
    
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

initialValue不重写,则直接返回null

ThreadLocal的set方法

//这个就简单了
public void set(T value) {
    
    
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    
    
        map.set(this, value);
    } else {
    
    
        createMap(t, value);
    }
}

ThreadLocal的remove方法

public void remove() {
    
    
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
    
    
        m.remove(this);
    }
}

最后说一下ThreadLocalMap,ThreadLocalMap是Thread类中的一个内部类,其实通过数组维护了ThreadLocal的Map表。相关源码如下

static class ThreadLocalMap {
    
    

    /**
     * The entries in this hash map extend WeakReference, using
     * its main ref field as the key (which is always a
     * ThreadLocal object).  Note that null keys (i.e. entry.get()
     * == null) mean that the key is no longer referenced, so the
     * entry can be expunged from table.  Such entries are referred to
     * as "stale entries" in the code that follows.
     */
    //这里用的是弱引用 Key是弱引用
    static class Entry extends WeakReference<ThreadLocal<?>> {
    
    
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
    
    
            super(k);
            value = v;
        }
    }
	
	private Entry[] table;
	
}

使用ThreadLocal的问题

无法回收的value

java开发者应该都听过内存泄漏的问题,内存泄漏,其实就是某个对象不再被使用,但是GC依旧无法回收这个对象占用的内存,日积月累,这些对象就会超过我们的内存限制,出现OOM的问题。如果ThreadLocal使用不当,这可能会出现这个问题

上面介绍的ThreadLocalMap的类中,Entry中key的构造,用到了弱引用。key用弱引用的构造函数给赋值了,用弱引用关联,说明其可以被垃圾回收器回收,如果某个时候,某个对象只被弱引用关联,则GC(垃圾回收器)会回收这个对象但是……ThreadLocalMap的Entry的value构造的时候,用的可是强引用

在正常情况下,线程运行终止,Thread对象被回收,自然其内部的ThreadLocalMap会被回收,同时ThreadLocalMap中的Entry对应的key和value都会被回收。但是,如果线程不终止,那么Entry中对应的value可能就很难被回收了,这个时候存在如下引用链

Thread->ThreadLocalMap->Entry(key为弱引用,可能被回收)->value

这样就导致value始终无法收回,GC只是收回了key。日积月累,就OOM了。

JDK其实已经考虑了这个问题,在set,remove,rehash方法中会扫描key为null的Entry,并将对应的Entry的value置为null,这样value就能正常被回收了。

但是,还有但是,我们如果忘了触发这几个方法,OOM依旧有可能发生,这也是阿里开发手册中,针对ThreadLocal的使用有一个强制的要求,要求ThreadLocal在不使用的时候,要手动remove

NPE问题

ThreadLocal中initialValue方法的源码如下

protected T initialValue() {
    
    
    return null;
}

由于这里方法声明的是一个泛型,因此需要注意装箱拆箱的NPE问题。

总结

ThreadLocal,两种场景,不用的时候务必手动remove,且需要注意装箱拆箱操作。同时需要说明的是在Spring中很多类似于ContextHolder结尾的,都或多或多和ThreadLocal有关系。

猜你喜欢

转载自blog.csdn.net/liman65727/article/details/121277521