分析ThreadLocal如何做到单个线程独享

简介: 分析ThreadLocal如何做到单个线程独享

分析ThreadLocal如何做到单个线程独享

前情概要

  • 我们可能都知道SimpleDateFormat这个类的实例它不是线程安全的,如果不知道,我把代码贴这儿:
// 类的成员变量
  protected Calendar calendar;
  // 这个私有方法会对calendar对象进行赋值,但是没有加锁,在多并发场景下,就造成了问题。
  // Called from Format after creating a FieldDelegate
    private StringBuffer format(Date date, StringBuffer toAppendTo,
                                FieldDelegate delegate) {
        // Convert input date to time field list
        calendar.setTime(date);
        // ....其他代码
    }
public class ThreadLocalDemo06 {
    public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            final int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalDemo06().date(finalI);
                    System.out.println(Thread.currentThread().getName() + "  " + date);
                }
            });
        }
        threadPool.shutdown();
    }
    public String date(int seconds) {
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
        return dateFormat.format(date);
    }
}
class ThreadSafeFormatter {
    // public final static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal 是不是会更好呢!
    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("mm:ss");
        }
    };
}
  • 这里使用了dateFormatThreadLocal.get()来保证获取到的一定是一个线程一个。

代码分析

java.lang.ThreadLocal#initialValue

  • 这是一个protected 修饰的方法,默认返回null。具体实现交给子类,返回类型T。
protected T initialValue() {    return null;}

java.lang.ThreadLocal#get

  • 这是获取对象的方法,为示例中使用,做到一个线程返回一个对象。
/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        // 1. 获取当前线程的引用
        Thread t = Thread.currentThread();
        // 2. 获取这个线程内部维护的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 3. 以当前的ThreadLocal对象作为key去获取键值对Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 4. 获取值
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
  • 从这个方法中可以看到,当已经存在对象时,获取对象分四步走。
  • 当这个对象还未存在时,调用setInitialValue()进行初始化。

java.lang.ThreadLocal#setInitialValue

/**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        T value = initialValue();
        // 1. 获取当前线程
        Thread t = Thread.currentThread();
        // 2. 获取线程中维护的ThreadLocalMap对象。
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 3. 将该线程中的map,以ThreadLoacal本身作为key,将Value进行设置。
            map.set(this, value);
        else
            // 如果map都不存在,则在该线程中创建该map,并进行set value。
            // 真是节省空间,都是用到才去创建!
            createMap(t, value);
        return value;
    }
  • 从这个方法看到,是这个方法去真正的调用了initialValue方法进行初始化的,所以ThreadLocal的对象获取其实也是一个懒加载的方式,用到了才去进行初始化。
  • 也看到了这里是对Thread中的map进行赋值的位置。

小结

  • 每个线程的default级别的成员变量ThreadLocal.ThreadLocalMap threadLocals都是由ThreadLocal进行维护的。

  • threadLocals 以ThreadLocal对象作为key,保证了一个线程中,同一个ThreadLocal的实例去执行get方法去获取到这个的对象都是唯一的。这也解释了为什么ThreadLocal的对象在示例中使用了static修饰,为了保证它的唯一性,当然,我更乐意在 static 前再加上 final 进行修饰。

Debug实操

  • 代码分析看过了,进行debug实际查看一下,就用以上的示例代码。
  • 示例代码中,线程池的核心线程数是16,最大线程数也是16.[暂且不论创建线程池的姿势不丝滑],那么在线程启动后,应当:每个线程在执行get()方法前,threadLocals这个map应该为null,执行后,每个线程保证只有一个。

初始化之前

  • 第一次执行get时,没有获取到需要进行setInitialValue()。可以观察到this对象的确不存在,他去进行set初始化。

初始化之后

  • 第二次这个线程再去获取时,就会去map中获取。

总结

要理清Thread、ThreadLocal、ThreadLocal.ThreadLocalMap三者的关系,结合debug,不难看到它究竟是怎么实现线程独享的一个ThreadLocal对象的。

目录
相关文章
|
12天前
|
并行计算 API 调度
探索Python中的并发编程:线程与进程的对比分析
【9月更文挑战第21天】本文深入探讨了Python中并发编程的核心概念,通过直观的代码示例和清晰的逻辑推理,引导读者理解线程与进程在解决并发问题时的不同应用场景。我们将从基础理论出发,逐步过渡到实际案例分析,旨在揭示Python并发模型的内在机制,并比较它们在执行效率、资源占用和适用场景方面的差异。文章不仅适合初学者构建并发编程的基础认识,同时也为有经验的开发者提供深度思考的视角。
|
2月前
|
存储 监控 Java
|
2月前
|
安全 Java 开发者
Swing 的线程安全分析
【8月更文挑战第22天】
37 4
|
2月前
|
Java 数据库连接 数据库
当线程中发生异常时的情况分析
【8月更文挑战第22天】
71 4
|
2月前
|
安全 Java 程序员
线程安全与 Vector 类的分析
【8月更文挑战第22天】
24 4
|
2月前
|
存储 缓存 安全
深度剖析Java HashMap:源码分析、线程安全与最佳实践
深度剖析Java HashMap:源码分析、线程安全与最佳实践
|
2月前
|
消息中间件 安全 Kafka
"深入实践Kafka多线程Consumer:案例分析、实现方式、优缺点及高效数据处理策略"
【8月更文挑战第10天】Apache Kafka是一款高性能的分布式流处理平台,以高吞吐量和可扩展性著称。为提升数据处理效率,常采用多线程消费Kafka数据。本文通过电商订单系统的案例,探讨了多线程Consumer的实现方法及其利弊,并提供示例代码。案例展示了如何通过并行处理加快订单数据的处理速度,确保数据正确性和顺序性的同时最大化资源利用。多线程Consumer有两种主要模式:每线程一个实例和单实例多worker线程。前者简单易行但资源消耗较大;后者虽能解耦消息获取与处理,却增加了系统复杂度。通过合理设计,多线程Consumer能够有效支持高并发数据处理需求。
87 4
|
1月前
|
安全 Java API
Java线程池原理与锁机制分析
综上所述,Java线程池和锁机制是并发编程中极其重要的两个部分。线程池主要用于管理线程的生命周期和执行并发任务,而锁机制则用于保障线程安全和防止数据的并发错误。它们深入地结合在一起,成为Java高效并发编程实践中的关键要素。
16 0
|
3月前
|
存储 SQL Java
(七)全面剖析Java并发编程之线程变量副本ThreadLocal原理分析
在之前的文章:彻底理解Java并发编程之Synchronized关键字实现原理剖析中我们曾初次谈到线程安全问题引发的"三要素":多线程、共享资源/临界资源、非原子性操作,简而言之:在同一时刻,多条线程同时对临界资源进行非原子性操作则有可能产生线程安全问题。
|
2月前
|
算法 安全 Java
深入解析Java多线程:源码级别的分析与实践
深入解析Java多线程:源码级别的分析与实践