优质文章,及时送达

Spring与线程安全

Spring作为一个IOC/DI容器,帮助我们管理了许许多多的“bean”。但其实,Spring并没有保证这些对象的线程安全,需要由开发者自己编写解决线程安全问题的代码。

Spring对每个bean提供了一个scope属性来表示该bean的作用域。它是bean的生命周期。例如,一个scope为singleton的bean,在第一次被注入时,会创建为一个单例对象,该对象会一直被复用到应用结束。

  • singleton:默认的scope,每个scope为singleton的bean都会被定义为一个单例对象,该对象的生命周期是与Spring IOC容器一致的(但在第一次被注入时才会创建)。

  • prototype:bean被定义为在每次注入时都会创建一个新的对象。

  • request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象。

  • session:bean被定义为在一个session的生命周期内创建一个单例对象。

  • application:bean被定义为在ServletContext的生命周期中复用一个单例对象。

  • websocket:bean被定义为在websocket的生命周期中复用一个单例对象。

我们交由Spring管理的大多数对象其实都是一些无状态的对象,这种不会因为多线程而导致状态被破坏的对象很适合Spring的默认scope,每个单例的无状态对象都是线程安全的(也可以说只要是无状态的对象,不管单例多例都是线程安全的,不过单例毕竟节省了不断创建对象与GC的开销)。

无状态的对象即是自身没有状态的对象,自然也就不会因为多个线程的交替调度而破坏自身状态导致线程安全问题。无状态对象包括我们经常使用的DO、DTO、VO这些只作为数据的实体模型的贫血对象,还有Service、DAO和Controller,这些对象并没有自己的状态,它们只是用来执行某些操作的。例如,每个DAO提供的函数都只是对数据库的CRUD,而且每个数据库Connection都作为函数的局部变量(局部变量是在用户栈中的,而且用户栈本身就是线程私有的内存区域,所以不存在线程安全问题),用完即关(或交还给连接池)。

有人可能会认为,我使用request作用域不就可以避免每个请求之间的安全问题了吗?这是完全错误的,因为Controller默认是单例的,一个HTTP请求是会被多个线程执行的,这就又回到了线程的安全问题。当然,你也可以把Controller的scope改成prototype,实际上Struts2就是这么做的,但有一点要注意,Spring MVC对请求的拦截粒度是基于每个方法的,而Struts2是基于每个类的,所以把Controller设为多例将会频繁的创建与回收对象,严重影响到了性能。

通过阅读上文其实已经说的很清楚了,Spring根本就没有对bean的多线程安全问题做出任何保证与措施。对于每个bean的线程安全问题,根本原因是每个bean自身的设计。不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock、CAS等这些实现线程同步的方法了。

下面将通过解析ThreadLocal的源码来了解它的实现与作用,ThreadLocal是一个很好用的工具类,它在某些情况下解决了线程安全问题(在变量不需要被多个线程共享时)。

公众号 Java后端 发布的关于 Spring 相关的文章,我整理成了 PDF ,关注公众号 Java后端 ,回复 666 下载。

ThreadLocal

ThreadLocal是一个为线程提供线程局部变量的工具类。它的思想也十分简单,就是为线程提供一个线程私有的变量副本,这样多个线程都可以随意更改自己线程局部的变量,不会影响到其他线程。不过需要注意的是,ThreadLocal提供的只是一个浅拷贝,如果变量是一个引用类型,那么就要考虑它内部的状态是否会被改变,想要解决这个问题可以通过重写ThreadLocal的initialValue函数来自己实现深拷贝,建议在使用ThreadLocal时一开始就重写该函数。

ThreadLocal与像synchronized这样的锁机制是不同的。首先,它们的应用场景与实现思路就不一样,锁更强调的是如何同步多个线程去正确地共享一个变量,ThreadLocal则是为了解决同一个变量如何不被多个线程共享。从性能开销的角度上来讲,如果锁机制是用时间换空间的话,那么ThreadLocal就是用空间换时间。

ThreadLocal中含有一个叫做ThreadLocalMap的内部类,该类为一个采用线性探测法实现的HashMap。它的key为ThreadLocal对象而且还使用了WeakReference,ThreadLocalMap正是用来存储变量副本的。

/*** 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.*/staticclassThreadLocalMap{/*** The entries in this hash map extend WeakReference, using* its main ref field as the key (which is always a* ThreadLocal object). Note that keys (i.e. entry.get* == ) 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> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal> k, Object v) {super(k);value = v;}}....}

ThreadLocal中只含有三个成员变量,这三个变量都是与ThreadLocalMap的hash策略相关的。

/*** ThreadLocals rely on per-thread linear-probe hash maps attached* to each thread (Thread.threadLocals and* inheritableThreadLocals). The ThreadLocal objects act as keys,* searched via threadLocalHashCode. This is a custom hash code* (useful only within ThreadLocalMaps) that eliminates collisions* in the common case where consecutively constructed ThreadLocals* are used by the same threads, while remaining well-behaved in* less common cases.*/private final int threadLocalHashCode = nextHashCode;

/*** The next hash code to be given out. Updated atomically. Starts at* zero.*/private static AtomicInteger nextHashCode =new AtomicInteger;

/*** The difference between successively generated hash codes - turns* implicit sequential thread-local IDs into near-optimally spread* multiplicative hash values for power-of-two-sized tables.*/private static final int HASH_INCREMENT = 0x61c88647;

/*** Returns the next hash code.*/privatestaticintnextHashCode {return nextHashCode.getAndAdd(HASH_INCREMENT);}

唯一的实例变量threadLocalHashCode是用来进行寻址的hashcode,它由函数nextHashCode生成,该函数简单地通过一个增量HASH_INCREMENT来生成hashcode。

至于为什么这个增量为0x61c88647,主要是因为ThreadLocalMap的初始大小为16,每次扩容都会为原来的2倍,这样它的容量永远为2的n次方,该增量选为0x61c88647也是为了尽可能均匀地分布,减少碰撞冲突。

/*** The initial capacity -- MUST be a power of two.*/private static final int INITIAL_CAPACITY = 16;

/*** Construct a new map initially containing (firstKey, firstValue).* ThreadLocalMaps are constructed lazily, so we only create* one when we have at least one entry to put in it.*/ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}

要获得当前线程私有的变量副本需要调用get函数。首先,它会调用getMap函数去获得当前线程的ThreadLocalMap,这个函数需要接收当前线程的实例作为参数。如果得到的ThreadLocalMap为,那么就去调用setInitialValue函数来进行初始化,如果不为,就通过map来获得变量副本并返回。

setInitialValue函数会去先调用initialValue函数来生成初始值,该函数默认返回,我们可以通过重写这个函数来返回我们想要在ThreadLocal中维护的变量。之后,去调用getMap函数获得ThreadLocalMap,如果该map已经存在,那么就用新获得value去覆盖旧值,否则就调用createMap函数来创建新的map。

/*** 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 != ) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != ) {@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 != )map.set(this, value);elsecreateMap(t, value);return value;}

protected T initialValue {return ;}

ThreadLocal的set与remove函数要比get的实现还要简单,都只是通过getMap来获得ThreadLocalMap然后对其进行操作。

/*** 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 != )map.set(this, value);elsecreateMap(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 != )m.remove(this);}

getMap函数与createMap函数的实现也十分简单,但是通过观察这两个函数可以发现一个秘密:ThreadLocalMap是存放在Thread中的。

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

// Thread中的源码

/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = ;

/** InheritableThreadLocal values pertaining to this thread. This map is* maintained by the InheritableThreadLocal class.*/ThreadLocal.ThreadLocalMap inheritableThreadLocals = ;

仔细想想其实就能够理解这种设计的思想。有一种普遍的方法是通过一个全局的线程安全的Map来存储各个线程的变量副本,但是这种做法已经完全违背了ThreadLocal的本意,设计ThreadLocal的初衷就是为了避免多个线程去并发访问同一个对象,尽管它是线程安全的。而在每个Thread中存放与它关联的ThreadLocalMap是完全符合ThreadLocal的思想的,当想要对线程局部变量进行操作时,只需要把Thread作为key来获得Thread中的ThreadLocalMap即可。这种设计相比采用一个全局Map的方法会多占用很多内存空间,但也因此不需要额外的采取锁等线程同步方法而节省了时间上的消耗。

ThreadLocal中的内存泄漏

我们要考虑一种会发生内存泄漏的情况,如果ThreadLocal被设置为后,而且没有任何强引用指向它,根据垃圾回收的可达性分析算法,ThreadLocal将会被回收。这样一来,ThreadLocalMap中就会含有key为的Entry,而且ThreadLocalMap是在Thread中的,只要线程迟迟不结束,这些无法访问到的value会形成内存泄漏。为了解决这个问题,ThreadLocalMap中的getEntry、set和remove函数都会清理key为的Entry,以下面的getEntry函数的源码为例。

/*** Get the entry associated with key. This method* itself handles only the fast path: a direct hit of existing* key. It otherwise relays to getEntryAfterMiss. This is* designed to maximize performance for direct hits, in part* by making this method readily inlinable.** @param key the thread local object* @return the entry associated with key, or if no such*/private Entry getEntry(ThreadLocal> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != && e.get == key)return e;elsereturn getEntryAfterMiss(key, i, e);}

/*** Version of getEntry method for use when key is not found in* its direct hash slot.** @param key the thread local object* @param i the table index for key's hash code* @param e the entry at table[i]* @return the entry associated with key, or if no such*/private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) {Entry tab = table;int len = tab.length;

// 清理key为的Entrywhile (e != ) {ThreadLocal> k = e.get;if (k == key)return e;if (k == )expungeStaleEntry(i);elsei = nextIndex(i, len);e = tab[i];}return ;}

在上文中我们发现了ThreadLocalMap的key是一个弱引用,那么为什么使用弱引用呢?使用强引用key与弱引用key的差别如下:

  • 强引用key:ThreadLocal被设置为,由于ThreadLocalMap持有ThreadLocal的强引用,如果不手动删除,那么ThreadLocal将不会回收,产生内存泄漏。

  • 弱引用key:ThreadLocal被设置为,由于ThreadLocalMap持有ThreadLocal的弱引用,即便不手动删除,ThreadLocal仍会被回收,ThreadLocalMap在之后调用set、getEntry和remove函数时会清除所有key为的Entry。

但要注意的是,ThreadLocalMap仅仅含有这些被动措施来补救内存泄漏问题。如果你在之后没有调用ThreadLocalMap的set、getEntry和remove函数的话,那么仍然会存在内存泄漏问题。

在使用线程池的情况下,如果不及时进行清理,内存泄漏问题事小,甚至还会产生程序逻辑上的问题。所以,为了安全地使用ThreadLocal,必须要像每次使用完锁就解锁一样,在每次使用完ThreadLocal后都要调用remove来清理无用的Entry。

参考文献

https://stackoverflow.com/questions/15745140/are-spring-objects-thread-safe

https://tarunsapra.wordpress.com/2011/08/21/spring-singleton-request-session-beans-and-thread-safety/

https://docs.spring.io/spring/docs/current/spring-framework-reference/index.html

作者

作者:SylvanasSun

链接:juejin.im/post/5a0045ef5188254de169968e

-END-

如果看到这里,说明你喜欢这篇文章,请 转发、点赞。同时标星(置顶)本公众号可以第一时间接受到博文推送。

1. 强烈推荐 16 款 IDEA 插件

最近整理一份面试资料《Java技术栈学习手册》,覆盖了Java技术、面试题精选、Spring全家桶、Nginx、SSM、微服务、数据库、数据结构、架构等等。

controller如何保证当前只有一个线程执行_今天我们来聊一聊 Spring 中的线程安全性...相关推荐

  1. 聊一聊Spring中的线程安全性

    原文出处:SylvanasSun Spring与线程安全 ThreadLocal ThreadLocal中的内存泄漏 参考文献 Spring与线程安全 Spring作为一个IOC/DI容器,帮助我们管 ...

  2. 聊一聊 Spring 中的线程安全性

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者 | SylvanasSun 来源 | juejin.im/p ...

  3. 今天我们来聊一聊 Spring 中的线程安全性

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 Spring与线程安全 Spring作为一个IOC/DI容器,帮助 ...

  4. java进阶 线程池 join用法总结:thread4.join();方法,就表明thread4.join();这个线程受到贵客待遇,直到这个线程执行完,被插入这个方法的载体线程才可以执行。

    那个线程调用join 举例 thread4.join();方法,就表明thread4.join();这个线程受到贵客待遇,直到这个线程执行完,被插入这个方法的载体线程才可以执行. package ja ...

  5. c++ 线程池_基础篇:高并发一瞥,线程和线程池的总结

    进程是执行程序的实体,拥有独属的进程空间(内存.磁盘等).而线程是进程的一个执行流程,一个进程可包含多个线程,共享该进程的所有资源:代码段,数据段(全局变量和静态变量),堆存储:但每个线程拥有自己的执 ...

  6. controller如何保证当前只有一个线程执行_聊聊Spring线程安全,读完之后,大部分程序员收藏了......

    Spring作为一个IOC/DI容器,帮助我们管理了许许多多的"bean".但其实,Spring并没有保证这些对象的线程安全,需要由开发者自己编写解决线程安全问题的代码. Spri ...

  7. 多线程线程池的实现java_如何在Java中实现线程池

    多线程线程池的实现java 线程是独立程序的执行路径. 在java中,每个线程都扩展java.lang.Thread类或实现java.lang.Runnable. 多线程是指在一个任务中同时执行两个或 ...

  8. 百度java的线程技术_自我提升(基础技术篇)——java线程简介

    前言:虽然自己平时都在用多线程,也能完成基本的工作需求,但总觉得,还是对线程没有一个系统的概念,所以,查阅了一些资料,理解那些大神和官方的资料,写这么一篇关于线程的文章 本来想废话一番,讲讲自己的经历 ...

  9. oracle trigger 延迟执行_一文详解Spring任务执行和调度

    一.概述 Spring框架分别使用TaskExecutor和TaskScheduler接口提供异步执行和任务调度的抽象.Spring还提供了这些接口的实现,这些接口支持线程池或将其委托给应用服务器环境 ...

最新文章

  1. 理解人类世界的常识对于人工智能是一项挑战
  2. 电子书下载|2020 年云原生年货小红书来啦!
  3. NET Core的代码安全分析工具 - Security Code Scan
  4. .NET ORM FreeSql 第一个正式版本发布 v1.0.0
  5. 电大计算机一级b考试试题,电大计算机考试本科试题
  6. Bootstrap列表组的情景类
  7. mysql找不到服务_win7系统安装mysql后找不到服务或提示找不到指定文件如何解决...
  8. 机器学习——LASSO算法
  9. java openoffic linux_Linux openoffice 安装测试
  10. 漫威电影和程序员、Git 到底有什么关系?
  11. Windows 系统安装
  12. C#Aspose操作Word Excel简版(后会研究补充更多功能)
  13. Java调用百度AI开放平台API
  14. php 获取带宽,php使用网络攻击占用超大带宽的,解决方案[转]
  15. Kafka 多话题消费者
  16. 美国计算机视觉专业排名,2020美国人工智能专业排名TOP10!
  17. html 合并单元格 步骤总结
  18. 2015移动安全病毒年报
  19. javascript求1~100的素数和
  20. Python实现数据预处理-离散值处理

热门文章

  1. Redis基础、应用、第三方支持组件总结
  2. MySQL解压版配置
  3. java中@Qualifier(string)是什么用法
  4. 【更新】比较智能的爬取姓名
  5. 在spring中,prototype scoped bean 如何注入单例bean中
  6. 在PHP中实现StringBuilder类
  7. Solr索引数据同步ReplicationHandler
  8. ssh协议是osi_TCP/IP 协议模型
  9. qtablewidget限制输入类型_对敏感型电子信号输入实施过压保护的可靠新方法
  10. 建立自己的voc数据集_一次将自己的数据集制作成PASCAL VOC格式的惨痛经历