【并发基础】一篇文章带你快速掌握ThreadLocal及其原理

40 篇文章 7 订阅
14 篇文章 32 订阅

目录

一、ThreadLocal简介

二、ThreadLocal的用途

2.1 典型场景1 :每个线程需要一个独享的对象

SimpleDateFormat的进化之路:

2.2 典型场景3 :当前用户信息需要被线程内所有方法共享

三、ThreadLocal的作用

3.1 使用ThreadLocal带来的好处

四、ThreadLocal的原理

4.1 数据结构

4.1.1 早期ThreadLocal设计对比

4.2 ThreadLocal的主要方法及源码分析

4.2.1 主要方法

4.2.2 T initialValue()方法

4.2.3 get()方法

4.2.4 set()方法

4.2.5 T remove()方法

4.3 ThreadLocalMap类

4.3.1 解决冲突

4.4 两种使用场景殊途同归

五、ThreadLocal注意点

5.1 内存泄漏

5.1.1 Value的泄漏

5.1.2 如何避免内存泄露(阿里规约)

5.2 空指针

5.3 共享对象

5.4 不需要强行使用ThreadLocal

5.5 优先使用框架的支持

5.5.1 Date TimeContextHolder类

5.5.2 RequestContextHolder类


一、ThreadLocal简介

在JDK1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。

ThreadLocal的作用是提供线程内的局部变量,在多线程环境下访问时能保证各个线程内的ThreadLocal变量各自独立。也就是说,每个线程的ThreadLocal变量是自己专用的,其他线程是访问不到的,对自己的ThreadLocal变量也不会影响其他线程的ThreadLocal变量。

二、ThreadLocal的用途

两大使用场景:

  • 典型场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random )。换句话说就是,多线程环境下需要对非线程安全对象的并发访问,并且该对象并不需要在线程间共享,只给每个线程自己使用即可,但是我们不想加锁,因为加锁很影响性能,降低并发量,这时候可以使用ThreadLocal来使得每个线程都持有一个该对象的副本。
  • 典型场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息) , 可以让不同方法直接使用,避免参数传递的麻烦。比如我们在登陆时从拦截器获取到了用户的姓名,而在后面的的很多操作中可能还会用到这个名字,如果我们每一个方法都传参传入姓名的话就非常麻烦,不如直接把姓名这个数据存在这个请求线程内部,这样想什么时候用直接取出来即可。

2.1 典型场景1 :每个线程需要一个独享的对象

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

SimpleDateFormat的进化之路

v0.1

/**
 * 描述:     两个线程打印日期
 */
public class ThreadLocalNormalUsage00 {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(10);
                System.out.println(date);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                String date = new ThreadLocalNormalUsage00().date(104707);
                System.out.println(date);
            }
        }).start();
    }

    /**
     * 将传入的秒转化为日期字符串
     * @param seconds  单位秒
     * @return
     */
    public String date(int seconds) {
        //Date()入参的单位是毫秒,从1970.1.1 00:00:00 GMT计时,传入多少毫秒,就从这个日期上往上累加
        Date date = new Date(1000 * seconds);
        // 设置使其格式化工具
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        // 格式化输出字符串日期
        return dateFormat.format(date);
    }
}

两个线程使用同一个SimpleDateFormat结果还算是正常,都可以正常输出。我们再来看一下n个线程使用同一个SimpleDateFormat会出现什么情况。

v0.2

/**
 * 描述:     30个线程打印日期
 */
public class ThreadLocalNormalUsage01 {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 30; i++) {
            int finalI = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage01().date(finalI);
                    System.out.println(date);
                }
            }).start();
            Thread.sleep(100);
        }
    }

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

这样写依旧是可以正常运行,但是这里还有一点性能上的问题,如果需要的线程很多,比如增加到1000个,向上面这个写法就需要循环创建销毁1000次线程对象,这个过程是非常消耗性能的,所以我们引入线程池来优化线程管理。

v0.3

/**
 * 描述:     1000个打印日期的任务,用线程池来执行
 */
public class ThreadLocalNormalUsage02 {
    // 如果线程很多的话,再像之前那样循环创建线程对象就会带来很大的开销了,创建销毁线程对象开销是很大的,这里我们就可以引入线程池来帮我们完成线程的创建管理减少开销
    // 创建有10个线程的线程池,让着10个线程去完成这1000个任务
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            // 将线程执行代码直接传入线程池,让线程池帮我们运行
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage02().date(finalI);
                    System.out.println(date);
                }
            });
        }
        // 关闭线程池
        threadPool.shutdown();
    }

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

这个代码还是可以正常运行的,但是也有一个问题,就是虽然Thread对象不需要重复创建销毁1000次了,但是在date()方法中还是需要创建SimpleDateFormat对象,这个date()需要被调用1000次,那么就会创建1000次SimpleDateFormat对象,这样开销依旧很大,所以我们可以让所有的线程都共用同一个simpleDateFormat对象,这样就能避免频繁的创建对象了,并且共用一个对象看起来也不会有什么影响,我们只是用这个对象来格式化日期格式而已。

v0.4

/**
 * 描述:     1000个打印日期的任务,用线程池来执行
 */
public class ThreadLocalNormalUsage03 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    // 创建静态全局变量,让所有线程共用这一个SimpleDateFormat对象
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage03().date(finalI);
                    System.out.println(date);
                }
            });
        }
        // 关闭线程池
        threadPool.shutdown();
    }
    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        return dateFormat.format(date);
    }
}

这个代码执行结果就出现了问题,出现了多次打印相同时间的情况,这种情况本不应该发生的。这是因为SimpleDateFormat本身是线程不安全的,所以当多个线程同时使用同一个SimpleDateFormat对象是,就会出现并发安全问题。

我们可以通过加锁来解决线程安全问题。

v0.5

/**
 * 描述:     加锁来解决线程安全问题
 */
public class ThreadLocalNormalUsage04 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage04().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
        String s = null;
        // 使用synchronized加类锁来保证线程安全
        synchronized (ThreadLocalNormalUsage04.class) {
            s = dateFormat.format(date);
        }
        return s;
    }
}

加了锁之后程序就可以正常运行了,没有线程安全问题了

但是最后通过加锁来解决线程安全问题后,就会导致1000个线程需要依次排队获取simpleDateFormat对象,会导致性能下降,吞吐量降低。所以我们就需要引入ThreadLocal来解决这个问题。

锁和ThreadLocal使用场景是有区别的,具体区别如下:

使用ThreadLocal来进行优化

v1.0

我们可以给线程池中的10个线程各自创建一个他们自己独有的simpleDateFormat对象,这样既能够避免线程安全问题,也能够避免多次重复创建simpleDateFormat对象带来的性能损耗。

/**
 * 描述:     利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
 */
public class ThreadLocalNormalUsage05 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage05().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }
    public String date(int seconds) {
        //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时
        Date date = new Date(1000 * seconds);
//        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        // 获取ThreadLocal中的SimpleDateFormat对象。这个对象在每个线程中只有独有的一份,这里使用get()方法就会自动获取当前线程所持有的ThreadLocal中的SimpleDateFormat对象
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
        return dateFormat.format(date);
    }
}

// 使用ThreadLocal来进行优化
class ThreadSafeFormatter {
    // 创建ThreadLocal,并且覆写初始化方法initialValue()  这里将dateFormatThreadLocal设置为静态变量,就可以随时取用
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            // 返回想要存入ThreadLocal的对象
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    // 使用lambda表达式来简化写法
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
}

SimpleDateFormat的进化之路

  1. 2个线程分别用自 己的SimpleDateFormat ,这没问题
  2. 后来延伸出10个 ,那就有10个线程和10个SimpleDateFormat ,这虽然写法不优雅(应该复用对象),但勉强可以接受
  3. 但是当需求变成了1000个,频繁的创建销毁线程对象会带来巨大消耗,那么必然要用线程池(否则消耗内存太多)
  4. 所有的线程都共用同一个simpleDateFormat对象,减少对象开销。
  5. 共用一个simpleDateFormat对象线程不安全的,出现了并发安全问题
  6. 我们可以选择加锁 ,加锁后结果正常,但是效率低
  7. 在这里更好的解决方案是使用ThreadLocal,因为ThreadLocal是每个线程独享的,不会在多个线程之间共享,也就不会产生线程安全问题,并且ThreadLocal也不会像加锁一样影响性能

2.2 典型场景3 :当前用户信息需要被线程内所有方法共享

一个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传到service-2(),再从service-20传到service-3()。以此类推,但是这样做会导致代码冗余且不易维护。

所以我们可以在每个线程内保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦。并且也能保证每个线程是维护的自己的变量,和其他线程的变量区分开。这也就是通过ThreadLocal完成了一个上下文设计模式

实现方法:

  • 用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取到的用户名、user ID等)
  • 这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的
  • 在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦
  • 强调的是同一个请求内(同一个线程内)不同方法间的共享
  • 不需重写initialValue()方法,但是必须手动调用set()方法

/**
 * 描述:     演示ThreadLocal用法2:避免传递参数的麻烦
 */
public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        new Service1().process("");
    }
}

// 获取用户姓名
class Service1 {
    public void process(String name) {
        // 获取用户姓名
        User user = new User("超哥");
        // 使用set()方法将用户姓名写入当前线程的ThreadLocal中
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    public void process() {
        // 获取到当前线程ThreadLocal中存储的用户姓名
        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);
        UserContextHolder.holder.remove();
    }
}

// 创建用来获取当前线程ThreadLocal的类
class UserContextHolder {
    // 设置静态变量,这里只需要new ThreadLocal即可,不需要在设置初始化方法了,
    // 因为在这个应用场景中,ThreadLocal中的数据并不是在创建ThreadLocal就存储进去的
    // 而是在调用方法的过程中才会写入到ThreadLocal种
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

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

三、ThreadLocal的作用

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

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

场景一: initialValue

如果我们能够确定在每个线程中要存储使用哪些对象,就可以用使用这个方法在初始化ThreadLocal对象的时候就将数据存入ThreadLocal中。在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制

场景二:set

如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,就用

ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用。

只要让每个线程独有对象,而不再不同的线程之间共享对象,就不会有线程安全的问题了

3.1 使用ThreadLocal带来的好处

  • 达到线程安全
  • 不需要加锁,提高执行效率
  • 更高效地利用内存、节省开销:相比于每个任务都新建一个SimpleDateFormat ,显然用Threadlocal可以节省内存和开销
  • 免去传参的繁琐:无论是场景一的工具类,还是场景二的用户名,都可以在任何地方直接通过ThreadLocal拿到,再也不需要每次都传同样的参数。ThreadLocal使得代码耦合度更低,更优雅

四、ThreadLocal的原理

4.1 数据结构

想学习原理,首先需要搞清楚Thread 、ThreadLocal以及ThreadLocalMap三者之间的关系:

每个Thread对象中都持有一个ThreadLocalMap成员变量,ThreadThreadLocalMap是一对一的关系,而ThreadLocalMap可以理解成一个Map(但实际并不是Map类),用来存储该线程所有持有所有ThreadLocal,因为一个线程可能有多个ThreadLocal对象,用来获取不同的对象。ThreadLocal对象和要存储进这个ThreadLocal中的对象数据也是一对一的关系,一个ThreadLocal只能存储进一个对象,如果想要存储多个不同的对象,就需要创建多个不同的ThreadLocal来存储在ThreadLocalMap中。

public class Thread implements Runnable {
    ...
    // Thread中成员属性ThreadLocalMap,用来存储该线程所持有的所有ThreadLocal对象
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
}


// ThreadLocalMap是ThreadLocal中的一个静态内部类
static class ThreadLocalMap {
    private Entry[] table;
    ...
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    ...
}

数据结构关系如下图所示

4.1.1 早期ThreadLocal设计对比

在JDK早期的设计中,每个ThreadLocal都有一个map对象,将线程作为map对象的key,要存储的变量作为map的value,如上一节所讲,和现在的ThreadLocal设计大不相同。

JDK8之后,每个Thread维护一个ThreadLocalMap对象,这个Map的key是ThreadLocal实例本身,value是存储的值要隔离的变量,是泛型,其具体过程如下:

  1. 每个Thread线程内部都有一个Map(ThreadLocalMap::threadlocals);
  2. Map里面存储ThreadLocal对象(key)和线程的变量副本(value);
  3. Thread内部的Map由ThreadLocal维护,由ThreadLocal负责向map获取和设置变量值;
  4. 对于不同的线程,每次获取副本值时,别的线程不能获取当前线程的副本值,就形成了数据之间的隔离。

JDK8之后设计的好处在于:

  • 每个Map存储的Entry的数量变少,在实际开发过程中,ThreadLocal的数量往往要少于Thread的数量,Entry的数量减少就可以减少哈希冲突。
  • 当Thread销毁的时候,ThreadLocalMap也会随之销毁,ThreadLocal也就会跟着销毁,减少内存使用,早期的ThreadLocal并不会自动销毁。

4.2 ThreadLocal的主要方法及源码分析

4.2.1 主要方法

  • T initialValue( ):初始化
  • void set(T t):为这个线程设置一个新值
  • T get():得到这个线程对应的value。如果是首次调用get() ,则会调用initialize来得到这个值
  • void remove( ):删除对应这个线程的值

4.2.2 T initialValue()方法

initialValue方法没有默认实现的,如果我们要用initialValue方法,需要自己实现,通常是匿名内部类的方式

// ThreadLocal.initialValue()
// 该方法需要重写,否则ThreadLocal只会返回null
protected T initialValue() {
    return null;
}
  1. 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发
  2. 当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法,这正对应了ThreadLocal的两种典型用法
  3. 通常每个线程最多调用一次此方法,但如果已经调用了remove()后,再调用get(),则可以再次调用此方法
  4. 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。

4.2.3 get()方法

get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value。注意,这个map以及map中的key和value都是保存在Thread线程对象中的,而不是保存在ThreadLocal中。

// ThreadLocal.get()
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 如果当前的map不是空,说明该线程已经有ThreadLocal调用过get()被初始化过了
    if (map != null) {
        // 
        /**
         * 从ThreadLocalMap中获取当前ThreadLocal的value值
         * 这里就类似于Map,传入key值,然后去获取对应的value
         * 这里传入的是this,这个get()方法是在ThreadLocal类当中的,那么这个this指的就是当前这个ThreadLocal对象,进而就能获取到存储到该ThreadLocal中的数据了
         */
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果不为空则返回value,如果当前ThreadLocal在map中没有数据,则还会执行该方法最后的setInitialValue()方法进行初始化
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            // 返回存储到当前线程中的该ThreadLocal的值
            return result;
        }
    }
    // 如果当前的map是空,说明该线程还没有初始化过ThreadLocal,则调用初始化方法
    return setInitialValue();
}

// ThreadLocal.getMap()
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// ThreadLocal.setInitialValue()
private T setInitialValue() {
    // 调用初始化方法,一般需要重写initialValue()方法,这样initialValue()就能初始化返回自己指定类型的对象
    T value = initialValue();
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 如果map已经创建了,则将当前ThreadLocal和要存入当前ThreadLocal的值作为key-value对存入ThreadLocalMap
    if (map != null)
        map.set(this, value);
    // 如果当前线程还没有创建,则为当前线程创建ThreadLocalMap,并将ThreadLocal和要存入当前ThreadLocal的值存入
    else
        createMap(t, value);
    
    // 返回初始化ThreadLocal的值
    return value;
}

 

4.2.4 set()方法

该方法和setInitialValue()方法很类似

// ThreadLocal.set()
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 如果当前线程已经创建ThreadLocalMap了,则将当前ThreadLocal和要存入当前ThreadLocal的值作为key-value对存入ThreadLocalMap,如果该ThreadLocal以前存储过值了,则这个操作就会将原有的值覆盖掉
    if (map != null)
        map.set(this, value);
    // 如果当前线程还没有创建,则为当前线程创建ThreadLocalMap,并将ThreadLocal和要存入当前ThreadLocal的值存入
    else
        createMap(t, value);
}

 

4.2.5 T remove()方法

// ThreadLocal.remove()
public void remove() {
    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 删除map中以当前ThreadLocal为key的键值对
    if (m != null)
        m.remove(this);
}

 

4.3 ThreadLocalMap

ThreadLocalMap类,它是ThreadLocal中的是个内部类。该类的对象是存储在Thread中的,即Thread.threadLocals

ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map键值对(但实际上ThreadLocalMap类并不是一个Map类型):

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

4.3.1 解决冲突

在冲突解决上,ThreadLocalMap和之前我们讲过的HashMap还是有一些区别的

HashMap在JDK1.8之后如果出现冲突就是采用拉链法,在有冲突的位置创建一个链表,将冲突节点都追加到链表中,当链表达到一定长度后则会将链表转换为红黑树,这就是HashMap解决冲突的方法

而ThreadLocalMap这里采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用拉链法或者创建红黑树来解决冲突。

4.4 两种使用场景殊途同归

学完源码后,我们再返回去看最开始讲的两种应用场景,通过源码分析可以看出,使用setInitialValue和直接set最后都是利用map.set()方法来设置值。也就是说,最后都会对应到ThreadLocalMap的一个Entry,只不过是起点和入口不一样,场景一的起点是get()方法,场景二的起点是set()方法。

五、ThreadLocal注意点

5.1 内存泄漏

什么是内存泄漏:某个对象不再使用,但是占用的内存却不能被回收

ThreadLocalMap中的Entry(key)继承自WeakReference,是弱引用。

// ThreadLocal.ThreadLocalMap.Entry
// 这里Entry继承自弱引用的ThreadLocal
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        // 调用继承父类的构造方法,也就是将ThreadLocal设置成了弱引用
        super(k);
        // 像这种普通的赋值,就是强引用
        value = v;
    }
}

 弱引用的特点是,如果这个对象只被弱弓引用关联(没有任何强引用关联) ,那么这个对象就可以被回收,也就是说就算是有一个弱引用指向对象,GC也会无视掉这个引用,还是将这个对象看成不可达对象,会将其回收掉。所以弱引用不会阻止GC。

5.1.1 Value的泄漏

ThreadLocalMap 的每个Entry都是一个对key的弱引用,同时,每个Entry都包含了一个对value的强引用。正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了,因为当线程中止之后,整个ThreadMap对象就会被回收,进而ThreadMap中存储的所有key-value对也都会被回收。

但是,如果线程不终止(比如线程需要保持很久,或者我们在用线程池的时候),那么key对应的value就不能被回收,因为有以下的调用链:Thread一> ThreadLocalMap一> Entry ( key为null )一>Value。

因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,随着线程ThreadLocal的增多就可能会出现OOM错误。

JDK已经考虑到了这个问题,所以在ThreadLocalMap中的set、remove、rehash等方法中会扫描key为null的Entry,并把对应的value设置为null,这样value指向的对象就变成了不可达对象了,也就可以被回收了

源码例子:

// ThreadLocal.ThreadLocalMap.resize()
private void resize() {
    Entry[] oldTab = table;
    int oldLen = oldTab.length;
    int newLen = oldLen * 2;
    Entry[] newTab = new Entry[newLen];
    int count = 0;
    for (int j = 0; j < oldLen; ++j) {
        Entry e = oldTab[j];
        if (e != null) {
            ThreadLocal<?> k = e.get();
            // 在扩容的时候会遍历判断key是不是为空,如果为null,则将其对应的value指向为空,这样就可以回收已经不用的对象了
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }
    setThreshold(newLen);
    size = count;
    table = newTab;
}

但是如果一个ThreadLocal不被使用,那么实际上set、remove、rehash方法也不会被调用、如果同时线程又不停止、那么调用链就一直存在、那么就导致了value的内存泄漏,这种情况下就需要我们在开发过程中严格遵守开发规范。

5.1.2 如何避免内存泄露(阿里规约)

调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法。比如这个线程即将结束的时候,再使用拦截器将线程拦截,将它的ThreadLocal给remove掉即可。

5.2 空指针

在进行get之前,必须先set,否则可能会返回NULL,并且返回类型如果不是对象类型,而是基本数据类型的话,还会报空指针异常,最好是用包装类来替代基本数据类型。

5.3 共享对象

如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是会有并发访问问题。所以一定要避免向ThreadLocal中存储static全局变量,这样依旧会导致并发异常。

5.4 不需要强行使用ThreadLocal

如果可以不使用ThreadLocal就解决问题,那么不要强行使用

  • 例如在任务数很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal,因为这种情况下使用ThreadLocal带来的收益并不大,反而还会增加任务量

5.5 优先使用框架的支持

例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏。

以Spring为例:

5.5.1 Date TimeContextHolder

package org.springframework.format.datetime.standard;
public final class DateTimeContextHolder {
    // 将时间的上下文存储到ThreadLocal中,这样每一个线程就会拥有自己独立的时间设置
    private static final ThreadLocal<DateTimeContext> dateTimeContextHolder = new NamedThreadLocal("DateTimeContext");
    ...
}

这里就是Spring提供的ThreadLocal支持

5.5.2 RequestContextHolder

为每一个请求都设置一个ThreadLocal。每次HTTP请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型应用场景。

package org.springframework.web.context.request;
public abstract class RequestContextHolder {
    private static final boolean jsfPresent = ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
    // 这里的NameThreadLocal就是ThreadLocal,知识spring在ThreadLocal的基础上又加了一个name属性,用来标识Thread名称
    private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");
    public RequestContextHolder() {
    }
    ...
}

 相关文章:【并发编程】synchronized关键字最全详解,看这一篇就够了
                   【并发编程】volatile关键字最全详解,看这一篇就够了
                   【并发基础】CAS(Compare And Swap)操作的底层原理以及应用详解
                   【并发编程】volatile关键字最全详解,看这一篇就够了
                   【并发编程】Java中的锁有哪些?各自都有什么样的特性?   

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值