Java 并发:深入理解 ThreadLocal

摘要:

  ThreadLocal 又名线程局部变量,是 Java 中一种较为特殊的线程绑定机制,用于保证变量在不同线程间的隔离性,以方便每个线程处理自己的状态。进一步地,本文以ThreadLocal类的源码为切入点,深入分析了ThreadLocal类的作用原理,并给出应用场景和一般使用步骤。


一. 对 ThreadLocal 的理解

1). ThreadLocal 概述

  ThreadLocal 又名 线程局部变量,是 Java 中一种较为特殊的 线程绑定机制,可以为每一个使用该变量的线程都提供一个变量值的副本,并且每一个线程都可以独立地改变自己的副本,而不会与其它线程的副本发生冲突。通过 ThreadLocal 存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种 隔离机制



2). ThreadLocal 在 JDK 中的定义

ThreadLocal
This class provides thread-local variables. These variables differ from their normal counterparts(副本) in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

Each thread holds an implicit reference to its copy of a thread-local variable (见下图) as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

          

我们可以从中摘出三条要点:

  • 每个线程都有关于该 ThreadLocal变量 的私有值

     每个线程都有一个独立于其他线程的上下文来保存这个变量的值,并且对其他线程是不可见的。

  • 独立于变量的初始值

     ThreadLocal 可以给定一个初始值,这样每个线程就会获得这个初始化值的一个拷贝,并且每个线程对这个值的修改对其他线程是不可见的。

  • 状态与某一个线程相关联

     ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制,理解这点对正确使用 ThreadLocal 至关重要。



3). 应用场景

  类 ThreadLocal 主要解决的就是为每个线程绑定自己的值,以方便其处理自己的状态。形象地讲,可以将 ThreadLocal变量 比喻成全局存放数据的盒子,盒子中可以存储每个线程的私有数据。例如,以下类用于生成对每个线程唯一的局部标识符。线程 ID 是在第一次调用 uniqueNum.get() 时分配的,在后续调用中不会更改。

import java.util.concurrent.atomic.AtomicInteger;

public class UniqueThreadIdGenerator {
    private static final AtomicInteger uniqueId = new AtomicInteger(0);

    private static final ThreadLocal<Integer> uniqueNum = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return uniqueId.getAndIncrement();
        }
    };

    public static void main(String[] args) {
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            String name = "Thread-" + i;
            threads[i] = new Thread(name){
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ": "
                            + uniqueNum.get());
                }
            };
            threads[i].start();
        }

        System.out.println(Thread.currentThread().getName() + ": "
                + uniqueNum.get());
    }
}/* Output(输出结果不唯一):
        Thread-1: 2
        Thread-0: 0
        Thread-2: 3
        main: 1
        Thread-3: 4
        Thread-4: 5
 *///:~

二. 深入分析ThreadLocal类

  下面,我们来看一下 ThreadLocal 的具体实现,该类一共提供的四个方法:

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }

  其中,get()方法是用来获取 ThreadLocal变量 在当前线程中保存的值,set() 用来设置 ThreadLocal变量 在当前线程中的值,remove() 用来移除当前线程中相关 ThreadLocal变量,initialValue() 是一个 protected 方法,一般需要重写。



1、 原理探究

1). 切入点: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() {
        Thread t = Thread.currentThread();    // 获取当前线程对象
        ThreadLocalMap map = getMap(t);     // 获取当前线程的成员变量 threadLocals
        if (map != null) {
            // 从当前线程的 ThreadLocalMap 获取该 thread-local variable 对应的 entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;   // 取得目标值
        }
        return setInitialValue();
    }


2).关键点: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();     // 默认实现返回 null
        Thread t = Thread.currentThread();   // 获得当前线程
        ThreadLocalMap map = getMap(t);     // 得到当前线程 ThreadLocalMap类型域 threadLocals
        if (map != null)
            map.set(this, value);  // 该 map 的键是当前 ThreadLocal 对象
        else
            createMap(t, value);
        return value;
    }

  我们紧接着看上述方法涉及到的三个方法:initialValue(),set(this, value) 和 createMap(t, value)。

(1) initialValue()

   /**
     * Returns the current thread‘s "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the <tt>initialValue</tt> method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns <tt>null</tt>; if the
     * programmer desires thread-local variables to have an initial
     * value other than <tt>null</tt>, <tt>ThreadLocal</tt> must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;            // 默认实现返回 null
    }

(2) createMap()

/**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     * @param map the map to store.
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue); // this 指代当前 ThreadLocal 变量,为 map 的键
    }


至此,可能大部分朋友已经明白了 ThreadLocal类 是如何为每个线程创建变量的副本的:

  ① 在每个线程Thread内部有一个 ThreadLocal.ThreadLocalMap 类型的成员变量 threadLocals,这个threadLocals就是用来存储实际的ThreadLocal变量副本的,键值为当前ThreadLocal变量,value为变量的副本(值);

  ② 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的值为value,存到 threadLocals;

  ③ 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在对应线程的threadLocals里面查找。



2、实例验证

  下面通过一个例子来证明通过ThreadLocal能达到在每个线程中创建变量副本的效果:

public class Test {

    ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
    ThreadLocal<String> stringLocal = new ThreadLocal<String>();

    public void set() {
        longLocal.set(Thread.currentThread().getId());
        stringLocal.set(Thread.currentThread().getName());
    }

    public long getLong() {
        return longLocal.get();
    }

    public String getString() {
        return stringLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        final Test test = new Test();

        test.set();
        System.out.println("父线程 main :");
        System.out.println(test.getLong());
        System.out.println(test.getString());

        Thread thread1 = new Thread() {
            public void run() {
                test.set();
                System.out.println("\n子线程 Thread-0 :");
                System.out.println(test.getLong());
                System.out.println(test.getString());
            };
        };
        thread1.start();
    }
}/* Output:
        父线程 main :
                    1
                    main

        子线程 Thread-0 :
                    12
                    Thread-0
 *///:~

  从这段代码的输出结果可以看出,在main线程中和thread1线程中,longLocal保存的副本值和stringLocal保存的副本值都不一样,并且进一步得出:

  • 实际上,通过 ThreadLocal 创建的副本是存储在每个线程自己的threadLocals中的;
  • 为何 threadLocals 的类型 ThreadLocalMap 的键值为 ThreadLocal 对象,因为每个线程中可有多个 threadLocal变量,就像上面代码中的 longLocal 和 stringLocal;
  • 在进行get之前,必须先set,否则会报空指针异常;若想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。

三. ThreadLocal的应用场景

  在 Java 中,类 ThreadLocal 解决的是变量在不同线程间的隔离性。 最常见的 ThreadLocal 使用场景有 数据库连接问题、Session管理等。

(1) 数据库连接问题

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
    public Connection initialValue() {
        return DriverManager.getConnection(DB_URL);
    }
};

public static Connection getConnection() {
    return connectionHolder.get();
}

(2) Session管理

private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {
    Session s = (Session) threadSession.get();
    try {
        if (s == null) {
            s = getSessionFactory().openSession();
            threadSession.set(s);
        }
    } catch (HibernateException ex) {
        throw new InfrastructureException(ex);
    }
    return s;
}

四. ThreadLocal 一般使用步骤

ThreadLocal 使用步骤一般分为三步:

  • 创建一个 ThreadLocal 对象 threadXxx,用来保存线程间需要隔离处理的对象 xxx;
  • 提供一个获取要隔离访问的数据的方法 getXxx(),在方法中判断,若 ThreadLocal对象为null时候,应该 new() 一个隔离访问类型的对象;
  • 在线程类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象,不会交叉。

引用

《Java 并发编程实战》

《Java 多线程编程核心技术》

[Java并发编程:Thread类的使用][16]

Java并发编程:深入剖析ThreadLocal

正确理解ThreadLocal

深入理解ThreadLocal

深入研究java.lang.ThreadLocal类

时间: 2024-10-09 18:34:49

Java 并发:深入理解 ThreadLocal的相关文章

【Java】深入理解ThreadLocal

一.前言 要理解ThreadLocal,首先必须理解线程安全.线程可以看做是一个具有一定独立功能的处理过程,它是比进程更细度的单位.当程序以单线程运行的时候,我们不需要考虑线程安全.然而当一个进程中包含多个线程的时候,就需要考虑线程安全问题,因为此时线程可能会同时操作同一个资源,当两个或者两个以上线程同时操作一个资源的时候,就会造成冲突.不一致等问题,即线程不安全. 解决线程安全问题,本质上就是解决资源共享问题,一般有以下手段: 1)可重入(不依赖环境):2)互斥(同一时间段只允许一个线程使用)

java并发的理解

我认为并发大体上分为两种情况 1,多个线程或者进程访问公共资源,比如12306 2,多个线程访问同一个实例变量,比如tomcat 多个请求的线程访问同一个单例bean,如果bean是有状态的,就可能出现并发问题 对于第一种情况,在同一个jvm下可以通过java 同步关键字synchronized解决.但是在分布式的情况下,则要用其他的方式解决,比如数据库乐观锁,zookeeper. 拿买票来举例,比如id 111还有10张票,现在同是有很多请求要要更新数据 id num 111 10 伪代码类似

Java并发编程之ThreadLocal类

ThreadLocal类可以理解为ThreadLocalVariable(线程局部变量),提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本,因此get总是返回当前执行线程在调用set时设置的最新值.可以将ThreadLocal<T>视为 包含了Map<Thread,T>对象,保存了特定于该线程的值. 概括起来说,对于多线程资源共享的问题,同步机制采用了"以时间换空间"的方式,而ThreadLocal采用了"以空间

Java并发编程:ThreadLocal

一.对ThreadLocal的理解 ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多.可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量. 这句话从字面上看起来很容易理解,但是真正理解并不是那么容易. 我们还是先来看一个例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class ConnectionManager {          private 

Java并发编程:ThreadLocal的使用以及实现原理解析

前言 前面的文章里,我们学习了有关锁的使用,锁的机制是保证同一时刻只能有一个线程访问临界区的资源,也就是通过控制资源的手段来保证线程安全,这固然是一种有效的手段,但程序的运行效率也因此大大降低.那么,有没有更好的方式呢?答案是有的,既然锁是严格控制资源的方式来保证线程安全,那我们可以反其道而行之,增加更多资源,保证每个线程都能得到所需对象,各自为营,互不影响,从而达到线程安全的目的,而ThreadLocal便是采用这样的思路. ThreadLocal实例 ThreadLocal翻译成中文的话大概

Java并发编程之ThreadLocal内存泄漏探究

使用 ThreadLocal 不当可能会导致内存泄露,是什么原因导致的内存泄漏呢? 我们首先看一个例子,代码如下: /** * Created by cong on 2018/7/14. */ public class ThreadLocalOutOfMemoryTest { static class LocalVariable { private Long[] a = new Long[1024*1024]; } // (1) final static ThreadPoolExecutor p

理解ThreadLocal(一)

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地编写出优美的多线程程序. ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”.其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些. 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变

7、Java并发编程:深入剖析ThreadLocal

Java并发编程:深入剖析ThreadLocal 想必很多朋友对ThreadLocal并不陌生,今天我们就来一起探讨下ThreadLocal的使用方法和实现原理.首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两个应用场景. 以下是本文目录大纲: 一.对ThreadLocal的理解 二.深入解析ThreadLocal类 三.ThreadLocal的应用场景 若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者

Java并发编程:深入剖析ThreadLocal

Java并发编程:深入剖析ThreadLocal 想必很多朋友对ThreadLocal并不陌生,今天我们就来一起探讨下ThreadLocal的使用方法和实现原理.首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两个应用场景. 以下是本文目录大纲: 一.对ThreadLocal的理解 二.深入解析ThreadLocal类 三.ThreadLocal的应用场景 若有不正之处请多多谅解,并欢迎批评指正. 请尊重作者