• 问题背景

这段时间在做项目的时候,考虑到Spring中的bean默认是单例模式的,那么当多个线程调用同一个bean的时候就会存在线程安全问题。如果是Spring中bean的创建模式为非单例的,也就不存在这样的问题了。

  • Spring 单例模式与线程安全

Spring 框架里的 bean ,或者说组件,获取实例的时候都是默认的单例模式,这是在多线程开发的时候要尤其注意的地方。 单例模式的意思就是只有一个实例。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。

当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求多对应的业务逻辑(成员方法),此时就要注意了,如果该处理逻辑中有对该单列状态的修改(体现为该单列的成员属性),则必须考虑线程同步问题。

同步机制的比较: ThreadLocal 和线程同步机制相比有什么优势呢? ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

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

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

由于 ThreadLocal 中可以持有任何类型的对象,低版本 JDK 所提供的 get() 返回的是 Object 对象,需要强制类型转换。但 JDK 5.0 通过泛型很好的解决了这个问题,在一定程度地简化 ThreadLocal 的使用。 括起来说,对于多线程资源共享的问题,同步机制采用了 “ 以时间换空间 ” 的方式,而 ThreadLocal 采用了 “ 以空间换时间 ” 的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

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

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

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

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 或者说 : 一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性 , 也就是说我们不用考虑同步的问题。  线程安全问题都是由全局变量及静态变量引起的。

若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

1 ) 常量始终是线程安全的,因为只存在读操作。

2 )每次调用方法前都新建一个实例是线程安全的,因为不会访问共享的资源。

3 )局部变量是线程安全的。因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。局部变量包括方法的参数变量和方法内变量。

有状态就是有数据存储功能。有状态对象 (Stateful Bean) ,就是有实例变量的对象   ,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。

无状态就是一次操作,不能保存数据。无状态对象 (Stateless Bean) ,就是没有实例变量的对象   . 不能保存数据,是不变类,是线程安全的。

有状态对象 :

无状态的 Bean 适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。有状态的 Bean ,多线程环境下不安全,那么适合用 Prototype 原型模式。Prototype:  每次对 bean 的请求都会创建一个新的 bean 实例。

Struts2 默认的实现是 Prototype 模式。也就是每个请求都新生成一个 Action 实例,所以不存在线程安全问题。需要注意的是,如果由 Spring 管理 action 的生命周期, scope 要配成 prototype 作用域。

  • 线程安全案例

SimpleDateFormat( 下面简称 sdf) 类内部有一个 Calendar 对象引用 , 它用来储存和这个 sdf 相关的日期信息 , 例如 sdf.parse(dateStr), sdf.format(date)  诸如此类的方法参数传入的日期相关 String, Date 等等 ,  都是交友 Calendar 引用来储存的 . 这样就会导致一个问题 , 如果你的 sdf 是个 static 的 ,  那么多个 thread  之间就会共享这个 sdf, 同时也是共享这个 Calendar 引用 ,  并且 ,  观察  sdf.parse()  方法 , 你会发现有如下的调用 :

1 Date parse() {
2 calendar.clear(); // 清理calendar
3 ... // 执行一些操作, 设置 calendar 的日期什么的
4 calendar.getTime(); // 获取calendar的时间
5 }

这里会导致的问题就是 ,  如果 线程 A  调用了  sdf.parse(),  并且进行了 calendar.clear() 后还未执行 calendar.getTime() 的时候 , 线程 B 又调用了 sdf.parse(), 这时候线程 B 也执行了 sdf.clear() 方法 ,  这样就导致线程 A 的的 calendar 数据被清空了 ( 实际上 A,B 的同时被清空了 ).  又或者当  A  执行了 calendar.clear()  后被挂起 ,  这时候 B  开始调用 sdf.parse() 并顺利 i 结束 ,  这样  A  的  calendar 内存储的的 date 变成了后来 B 设置的 calendar 的 date

这个问题背后隐藏着一个更为重要的问题 -- 无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。 format 方法在运行过程中改动了SimpleDateFormat 的 calendar 字段,所以,它是有状态的。

这也同时提醒我们在开发和设计系统的时候注意下一下三点 :

1. 自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明

2. 对线程环境下,对每一个共享的可变变量都要注意其线程安全性

3. 我们的类和方法在做设计的时候,要尽量设计成无状态的

  • 解决办法

1. 需要的时候创建新实例:

说明:在需要用到 SimpleDateFormat  的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。在一般情况下,这样其实对性能影响比不是很明显的。

2. 使用同步:同步 SimpleDateFormat 对象

 1 public class DateSyncUtil {
2 private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
3
4 public static String formatDate(Date date)throws ParseException{
5 synchronized(sdf){
6 return sdf.format(date);
7 }
8 }
9
10 public static Date parse(String strDate) throws ParseException{
11 synchronized(sdf){
12 return sdf.parse(strDate);
13 }
14 }
15 }
说明:当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block ,多线程并发量大的时候会对性能有一定的影响。

3. 使用 ThreadLocal :

 1 public class ConcurrentDateUtil {
2 private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
3 @Override
4 protected DateFormat initialValue() {
5 return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
6 }
7 };
8 public static Date parse(String dateStr) throws ParseException {
9 return threadLocal.get().parse(dateStr);
10 }
11 public static String format(Date date) {
12 return threadLocal.get().format(date);
13 }
14 }

 1 ThreadLocal<DateFormat>();
2
3 public static DateFormat getDateFormat()
4 {
5 DateFormat df = threadLocal.get();
6 if(df==null){
7 df = new SimpleDateFormat(date_format);
8 threadLocal.set(df);
9 }
10 return df;
11 }
12 public static String formatDate(Date date) throws ParseException {
13 return getDateFormat().format(date);
14 }
15 public static Date parse(String strDate) throws ParseException {
16 return getDateFormat().parse(strDate);
17 }
18 }

说明:使用 ThreadLocal,  也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。

4. 抛弃 JDK ,使用其他类库中的时间格式化类:

1. 使用 Apache commons  里的 FastDateFormat ,宣称是既快又线程安全的SimpleDateFormat,  可惜它只能对日期进行 format,  不能对日期串进行解析。

2. 使用 Joda-Time 类库来处理时间相关问题

做一个简单的压力测试,方法一最慢,方法三最快,但是就算是最慢的方法一性能也不差,一般系统方法一和方法二就可以满足,所以说在这个点很难成为你系统的瓶颈所在。从简单的角度来说,建议使用方法一或者方法二,如果在必要的时候,追求那么一点性能提升的话,可以考虑用方法三,用 ThreadLocal 做缓存。

Joda-Time 类库对时间处理方式比较完美,建议使用。

Spring单例模式与线程安全的更多相关文章

  1. 转-Spring单例模式与线程安全

    问题背景 这段时间在做项目的时候,考虑到Spring中的bean默认是单例模式的,那么当多个线程调用同一个bean的时候就会存在线程安全问题.如果是Spring中bean的创建模式为非单例的,也就不存 ...

  2. Spring单例模式多线程安全问题-有状态的Bean

    Spring单例与线程安全小结 一.Spring单例模式与线程安全 Spring框架里的bean,或者说组件,获取实例的时候都是默认的单例模式,这是在多线程开发的时候要尤其注意的地方. 单例模式的意思 ...

  3. spring提供的线程池

    SPRING中的线程池ThreadPoolTaskExecutor 分类: JAVA Spring2013-07-12 10:36 14896人阅读 评论(9) 收藏 举报 Spring线程池多线程 ...

  4. Spring中的线程池和定时任务功能

    1.功能介绍 Spring框架提供了线程池和定时任务执行的抽象接口:TaskExecutor和TaskScheduler来支持异步执行任务和定时执行任务功能.同时使用框架自己定义的抽象接口来屏蔽掉底层 ...

  5. spring boot自定义线程池以及异步处理

    spring boot自定义线程池以及异步处理@Async:什么是线程池?线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务.线程池线程都是后台线程.每个线程都使 ...

  6. 并发编程学习笔记(3)----synchronized关键字以及单例模式与线程安全问题

    再说synchronized关键字之前,我们首先先小小的了解一个概念-内置锁. 什么是内置锁? 在java中,每个java对象都可以用作synchronized关键字的锁,这些锁就被称为内置锁,每个对 ...

  7. JAVA设计模式-单例模式(Singleton)线程安全与效率

    一,前言 单例模式详细大家都已经非常熟悉了,在文章单例模式的八种写法比较中,对单例模式的概念以及使用场景都做了很不错的说明.请在阅读本文之前,阅读一下这篇文章,因为本文就是按照这篇文章中的八种单例模式 ...

  8. spring启动异步线程

    大纲: spring启动异步线程 spring配置线程池 一.spring启动异步线程 spring启动异步线程方法就是在方法上加上注解@Async,然后启动类或配置类上加上注解@EnableAsyn ...

  9. Spring 单例模式和多例模式

    1.Spring中的对象默认都是 单例模式. 2.使用 @Scope("prototype") 注解来使对象成为多例模式. 3.通过@Autowired 注入的Service 或者 ...

随机推荐

  1. nodejs 中自定义事件

    经常看到 req.on('error', function(){...}); 这种代码. 在nodejs中,可以使用 EventEmitter来实现. 具体的关键词有如下几个: var reqEven ...

  2. 使用eclipse开发Morphline的Java代码段

    背景:morphline是一个轻量级的etl工具.除了提供标准化的方法之外,还可以定制化的开发java片段.定制化的java片段会在加载时被作为一个独立的类编译,对源数据作处理. morphline关 ...

  3. 笔记整理之BCP

    很多时候,需要批量的导数据,可能大家想到的第一反应就是右键数据库->任务->导入导出数据.但是其实微软自身提供的大容量导入导出工具,有bcp, bulkinsert 之类的也是很好用.今天 ...

  4. Mvc model验证总结

    Model 验证总结 Model 验证总结 1 一.Model 验证标记 1 1.启用客户端验证: 1 2.在 Model 中加入验证标记 2 3 . Model 类中可以添加的验证标记: 3 3.1 ...

  5. redis 集群创建常见几个问题

    Redis配置集群遇到问题及解决方法   配置完所有主节点后,报" ERR Invalid node address specified" 由于Redis-trib.rb 对域名或 ...

  6. strcpy 函数的实现

    原型声明:extern char *strcpy(char *dest,const char *src); 头文件:string.h   功能:把从src地址开始且含有‘\0’结束符的字符串赋值到以d ...

  7. Caffe源码解析6:Neuron_Layer

    转载请注明出处,楼燚(yì)航的blog,http://home.cnblogs.com/louyihang-loves-baiyan/ NeuronLayer,顾名思义这里就是神经元,激活函数的相应 ...

  8. java1.8函数式编程概念

    有关函数式编程 ·1 函数作为一等公民 特点:将函数作为参数传递给另外一个函数:函数可以作为另外一个函数的返回值 ·2 无副作用 函数的副作用指的是函数在调用过程中,除了给出了返回值外,还修改了函数外 ...

  9. MAVEN构建Spring +Spring mvc + Mybatis 项目(Maven配置部分(workshop))

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/20 ...

  10. [No00006F]总结C#获取当前路径的各种方法

    using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; usin ...