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

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()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象,不会交叉。

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

(0)

相关推荐

  • Java 多线程并发编程_动力节点Java学院整理

    一.多线程 1.操作系统有两个容易混淆的概念,进程和线程. 进程:一个计算机程序的运行实例,包含了需要执行的指令:有自己的独立地址空间,包含程序内容和数据:不同进程的地址空间是互相隔离的:进程拥有各种资源和状态信息,包括打开的文件.子进程和信号处理. 线程:表示程序的执行流程,是CPU调度执行的基本单位:线程有自己的程序计数器.寄存器.堆栈和帧.同一进程中的线程共用相同的地址空间,同时共享进进程锁拥有的内存和其他资源. 2.Java标准库提供了进程和线程相关的API,进程主要包括表示进程的jav

  • java并发编程_线程池的使用方法(详解)

    一.任务和执行策略之间的隐性耦合 Executor可以将任务的提交和任务的执行策略解耦 只有任务是同类型的且执行时间差别不大,才能发挥最大性能,否则,如将一些耗时长的任务和耗时短的任务放在一个线程池,除非线程池很大,否则会造成死锁等问题 1.线程饥饿死锁 类似于:将两个任务提交给一个单线程池,且两个任务之间相互依赖,一个任务等待另一个任务,则会发生死锁:表现为池不够 定义:某个任务必须等待池中其他任务的运行结果,有可能发生饥饿死锁 2.线程池大小 注意:线程池的大小还受其他的限制,如其他资源池:

  • Java 并发编程之线程挂起、恢复与终止

    挂起和恢复线程 Thread 的API中包含两个被淘汰的方法,它们用于临时挂起和重启某个线程,这些方法已经被淘汰,因为它们是不安全的,不稳定的.如果在不合适的时候挂起线程(比如,锁定共享资源时),此时便可能会发生死锁条件--其他线程在等待该线程释放锁,但该线程却被挂起了,便会发生死锁.另外,在长时间计算期间挂起线程也可能导致问题. 下面的代码演示了通过休眠来延缓运行,模拟长时间运行的情况,使线程更可能在不适当的时候被挂起: public class DeprecatedSuspendResume

  • Java多线程并发编程(互斥锁Reentrant Lock)

    Java 中的锁通常分为两种: 通过关键字 synchronized 获取的锁,我们称为同步锁,上一篇有介绍到:Java 多线程并发编程 Synchronized 关键字. java.util.concurrent(JUC)包里的锁,如通过继承接口 Lock 而实现的 ReentrantLock(互斥锁),继承 ReadWriteLock 实现的 ReentrantReadWriteLock(读写锁). 本篇主要介绍 ReentrantLock(互斥锁). ReentrantLock(互斥锁)

  • Java多线程并发编程 并发三大要素

    一.原子性 原子,一个不可再被分割的颗粒.原子性,指的是一个或多个不能再被分割的操作. int i = 1; // 原子操作 i++; // 非原子操作,从主内存读取 i 到线程工作内存,进行 +1,再把 i 写到朱内存. 虽然读取和写入都是原子操作,但合起来就不属于原子操作,我们又叫这种为"复合操作". 我们可以用synchronized 或 Lock 来把这个复合操作"变成"原子操作. 例子: private synchronized void increase

  • Java并发编程总结——慎用CAS详解

    一.CAS和synchronized适用场景 1.对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源:而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能. 2.对于资源竞争严重的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized.以java.util.concurrent.atomic包中AtomicInteger类为例,其getAn

  • java并发编程之cas详解

    CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值.这听起来可能有一点复杂但是实际上你理解之后发现很简单,接下来,让我们跟深入的了解一下这项技术. CAS的使用场景 在程序和算法中一个经常出现的模式就是"check and act"模式.先检查后操作模式发生在代码中首先检查一个变量的值,然后再基于这个值做一些操作.下面是一个

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

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

  • Java并发编程之ThreadLocal详解

    目录 一.什么是ThreadLocal? 二.ThreadLocal的使用场景 三.如何使用ThreadLocal 四.数据库连接时的使用 五.ThreadLocal工作原理 六.小结 七.注意点 一.什么是ThreadLocal? ThreadLocal叫做线程本地变量,ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的.ThreadLocal为变量在每个线程中都创建了一个副本,则每个线程都可以访问自己内部的副本变量. 二.ThreadLocal的使用场景 1.当对象

  • 深入理解Java并发编程之ThreadLocal

    目录 ThreadLocal简介 ThreadLocal源码解析 实现原理 ThreadLocalMap源码分析 InheritableThreadLocal 参考资料 ThreadLocal简介 变量值的共享可以使用public static的形式,所有线程都使用同一个变量,如果想实现每一个线程都有自己的共享变量该如何实现呢?JDK中的ThreadLocal类正是为了解决这样的问题. ThreadLocal类并不是用来解决多线程环境下的共享变量问题,而是用来提供线程内部的共享变量,在多线程环境

  • Java并发编程之threadLocal

    目录 1.ThreadLocal介绍 2.ThreadLocal使用实例 3.ThreadLocal实现原理 1.ThreadLocal介绍 多个线程访问同一个共享变量时特别容易出现并发问题,特别是多线程需要对共享变量进行写入时.为了保证线程安全,一般使用者在访问共享变量的时候需要进行适当的同步,如图 同步的一般措施是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的负担,那么有没有一种方法可以做到,当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢?其实ThreadL

  • 实例讲解Java并发编程之ThreadLocal类

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

  • Java并发编程之Semaphore(信号量)详解及实例

    Java并发编程之Semaphore(信号量)详解及实例 概述 通常情况下,可能有多个线程同时访问数目很少的资源,如客户端建立了若干个线程同时访问同一数据库,这势必会造成服务端资源被耗尽的地步,那么怎样能够有效的来控制不可预知的接入量呢?及在同一时刻只能获得指定数目的数据库连接,在JDK1.5 java.util.concurrent 包中引入了Semaphore(信号量),信号量是在简单上锁的基础上实现的,相当于能令线程安全执行,并初始化为可用资源个数的计数器,通常用于限制可以访问某些资源(物

  • Java并发编程之Exchanger方法详解

    简介 Exchanger是一个用于线程间数据交换的工具类,它提供一个公共点,在这个公共点,两个线程可以交换彼此的数据. 当一个线程调用exchange方法后将进入等待状态,直到另外一个线程调用exchange方法,双方完成数据交换后继续执行. Exchanger的使用 方法介绍 exchange(V x):阻塞当前线程,直到另外一个线程调用exchange方法或者当前线程被中断. x : 需要交换的对象. exchange(V x, long timeout, TimeUnit unit):阻塞

  • Java并发编程之LockSupport类详解

    一.LockSupport类的属性 private static final sun.misc.Unsafe UNSAFE; // 表示内存偏移地址 private static final long parkBlockerOffset; // 表示内存偏移地址 private static final long SEED; // 表示内存偏移地址 private static final long PROBE; // 表示内存偏移地址 private static final long SEC

  • Java并发编程之ConcurrentLinkedQueue源码详解

    一.ConcurrentLinkedQueue介绍 并编程中,一般需要用到安全的队列,如果要自己实现安全队列,可以使用2种方式: 方式1:加锁,这种实现方式就是我们常说的阻塞队列. 方式2:使用循环CAS算法实现,这种方式实现队列称之为非阻塞队列. 从点到面, 下面我们来看下非阻塞队列经典实现类:ConcurrentLinkedQueue (JDK1.8版) ConcurrentLinkedQueue 是一个基于链接节点的无界线程安全的队列.当我们添加一个元素的时候,它会添加到队列的尾部,当我们

随机推荐