Java 并发编程之ThreadLocal详解及实例

Java 理解 ThreadLocal

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

一. 对 ThreadLocal 的理解

1). ThreadLocal 概述

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

2). ThreadLocal 在 JDK 中的定义


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

每个线程都有关于该 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 uniqueNum = new ThreadLocal() {

    @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 initialValue 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}.

  *

  * 

This implementation simply returns null; if the * programmer desires thread-local variables to have an initial * value other than null, ThreadLocal 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 longLocal = new ThreadLocal();

  ThreadLocal stringLocal = new ThreadLocal();

  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 connectionHolder = new ThreadLocal() {

  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()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象,不会交叉。
对标阿里P6级架构师
获取更多资料可扫描下方二维码

你可能感兴趣的:(Java 并发编程之ThreadLocal详解及实例)