ThreadLocal详解

1.使用场景

        1.每个线程需要一个独享的对象;

                通常是工具类,例如SimpleDateFormat工具类;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
 * 演示多线程情况下使用ThreadLocal处理线程安全问题且处理重复创建SimpleDateFormat对象问题;
 *      任务说明:
 *          10个线程去执行1000个格式化日期的任务
 */
public class ThreadLocalOne {

    public static void main(String[] args){
        // 固定10个线程
        ExecutorService pool = Executors.newFixedThreadPool(10);
        // 利用SET集合去重的特性判断线程是否有安全(即是否输出相同日期数据)
        HashSet set = new HashSet<>();
        // 固定1000个任务
        for (int i = 1; i <= 1000 ; i++) {
            int finalI = i;
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    // String dateStr = dateFormatOne(finalI);      // 无线程安全问题【此方式最多会创建任务数量个SimpleDateFormat对象】;
                    // String dateStr = dateFormatTwo(finalI);      // 有线程安全问题
                    // String dateStr = dateFormatThree(finalI);    // 无线程安全问题【此方式最多会创建一个SimpleDateFormat对象】
                    String dateStr = dateFormatFour(finalI);        // 无线程安全问题(推荐使用)【此方式最多会创建“线程池最大线程数”个SimpleDateFormat对象】;
                    System.out.println("当前打印的日期为:" + dateStr);
                    set.add(dateStr); // 将日期存储到SET集合中
                }
            });
        }
        pool.shutdown(); // 关闭线程池
        while (true){ // 循环检测线程池是否执行完毕
            if(pool.isTerminated()){ // 判断线程池是否执行完毕
                System.err.println("是否有线程安全问题(是否出现重复数据):" + !(1000 == set.size()));
                break; // 正在工作线程 + 工作队列任务都执行完毕则跳出检测
            }
        }
    }

    /**
     * 写法一:(常规写法->无线程安全问题)
     * 格式化日期函数
     *      每次线程来都创建一个新的SimpleDateFormat对象无线程安全问题,弊端在于多线程环境下多次调用将产生很多个SimpleDateFormat对象浪费内存;
     */
    public static String dateFormatOne(int seconds){
        Date date = new Date(1000 * seconds); // 该参数接收一个毫秒值,故在此将传入的秒*1000转为毫秒
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return  dateFormat.format(date);
    }


    /**
     * 写法二:(错误写法->有线程安全问题)
     *      写法一虽无线程安全问题但因为重复多次创建SimpleDateFormat对象造成了内存的浪费;所以尝试将SimpleDateFormat对象提出来在使用前创建一次;
     *      因为SimpleDateFormat类本身不是线程安全的所以多线程环境下会出现线程安全问题;
     */
    static SimpleDateFormat dateFormatTwo = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); // 事先声明一个SimpleDateFormat对象,后面其它线程都使用此对象去格式化
    public static String dateFormatTwo(int seconds){
        Date date = new Date(1000 * seconds); // 该参数接收一个毫秒值,故在此将传入的秒*1000转为毫秒
        return  dateFormatTwo.format(date);
    }


    /**
     * 写法三:(一般写法->无线程安全问题)
     *      通过加锁的方式确保线程安全,虽然通过加锁保证线程安全且只创建了一个SimpleDateFormat对象,但因为使用了“synchronized”导致了原本多线程并行执行现在变成串行执行(执行效率低)
     */
    static SimpleDateFormat dateFormatThree = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    public static String dateFormatThree(int seconds){
        Date date = new Date(1000 * seconds); // 该参数接收一个毫秒值,故在此将传入的秒*1000转为毫秒
        String dateStr = null;
        synchronized (ThreadLocalOne.class){
            dateStr = dateFormatThree.format(date);
        }
        return dateStr;
    }


    /**
     * 写法四:(较优写法(推荐)->无线程安全问题)
     *      通过ThreadLocal为线程池中固定的10个线程创建SimpleDateFormat对象,使用此方式是线程安全的因为此时线程池中的这10个线程都有一个自己独有的SimpleDateFormat对象,
     *      而且只用创建固定线程数的SimpleDateFormat对象节约了内存;
     */
    public static String dateFormatFour(int seconds){
        Date date = new Date(1000 * seconds); // 该参数接收一个毫秒值,故在此将传入的秒*1000转为毫秒
        SimpleDateFormat dateFormatFour = MakeThreadLoacl.dateFormatThreadLocal.get();
        return dateFormatFour.format(date);
    }
}


    /**
     * 通过ThreadLocal为每个线程创建一个SimpleDateFormat对象
     */
    class MakeThreadLoacl{
        // 此处的dateFormatThreadLocal对象被static修饰只会在当前类初始化的时候创建一次不会重复创建(就算重复调用MakeThreadLoacl.dateFormatThreadLocal返回的对象也是同一个地址值);
        public static ThreadLocal dateFormatThreadLocal = new ThreadLocal(){
            // 重写initialValue方法的目的在于创建ThreadLocal对象时就对其初始化格式:yyyy-MM-dd hh:mm:ss
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
            }
        };
    }

        2.每个线程内需要保存全局变量;

                比如在拦截器中获取用户信息并保存到ThreadLocal中,保存在ThreadLocal中的用户信息在本次请求的“任何方法”中都可以获取到这个用户信息。除了当前线程新开一个子线程去执行其它业务那么在这个子线程中是不能获取到上面存储的用户信息的,因为ThreadLocal是将当前线程作为Key去存取数据的,如果新开一个线程因为保存用户信息的线程和新开的线程不是同一个线程所以在新开的线程中是获取不到这个用户信息的,除非你将当前保存用户信息的线程也传递到子线程业务中使用保存用户信息的线程去获取用户信息;

/**
 * 演示在当前线程执行的所有业务方法中共享“用户信息”
 *
 *  调用链路:
 *      1.业务一存储User信息,并调用业务二;
 *      2.业务二获取业务一存储的User信息并打印,且调用业务三
 *      3.业务三获取业务一存储的User信息并打印
 */
public class ThreadLocalTwo {
    // 程序入口
    public static void main(String[] args) {
        Service1 service = new Service1();
                 service.process();
    }
}


/**
 * 业务一
 *      用于存放User信息到ThreadLocal中供后续Service2,Service3获取这个用户信息
 */
class Service1{
    public void process(){
        // 构建用户信息
        User user = new User("张学友",56);
        // 将用户信息存储在ThreadLocal中
        UserContextHolder.holder.set(user);
        // 调用业务二
        new Service2().process();
    }
}


/**
 * 业务二
 *      获取业务一中存储在ThreadLocal中的User用户信息
*/
class Service2{
    public void process(){
        // 获取业务一中存储的User对象信息
        User user = UserContextHolder.holder.get();
        // 打印获取到的用户信息
        System.out.println("业务二中获取到业务一中存储的User对象信息:" + user);
        // 调用业务三
        new Service3().process();
    }
}


/**
 * 业务三
 *      获取业务一中存储在ThreadLocal中的User用户信息
 */
class Service3{
    public void process(){
        // 获取业务一中存储的User对象信息
        User user = UserContextHolder.holder.get();
        // 打印获取到的用户信息
        System.out.println("业务三中获取到业务一中存储的User对象信息:" + user);
    }
}


/**
 * 提供一个ThreadLocal对象用于存储多线程环境下保存/获取User实体的载体
*/
class UserContextHolder{
     // 被static修饰此对象只会实例化一次
     static ThreadLocal holder = new ThreadLocal();
}


/**
 * 用于存储共享的“用户信息”实体
 */
class User{
    private String name;
    private int age;

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

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

ThreadLocal两种设置值的区别?

           如果需要保存到ThreadLocal中对象的生成时机由我们随意控制可以像场景一那样重写initialValue方法去设置值,如果需要保存到ThreadLocal中对象的生成时机不由我们随意控制则我们可以使用场景二的方式使用set设置值;

2.ThreadLocal作用

        1.让某个需要的对象在线程间隔离;

        2.在“任何方法”中都可以轻松的获取到存储的对象;

3.ThreadLocal优点

        1.线程安全;

        2.无需加锁执行效率高;

                本章场景一使用“synchronized”加锁控制创建SimpleDateFormat对象数量(劣势加锁并行变串行);

        3.节省内存消耗;

                本章场景一使用ThreadLocal降低创建SimpleDateFormat对象数量;

        4.方便参数传递代码解耦;

4.Thread&ThreadLocalMap&ThreadLocal关系

        如需明白ThreadLocal原理及各方法,需先了解清楚Thread,ThreadLocalMap,ThreadLocal之间的关系;

        每个Thread中都有一个ThreadlLocalMap成员变量该成员变量中有一个键值对数组Entry[] table用于存放数据。ThreadLocalMap用于存储多个ThreadLocal,存储在ThreadLocalMap中的数据格式为K-V键值对,其中ThreadLocal引用作为存储在ThreadlLocalMap中的Key,Value为存储在ThreadLcoal中具体的值,当ThreadLocalMap存储数据时发生Hash冲突它采用“线性探测法”,也就是说如果发生冲突就继续找下一个空位去存储数据,而不是像HashMap那样使用链表去存储冲突的数据;

5.ThreadLocal原理

        ThreadLocal是以键值对的形式去存储数据的每个Thread都持有一个ThreadLocalMap成员变量用于存储多个ThreadLocal对象;

补充:

        可使用第6节ThreadLocal重要方法详解中的set/get方法原理作为补充;

6.ThreadLocal重要方法详解

        1.initialValue

                1.1.方法返回当前线程的“初始值”该方法为延迟加载方法当线程首次调用get方法时且该ThreadLocal对象没有被set过值时才会触发调用initialValue方法;

                1.2.每个线程最多调用一次initialValue方法但如果调用了remove方法后再调用get方法则此时会再执行initialValue方法;

                1.3.如果不重写initialValue方法该方法会返回null,一般像场景一那样使用匿名内部类的方式来重写initialValue方法;

        2.set

                用途:

                        为线程设置一个新的值;

                原理:

                        set方法先获取当前线程的ThreadLocalMap对象(因为ThreadlLocal存放在线程的ThreadLocalMap对象中),再判断当前ThreadLocalMap对象是否为空,非空则覆盖设置值,为空则调用createMap方法创建ThreadLocalMap对象并设值;

                源码:

# ThreadLocal set 方法源码
    public void set(T value) {
        Thread t = Thread.currentThread(); // 获取当前线程对象
        ThreadLocalMap map = getMap(t); // 将当前线程对象传入调用getMap方法获取到当前线程的ThreadLocalMap对象
        if (map != null) // 如果ThreadLocalMap对象不为空则直接覆盖值
            map.set(this, value); // 此处的this指代当前ThreadLocal对象
        else
            createMap(t, value); // 如果ThreadLocalMap对象为空则创建ThreadLocalMap对象并设值
    }

        3.get

                用途:

                        得到线程在ThreadLocal中设置的value。如果是首次调用get方法且该ThreadLocal对象没有被set过值则会调用initialValue来初始化这个值;

                原理:

                        get方法先获取当前线程的ThreadLocalMap对象(因为ThreadlLocal存放在线程的ThreadLocalMap对象中),然后调用map.getEntry方法,把当前ThreadLocal的引用作为参数传入获取到对应的Entry,最后再从Entry中取值;                     

                源码:

# ThreadLocal get 方法源码
    public T get() {
        Thread t = Thread.currentThread(); // 获取当前线程对象
        ThreadLocalMap map = getMap(t); // 把当前线程作为参数调用getMap()方法去获取当前线程的ThreadLocalMap(每个线程都有一个ThreadLocalMap用于存放ThreadLocal对象,这里的map数据都是存放在当前线程中的)
        if (map != null) { // 如果获取到的ThreadLocalMap有值说明当前ThreadLocal已调用“InitialValue”完成了初始化或者当前的ThreadLocal已调用过“set”方法赋值
            ThreadLocalMap.Entry e = map.getEntry(this); // 此处的this指代当前的ThreadLocal
            if (e != null) { // 如果有值则返回数据
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue(); // 如果获取到的ThreadLocalMap为空则调用已调用“setInitialValue”方法进行初始化
    }

        4.remove

                用途:

                        删除线程保存的值;

# 在场景二中调用remove方法后,后续都拿不到保存在ThreadLocal中的User信息
UserContextHolder.holder.remove();

              原理:

                        remove方法先获取当前线程的ThreadLocalMap对象(因为ThreadlLocal存放在线程的ThreadLocalMap对象中),如果获取到的ThreadLocalMap对象不为空则将当前ThreadLcoal对象的引用作为“Key”去删除数据;

             源码:

# ThreadLocal remove方法源码
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread()); // 当前线程作为参数获取当前线程的ThreadLocalMap对象
         if (m != null) // 如果ThreadLocalMap不为空
             m.remove(this); // this指代当前ThreadLocal对象,将当前ThreadLocal对象的引用作为“Key”去删除数据
     }

7.ThreadLocal注意点

1.内存泄露

        内存泄露指某个对象不再使用但是该对象占用的内存不能被JVM回收;

1.1.ThreadLocal出现内存泄露的原因?

        ThreadLcoal数据存储在ThreadLocalMap中而ThreadLocalMap又是存储在每个线程里的(即存储在ThreadLocal中数据最终存储在线程中),如果此时有线程执行完业务但没有被销毁(比如线程池中线程执行完业务后又返回到线程中),该线程还一直存活那么保存在线程内的ThreadLocalMap及ThreadLocal不会被GC回收(强引用),这些存储在线程中的数据不会再被使用,算是无用数据此时只要有足够多的线程或存储在ThreadLocalMap中的数据足够大就有造成内存泄露问题的可能并发生“内存溢出”;

1.2.解决ThreadLocal内存泄露问题

        在使用完ThreadLocal后可以调用remove方法删除保存在线程中的数据。比如在拦截器中“前置拦截器”用于保存数据到ThreadLocal(ThreadLocal最终将数据保存到当前线程中),在“后置拦截器”中调用remove方法删除保存在ThreadLocal中的数据;

2.空指针异常

        使用ThreadLocal在线程A中设值,再重线程B中去获取值是获取不到的,因为ThreadLocal在设置值时是将数据存储在线程A的ThreadLocalMap中,而取值时是在线程B中的ThreadLocalMap中取值的,此时因为线程B的ThreadLocalMap是空的所以在线程B中取值为null;

原因:

        线程A调用set方法时此时源码中获取到的当前线程对象是“线程A”所以数据存在线程A的ThreadLocalMap中。线程B调用get方法时此时源码中获取到的当前线程是“线程B”所以是重“线程B”的ThreadLocalMap中去获取值,而“线程B”的ThreadLocalMap未被初始化为空所以返回null。

/**
 * 演示使用ThreadlLocal在线程A中设值从线程B中获取值为null情况
 */
public class ThreadLocalThree {
    public static void main(String[] args) {
        new ServiceOne().process();
    }
}

/**
 * 业务方法一:
 *      1.验证ThreadLocal未重写initialValue方法且未先set值直接使用get获取时返回为null
 *      2.往ThreadLocal中设置Product
 */
class ServiceOne{
    public void process(){
        // 验证ThreadLocal未重写initialValue方法且未先set值直接使用get获取时返回为null
        Product product = ProductContextHolder.holder.get();
        System.out.println("验证ThreadLocal未重写initialValue方法且未先set值直接使用get获取时返回为null:" + product); // 打印 null

        // 往ThreadLocal中设置Product
        ProductContextHolder.holder.set(new Product("001","王小虎"));
        new ServiceTwo().process(); // 调用业务方法二
    }
}

// 业务方法二
class ServiceTwo{
    public void process(){
        Product product = ProductContextHolder.holder.get();
        System.out.println("业务方法二中获取到了业务方法一中设置的Product数据:" + product); // 打印 Product{id='001', name='王小虎'}

        // 尝试新开一个线程,在子线程中去获取数据(获取结果为null)
        new Thread(new Runnable() {
            @Override
            public void run() {
                Product product = ProductContextHolder.holder.get();
                System.out.println("业务方法二中尝试获取业务方法一中设置的Product数据:" + product); // 打印 null
            }
        }).start();
    }
}

// ThreadLocal用于存储Product数据
class ProductContextHolder{
    static ThreadLocal holder = new ThreadLocal();
}

// 数据实体
class Product{
    private String id;
    private String name;

    public Product(String id, String name) {
        this.id = id;
        this.name = name;
    }
    @Override
    public String toString() {
        return "Product{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                '}';
    }
}

3.共享对象

        如果在多个线程中使用ThreadLocal.set()进去的对象本来就是多线程共享的对象如被static修饰的对象,那么多个线程使用ThreadLocal.get()取得的这个共享对象还是有并发安全问题;

错误用法:

        用ThreadLocal去共享一个被static修饰的对象;

8.ThreadLocal在Spring中的应用

        1.RequestContextHolder;

        2.DaetTimeContextHolder

你可能感兴趣的:(多线程,java,jvm,开发语言)