彻底理解ThreadLocal

转载自 彻底理解ThreadLocal

 

先总述,后分析

  深挖过threadLocal之后,一句话概括:Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。所以ThreadLocal的应用场合,最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

  数据隔离的秘诀其实是这样的,Thread有个TheadLocalMap类型的属性,叫做threadLocals,该属性用来保存该线程本地变量。这样每个线程都有自己的数据,就做到了不同线程间数据的隔离,保证了数据安全。

  接下来采用jdk1.8源码进行深挖一下TheadLocal和TheadLocalMap。

ThreadLocal是什么

  早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

  当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

  从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

  所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。

 

原理

  ThreadLocal,连接ThreadLocalMap和Thread。来处理Thread的TheadLocalMap属性,包括init初始化属性赋值、get对应的变量,set设置变量等。通过当前线程,获取线程上的ThreadLocalMap属性,对数据进行get、set等操作。

  ThreadLocalMap,用来存储数据,采用类似hashmap机制,存储了以threadLocal为key,需要隔离的数据为value的Entry键值对数组结构。

  ThreadLocal,有个ThreadLocalMap类型的属性,存储的数据就放在这儿。

ThreadLocal、ThreadLocal、Thread之间的关系

  ThreadLocalMap是ThreadLocal内部类,由ThreadLocal创建,Thread有ThreadLocal.ThreadLocalMap类型的属性。源码如下:

Thread的属性:

public  class Thread implements Runnable {  /*...其他属性...*/  /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */  ThreadLocal.ThreadLocalMap threadLocals = null;  /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */  ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

ThreadLocal和ThreadLocalMap

public class ThreadLocal<T> {  /**..其他属性和方法稍后介绍...*/  /** * ThreadLocalMap is a customized hash map suitable only for * maintaining thread local values. No operations are exported * outside of the ThreadLocal class. The class is package private to * allow declaration of fields in class Thread.  To help deal with * very large and long-lived usages, the hash table entries use * WeakReferences for keys. However, since reference queues are not * used, stale entries are guaranteed to be removed only when * the table starts running out of space. */  static class ThreadLocalMap {  

由ThreadLocal对Thread的TreadLocalMap进行赋值

 

/** * 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 */  
void createMap(Thread t, T firstValue) {  t.threadLocals = new ThreadLocalMap(this, firstValue);  
}

ThreadLocal的接口方法

ThreadLocal类核心方法set、get、initialValue、withInitial、setInitialValue、remove:

/** * 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 {@code 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}. * * <p>This implementation simply returns {@code null}; if the * programmer desires thread-local variables to have an initial * value other than {@code null}, {@code 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;  }  /** * Creates a thread local variable. The initial value of the variable is * determined by invoking the {@code get} method on the {@code Supplier}. * * @param <S> the type of the thread local's value * @param supplier the supplier to be used to determine the initial value * @return a new thread local variable * @throws NullPointerException if the specified supplier is null * @since 1.8 */  public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {  return new SuppliedThreadLocal<>(supplier);  }  /** * Creates a thread local variable. * @see #withInitial(java.util.function.Supplier) */  public ThreadLocal() {  }  /** * 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);  if (map != null) {  ThreadLocalMap.Entry e = map.getEntry(this);  if (e != null) {  @SuppressWarnings("unchecked")  T result = (T)e.value;  return result;  }  }  return 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();  Thread t = Thread.currentThread();  ThreadLocalMap map = getMap(t);  if (map != null)  map.set(this, value);  else  createMap(t, value);  return value;  }  /** * Sets the current thread's copy of this thread-local variable * to the specified value.  Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of *        this thread-local. */  public void set(T value) {  Thread t = Thread.currentThread();  ThreadLocalMap map = getMap(t);  if (map != null)  map.set(this, value);  else  createMap(t, value);  }  /** * Removes the current thread's value for this thread-local * variable.  If this thread-local variable is subsequently * {@linkplain #get read} by the current thread, its value will be * reinitialized by invoking its {@link #initialValue} method, * unless its value is {@linkplain #set set} by the current thread * in the interim.  This may result in multiple invocations of the * {@code initialValue} method in the current thread. * * @since 1.5 */  public void remove() {  ThreadLocalMap m = getMap(Thread.currentThread());  if (m != null)  m.remove(this);  }  
  • initialValue返回该线程局部变量的初始值。该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
  • withInitial提供一个Supplier的lamda表达式用来当做初始值,java8引入。
  • setInitialValue设置初始值。在get操作没有对应的值时,调用此方法。private方法,防止被覆盖。过程和set类似,只不过是用initialValue作为value进行设置。
  • set设置当前线程对应的线程局部变量的值。先取出当前线程对应的threadLocalMap,如果不存在则用创建一个,否则将value放入以this,即threadLocal为key的映射的map中,其实threadLocalMap内部和hashMap机制一样,存储了Entry键值对数组,后续会深挖threadLocalMap。
  • get该方法返回当前线程所对应的线程局部变量。和set类似,也是先取出当前线程对应的threadLocalMap,如果不存在则用创建一个,但是是用inittialValue作为value放入到map中,且返回initialValue,否则就直接从map取出this即threadLocal对应的value返回。
  • remove将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。需要注意的是如果remove之后又调用了get,会重新初始化一次,即再次调用initialValue方法,除非在get之前调用set设置过值。

ThreadLocalMap简介

  看名字就知道是个map,没错,这就是个hashMap机制实现的map,用Entry数组来存储键值对,key是ThreadLocal对象,value则是具体的值。值得一提的是,为了方便GC,Entry继承了WeakReference,也就是弱引用。里面有一些具体关于如何清理过期的数据、扩容等机制,思路基本和hashmap差不多,有兴趣的可以自行阅读了解,这边只需知道大概的数据存储结构即可。

/** * ThreadLocalMap is a customized hash map suitable only for * maintaining thread local values. No operations are exported * outside of the ThreadLocal class. The class is package private to * allow declaration of fields in class Thread.  To help deal with * very large and long-lived usages, the hash table entries use * WeakReferences for keys. However, since reference queues are not * used, stale entries are guaranteed to be removed only when * the table starts running out of space. */  
static class ThreadLocalMap {  /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object).  Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table.  Such entries are referred to * as "stale entries" in the code that follows. */  static class Entry extends WeakReference<ThreadLocal<?>> {  /** The value associated with this ThreadLocal. */  Object value;  Entry(ThreadLocal<?> k, Object v) {  super(k);  value = v;  }  }  

Thread同步机制的比较

  ThreadLocal和线程同步机制相比有什么优势呢?

  Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。

  在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

  而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

  概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

  Spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。

  一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。

 

  同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。

  下面的实例能够体现Spring对有状态Bean的改造思路:

代码清单3 TestDao:非线程安全

package com.test;  import java.sql.Connection;  
import java.sql.SQLException;  
import java.sql.Statement;  public class TestDao {  private Connection conn;// ①一个非线程安全的变量  public void addTopic() throws SQLException {  Statement stat = conn.createStatement();// ②引用非线程安全变量  // …  }  
}  

由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:

代码清单4 TestDao:线程安全

package com.test;  import java.sql.Connection;  
import java.sql.SQLException;  
import java.sql.Statement;  public class TestDaoNew {// ①使用ThreadLocal保存Connection变量  private static ThreadLocal<Connection> connThreadLocal = ThreadLocal.withInitial(Test::createConnection);  // 具体创建数据库连接的方法  private static Connection createConnection() {  Connection result = null;  /** * create a real connection... * such as : * result = DriverManager.getConnection(dbUrl, dbUser, dbPwd); */  return result;  }  // ③直接返回线程本地变量  public static Connection getConnection() {  return connThreadLocal.get();  }  // 具体操作  public void addTopic() throws SQLException {  // ④从ThreadLocal中获取线程对应的Connection  Statement stat = getConnection().createStatement();  //....any other operation  }  
}  

不同的线程在使用TopicDao时,根据之前的深挖get具体操作,判断connThreadLocal.get()会去判断是有map,没有则根据initivalValue创建一个Connection对象并添加到本地线程变量中,initivalValue对应的值也就是上述的lamba表达式对应的创建connection的方法返回的结果,下次get则由于已经有了,则会直接获取已经创建好的Connection,这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。

  当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。

 

ConnectionManager.java

package com.test;  import java.sql.Connection;  
import java.sql.DriverManager;  
import java.sql.SQLException;  public class ConnectionManager {  private static ThreadLocal<Connection> connectionHolder = ThreadLocal.withInitial(() -> {  Connection conn = null;  try {  conn = DriverManager.getConnection(  "jdbc:mysql://localhost:3306/test", "username",  "password");  } catch (SQLException e) {  e.printStackTrace();  }  return conn;  });  public static Connection getConnection() {  return connectionHolder.get();  }  
}  

线程隔离的秘密

秘密就就在于上述叙述的ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。


为了加深理解,我们接着看上面代码中出现的getMap和createMap方法的实现:

 

 

 

/** * Get the map associated with a ThreadLocal. Overridden in * InheritableThreadLocal. * * @param  t the current thread * @return the map */  
ThreadLocalMap getMap(Thread t) {  return t.threadLocals;  
}  /** * 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);  
}  

小结

  ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

 

后记

  看到网友评论的很激烈,甚至关于ThreadLocalMap不是ThreadLocal里面的,而是Thread里面的这种评论都出现了,于是有了这个后记,下面先把jdk源码贴上,源码最有说服力了。

/** * ThreadLocalMap is a customized hash map suitable only for * maintaining thread local values. No operations are exported * outside of the ThreadLocal class. The class is package private to * allow declaration of fields in class Thread.  To help deal with * very large and long-lived usages, the hash table entries use * WeakReferences for keys. However, since reference queues are not * used, stale entries are guaranteed to be removed only when * the table starts running out of space. */  static class ThreadLocalMap {...}  


  源码就是以上,这源码自然是在ThreadLocal里面的,有截图为证。

 

  本文是自己在学习ThreadLocal的时候,一时兴起,深入看了源码,思考了此类的作用、使用范围,进而联想到对传统的synchronize共享变量线程安全的问题进行比较,而总结的博文,总结一句话就是一个是锁机制进行时间换空间,一个是存储拷贝进行空间换时间。

 

 

 

 

 

 

 

 

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/326992.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

云计算设计模式(四)——消费者的竞争模式

允许多个并发用户处理在同一个通讯通道接收的消息。这种模式使系统能够同时处理多个邮件&#xff0c;以优化吞吐量&#xff0c;提高可扩展性和可用性&#xff0c;以及平衡工作负载。 背景和问题 在云中运行的应用程序&#xff0c;可以预计&#xff0c;以处理大量的请求。而不是…

前后端分离趋势谈

最近已经不止一个人和我提起过vue了&#xff0c;在我的前端印象中&#xff0c;我还停留在smarty渲染模版&#xff0c;jquery做js处理。学了一晚上&#xff0c;对现在这种工程化webpack打包生成html&#xff0c;js&#xff0c;css的生产方式越来越有兴趣了。工作年限摆在这里的好…

云计算设计模式(五)——计算资源整合模式

合并多个任务或操作成一个单一的计算单元。这种模式可以提高计算资源的利用率&#xff0c;并降低与云托管的应用程序进行计算处理相关的成本和管理开销。 背景和问题 云应用程序频繁执行各种操作。在某些解决方案也可能是有意义的最初遵循的关注点分离的设计原则&#xff0c;并…

Visual Studio 2017 RC3支持.NET Core,延迟对Python的支持

Visual Studio 2017第三个候选版本上周发布&#xff0c;解决了之前发现的安装程序的小问题。由于这些问题得到了解决&#xff0c;现在值得关注的就是这次版本中更新了什么内容。&#xff08;版本是发布于1月27日的build 26127.00&#xff09; RC3版本中最值得关注的部分就是对N…

虚拟研讨会:.NET的未来在哪里?

.NET生态系统在过去的一年中发生了很多事情。在几个方面发展非常迅速&#xff1a;Xamari、UWP、.NET Core、.NET native、F#和开源等等。 如果要关注细节&#xff0c;那大的景象难以描绘。因为在每个方面都有新的动作&#xff1a;跨平台、云、移动、Web应用和通用应用。开发人员…

使用Servlet上传多张图片——访问提示

上传文件&#xff0c;我们在做项目中补课避免的&#xff0c;有时候我们需要上传单张或者单个文件&#xff0c;但是有时候我们就需要上传多个文件或者多张图片了&#xff0c;我们这里以多张&#xff08;4张&#xff09;图片为例&#xff0c;再多也都是一样的概念&#xff0c;接下…

云计算设计模式(六)——命令和查询职责分离(CQRS)模式

隔离&#xff0c;通过使用不同的接口&#xff0c;从操作读取数据更新数据的操作。这种模式可以最大限度地提高性能&#xff0c;可扩展性和安全性;支持系统在通过较高的灵活性&#xff0c;时间的演变;防止更新命令&#xff0c;从造成合并在域级别上的冲突。 背景和问题 在传统的…

Intellij IDEA 那些隐藏好用的小技巧

转载自 Intellij IDEA 那些隐藏好用的小技巧 概述 之前写了一篇介绍IntellIJ IDEA的文章《 Intellij Idea非常6的10个姿势 》&#xff0c;主要是列出一些平时大家可能没用过或者没怎么用&#xff0c;但是又非常好用的IntellIJ IDEA小技巧。由于篇幅原因&#xff0c;只是列出了…

约瑟夫(环)问题(Josephu)(单向环形链表)

问题描述 代码实现 package com.atguigu.linkedlist;import com.sun.org.apache.bcel.internal.generic.NEW;/*** 创建人 wdl* 创建时间 2021/3/19* 描述*/ public class Josepfu {public static void main(String[] args) {//测试一把看看构建的环形链表和遍历是否正确Circle…

vue+vscode+nodejs 开发环境搭建

参考文献 vuevscodenodejs 开发环境搭建 - Desperador - 博客园 nodejs 指定全局安装路径和缓存路径 - Curedfisher - 博客园 安装配置nodejs并创建Vue项目 vscode下载地址&#xff1a; Documentation for Visual Studio Code nodejs安装配置 1.下载 地址&#xff1a; …

用数组模拟栈

思路分析 代码实现 package com.atguigu.stack;import com.sun.org.apache.xml.internal.dtm.ref.sax2dtm.SAX2DTM2;import java.net.ServerSocket; import java.util.ArrayList; import java.util.Scanner; import java.util.Stack;/*** 创建人 wdl* 创建时间 2021/3/20* 描述…

Redis PK Memcached,哪个更牛叉

转载自 Redis PK Memcached&#xff0c;哪个更牛叉 说到 redis 就会联想到 memcached&#xff0c;反之亦然。了解过两者的同学有那么个大致的印象&#xff1a; redis 与 memcached 相比&#xff0c;不仅支持简单的 key-value 数据类型&#xff0c;同时还提供 list,set,zset,ha…

CoreCLR源码探索(三) GC内存分配器的内部实现

在前一篇中我讲解了new是怎么工作的, 但是却一笔跳过了内存分配相关的部分.在这一篇中我将详细讲解GC内存分配器的内部实现.在看这一篇之前请必须先看完微软BOTR文档中的"Garbage Collection Design",原文地址是: https://github.com/dotnet/coreclr/blob/master/Doc…

vue学习1

P1 01_Vue学习目标03:50 P2 02_前端知识体系16:27 P3 03_前后端分离的演变史17:13 P4 04_前端MVVM模式09:31 P5 05_Vue是什么07:23 P6 06_第一个Vue应用程序07:06 P7 07_Vue实例声明周期05:35 P8 08_条件渲染06:59 P9 09_列表渲染03:34 P10 10_事件处理03:44…

Tomcat 的 Server 文件配置详解

转载自 Tomcat 的 Server 文件配置详解 前言 Tomcat隶属于Apache基金会&#xff0c;是开源的轻量级Web应用服务器&#xff0c;使用非常广泛。server.xml是Tomcat中最重要的配置文件&#xff0c;server.xml的每一个元素都对应了Tomcat中的一个组件&#xff1b;通过对xml文件中…

.Net基础体系和跨框架开发普及

.net体系经过十几年发展&#xff0c;发生了很多变化。特别是在最近两年&#xff0c;随着开源和跨平台的发展&#xff0c;衍生出很多概念&#xff0c;像标准库&#xff0c;可移植库&#xff0c;.Net Core等&#xff0c;相信有不少同学对他们之间的关系是有一些困惑的&#xff0c…

‘1‘ VS 1

‘1’-481; 把字符转换为数字&#xff0c;利用ASCALL表