java--ThreadLocal详解

目录

1.ThreadLocal的用途两个使用场景

1.1场景1

1.2两个线程分别用自己的SimpleDateFormat

1.3使用线程池创建1000个打印线程分别用自己的SimpleDateFormat

 打印可以看到有很多处两个重复的日期

1.4加锁解决线程安全问题

1.5SimpleDateFormat小结

1.5更好的解决方案是使用ThreadLoacl

1.6总结

1.2场景2

 1.2.1方法

 1.3总结

1.3.1两个作用

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

1.3.3带来的好处

2.ThreadLocal原理

2.1主要方法

1.T initialValue()

2.set(T value) 为这个线程设一个新值

3.get() 得到这个线程对应的value 

 4.remove() 删除对应这个线程的值

2.2ThreadLocalMap

3.ThreadLocal使用注意点

3.1内存泄露

3.2Key的泄露

3.3如何避免内存泄露

 3.2ThreadLocal带来的空指针异常

3.3共享对象

3.4注意点

3.5实际应用场景

DateTimeContextHolder


 

ThreadLocal是本地线程,不是公用的线程。

1.ThreadLocal的用途两个使用场景

1.1场景1

每个线程对象需要一个共享对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Radmon)

每个Thread内有自己在实例副本,不共享。

举个例子:教材只有一本,30个同学都抢着看。一起做笔记有线程安全问题,并发的读写会带来数据不一致,用了ThreadLocal后相当于把这本教材复印了30份教材,每个同学都使用自己的教材,这里的每一本书每一个实例,都只能当前同学当前的线程可以访问到,并且使用。

1.2两个线程分别用自己的SimpleDateFormat

package cn.butool;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 用两个线程分别打印日期信息
 */
public class ThreadLocalSimpleDateFormatDemo {
    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            System.out.println(new ThreadLocalSimpleDateFormatDemo().dataToString(10));
        }
        );
        thread1.start();

        Thread thread2 = new Thread(() -> {
            System.out.println(new ThreadLocalSimpleDateFormatDemo().dataToString(212104277));
        }
        );
        thread2.start();
    }

    /**
     * 将秒转换成日期字符串
     *
     * @param seconds
     * @return
     */
    public String dataToString(int seconds) {
        //参数的单位是毫秒 是从1970.1.1 00:00:00 GMT计时
        Date date = new Date(seconds * 1000);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return simpleDateFormat.format(date);
    }
}

 并没有问题

1970-01-20 10:34:39
1970-01-01 08:00:10

1.3使用线程池创建1000个打印线程分别用自己的SimpleDateFormat

package cn.butool;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 用线程池创建分别打印日期信息
 */
public class ThreadLocalSimpleDateFormatDemo {
    private static ExecutorService executorService = Executors.newFixedThreadPool(10);
    //dateFormat 不需要每次都新建
    private static SimpleDateFormat simpleDateFormat = 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;
            executorService.submit(() -> {
                System.out.println(new ThreadLocalSimpleDateFormatDemo().dataToString(10+ finalI));
            });
        }
        executorService.shutdown();

    }

    /**
     * 将秒转换成日期字符串
     *
     * @param seconds
     * @return
     */
    public String dataToString(int seconds) {
        //参数的单位是毫秒 是从1970.1.1 00:00:00 GMT计时
        Date date = new Date(seconds * 1000);
        return simpleDateFormat.format(date);
    }
}

 打印可以看到有很多处两个重复的日期

1970-01-01 08:15:51
1970-01-01 08:16:14
1970-01-01 08:16:13
1970-01-01 08:16:12
1970-01-01 08:16:17
1970-01-01 08:16:18
1970-01-01 08:16:11
1970-01-01 08:16:11

1.4加锁解决线程安全问题

package cn.butool.threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 用线程池创建分别打印日期信息
 */
public class ThreadLocalSimpleDateFormatDemo {
    private static ExecutorService executorService = Executors.newFixedThreadPool(10);
    //dateFormat 不需要每次都新建
    private static SimpleDateFormat simpleDateFormat = 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;
            executorService.submit(() -> {
                System.out.println(new ThreadLocalSimpleDateFormatDemo().dataToString(10+ finalI));
            });
        }
        executorService.shutdown();

    }

    /**
     * 将秒转换成日期字符串
     *
     * @param seconds
     * @return
     */
    public String dataToString(int seconds) {
        //参数的单位是毫秒 是从1970.1.1 00:00:00 GMT计时
        Date date = new Date(seconds * 1000);
        String format=null;
        synchronized (ThreadLocalSimpleDateFormatDemo.class){
            format = simpleDateFormat.format(date);
        }
        return format;
    }
}

1.5SimpleDateFormat小结

  1. 两个线程分别用自己的SimpleDateFormat
  2. 1000个线程要用到线程池了,否则消耗内存太多
  3. 优化代码,所有线程共用一个SimpleDateFormat对象
  4. 加锁解决重复对象,但是降低效率

1.5更好的解决方案是使用ThreadLoacl

利用ThreadLocal给每个线程分配自己的DateFormat对象,同时保证了线程安全。高效利用了内存

package cn.butool.threadlocal;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 利用ThreadLocal给每个线程分配自己的DateFormat对象,
 * 同时保证了线程安全。高效利用了内存
 */
public class ThreadDateFormatResult {
    private static ExecutorService executorService = Executors.newFixedThreadPool(10);
    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            executorService.submit(() -> {
                System.out.println(new ThreadDateFormatResult().dataToString(10+ finalI));
            });
        }
        executorService.shutdown();

    }

    /**
     * 将秒转换成日期字符串
     *
     * @param seconds
     * @return
     */
    public String dataToString(int seconds) {
        //参数的单位是毫秒 是从1970.1.1 00:00:00 GMT计时
        Date date = new Date(seconds * 1000);
        SimpleDateFormat simpleDateFormat = ThreadSafeDateFormat.dateFormatThreadLocalLambda.get();
        return simpleDateFormat.format(date);
    }

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

         /**
          * Lambda表达式写法和dateFormatThreadLocal效果相同
          */
        public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocalLambda = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
    }
}

1.6总结

  1. 两个线程分别用自己的SimpleDateFormat没有问题
  2. 延深1000个需要使用线程池,否则消耗内存过大
  3. 使用线程池后发现,最好使用同一个SimpleDateFormat对象,否则创建1000个任务也是创建1000对象
  4. 发现是线程不安全的,出现并发安全问题
  5. 选择加锁,枷锁结果正常,但是效率低
  6. 使用更好的解决方案ThreadLocal,线程安全的,每个线程内都有一个自己的独享的SimpleDateFormat对象

1.2场景2

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

service调用,每个都需要user对象,层层传递,代码冗余且不易维护。

每个线程保存全局变量,可以让不同的方法直接使用。避免参数传递的麻烦。

 1.2.1方法

  1. 用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取到的用户名,userId等)
  2. 这些信息在同一个线程内相同,但不同的线程使用的业务内容是不同的
  3. 使用ThreadLocal,可以在不影响性能的情况下,无需层层传递,就可以保存当前线程内的用户信息
  4. 强调是同一个请求内,同一个线程内,不同方法间的共享
  5. 不需要重写initialValue方法
package cn.butool.threadlocal;

/**
 * 演示threadLocal避免传递参数的代码冗余
 */
public class UseThreadLocalSaveUser {
    public static void main(String[] args) {
        new Service1().process();

    }
}

class Service1 {
    // 读取到用户信息
    public void process() {
        User user = new User("busl");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    // 读取到用户信息
    public void process() {
        System.out.println("service2"+UserContextHolder.holder.get().getName());
        new Service3().process();
    }
}
class Service3 {
    // 读取到用户信息
    public void process() {
        System.out.println("service3"+UserContextHolder.holder.get().getName());
    }
}

/**
 * 用户上下文持有者
 */
class UserContextHolder{
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
    private String name;

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

    public String getName() {
        return name;
    }

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

 1.3总结

1.3.1两个作用

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

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

  1. 在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制
  2. 如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用。

1.3.3带来的好处

  1. 线程安全
  2. 不需要加锁,提高执行效率
  3. 更高效的利用内存、节省开销:想对于每个任务都新建一个SimpleDateFormat
  4. 免去传参的繁琐,使得代码更优雅

2.ThreadLocal原理

  • Thread、ThreadLocal和ThreadLocalMap三者之间的关系
  • 每个Thread对象中都持有一个ThreadLocalMap成员变量
  • ThreadLocalMap可以存储多个ThreadLocal

2.1主要方法

1.T initialValue()

  • 该方法会返回当前线程对应的初始值,这是一个延迟加载的方法,只有在调用get的时候,才会触发。
  • 当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法
  • 通常,每个线程最多调用一次此方法,但如果已经调用了remove0后,再调用get0,则可以再次调用此方法
  • 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue0方法,以便在后续使用中可以初始化副本对象。
public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        // 1.使用前调用了set方法
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 2.调用setInitialValue方法
        return setInitialValue();
    }


// set()的变量,用于建立initialValue。在用户重写了set()方法的情况下使用,而不是set()

private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

2.set(T value) 为这个线程设一个新值

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

3.get() 得到这个线程对应的value 

get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

 4.remove() 删除对应这个线程的值

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

 演示:

package cn.butool.threadlocal;

/**
 * 演示threadLocal避免传递参数的代码冗余
 */
public class UseThreadLocalSaveUser {
    public static void main(String[] args) {
        new Service1().process();
    }
}

class Service1 {
    // 读取到用户信息
    public void process() {
        User user = new User("busl");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    // 读取到用户信息
    public void process() {
        System.out.println("service2"+UserContextHolder.holder.get().getName());
        UserContextHolder.holder.remove();
        new Service3().process();
    }
}
class Service3 {
    // 读取到用户信息
    public void process() {
        User user = new User("busl2");
        UserContextHolder.holder.set(user);
        System.out.println("service3"+UserContextHolder.holder.get().getName());
    }
}

/**
 * 用户上下文持有者
 */
class UserContextHolder{
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
    private String name;

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

    public String getName() {
        return name;
    }

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

打印:

service2busl
service3busl2

2.2ThreadLocalMap

  • ThreadLocalMap类也就是ThreadLocal.threadLocals
ThreadLocal.ThreadLocalMap threadLocals = null;
  • ThreadLocalMap是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认定为一个map键值对
    键:这个ThreadLocal 值实际的成员变量,比如User或者simpleDateFormat对象
  • setInitialValue和直接set最后都是调用map.set()方法来设置值,最后都会对应到ThreadLocalMap的一个Entry,只不过起点和入口不一样

3.ThreadLocal使用注意点

3.1内存泄露

某个对象不在有用了,但是占用的内存却不能回收,会导致这一部分始终被占用,如果这种情况有很多。越来越多的情况会导致内存不够用了,超限制了。

3.2Key的泄露

ThreadLocalMap 中的内部类 Entry继承自WeakReference,是弱引用

弱引用的特点:如果这个对象只被弱引用关联,那么这个对象就可以被回收,弱引用不会阻止GC。

强引用:通常 一个对象等于什么,比如  下面的  value = v;

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            // ThreadLocal  Object 
            Entry(ThreadLocal<?> k, Object v) {
      
                super(k);
                value = v;
            }
        }
public WeakReference(T referent) {
        super(referent);
    }

ThreadLocalMap的每个Entry都是一个对key的弱引用,和一个对value的value的强引用。

正常情况下,当线程终止,保存在Threadlocal里面的Value会被垃圾回收,因此没有任何强引用了。

但是,如果线程不终止,保持很久,那么key对应的value就不能被回收,因此 有以下调用链:

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

因为value 和Thread之间还存在强引用链路,所以导致value无法回收,就可能出现oom

JDK已经考虑到这个问题,所以set,remove,rehash方法中会扫描key为null的Entry,并且把对应的value设置为null,这样value就可以被对象回收,下面的考虑,把强引用链给断掉。

                   if (k == null) {
                        e.value = null; // Help the GC
                    } 

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();
                    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的内存泄漏

3.3如何避免内存泄露

调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏。

所以使用完ThreadLocal之后,应该调用remove方法。

在实际开发中,如果使用拦截器的方法拦截保存用户信息,应该在线程请求完成前拦住,调用remove方法。

class Service3 {
    // 读取到用户信息
    public void process() {
        User user = new User("busl2");
        UserContextHolder.holder.set(user);
        System.out.println("service3"+UserContextHolder.holder.get().getName());
        // 在最后的代码使用完去remove
        UserContextHolder.holder.remove();
    }
}

 3.2ThreadLocal带来的空指针异常

package cn.butool.threadlocal;

/**
 *
 */
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) {
        ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
        System.out.println(threadLocalNPE.get());
        Thread thread = new Thread(() -> {
            threadLocalNPE.set();
            System.out.println(threadLocalNPE.get());
        });
        thread.start();
    }
}

//改为Long即可,编码问题

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

3.3共享对象

如果在每个线程中ThreadLocal.set0进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get0取得的还是这个共享对象本身,还是有并发访问问题 

3.4注意点

1.如果可以不适用ThreadLoca就解决问题,那么不要强行使用

        例如任务很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLoca 

2.优先使用框架的支持,而不是自己的创造

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

3.5实际应用场景

DateTimeContextHolder

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.format.datetime.standard;

import java.time.format.DateTimeFormatter;
import java.util.Locale;
import org.springframework.core.NamedThreadLocal;

public final class DateTimeContextHolder {
    private static final ThreadLocal<DateTimeContext> dateTimeContextHolder = new NamedThreadLocal("DateTime Context");

    public DateTimeContextHolder() {
    }

    public static void resetDateTimeContext() {
        dateTimeContextHolder.remove();
    }

    public static void setDateTimeContext(DateTimeContext dateTimeContext) {
        if (dateTimeContext == null) {
            resetDateTimeContext();
        } else {
            dateTimeContextHolder.set(dateTimeContext);
        }

    }

    public static DateTimeContext getDateTimeContext() {
        return (DateTimeContext)dateTimeContextHolder.get();
    }

    public static DateTimeFormatter getFormatter(DateTimeFormatter formatter, Locale locale) {
        DateTimeFormatter formatterToUse = locale != null ? formatter.withLocale(locale) : formatter;
        DateTimeContext context = getDateTimeContext();
        return context != null ? context.getFormatter(formatterToUse) : formatterToUse;
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值