Spring在SingleTon模式下的线程安全详解

目录
  • 1、有状态的bean与无状态的bean
  • 2、Spring中的单例
  • 3、Spring使用ThreadLocal解决线程安全问题案例
  • 4、ThreadLocal与线程同步机制的比较

1、有状态的bean与无状态的bean

  • 有状态bean:每个用户有自己特有的一个实例,在用户的生存期内,bean保存了用户的信息,即有状态;一旦用户灭亡(调用结束或实例结束),bean的生命期也告结束。即每个用户最初都会得到一个初始的bean。
  • 无状态bean:bean一旦实例化就被加进会话池中,各个用户都可以共用。即使用户已经消亡,bean的生命期也不一定结束,它可能依然存在于会话池中,供其他用户调用。由于没有特定的用户,那么也就不能保持某一用户的状态,所以叫无状态bean。但无状态会话bean 并非没有状态,如果它有自己的属性(变量)。

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

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

在Spring的Bean配置中,存在这样两种情况:

<bean id="testManager" class="com.sw.TestManagerImpl" scope="singleton" />  
<bean id="testManager" class="com.sw.TestManagerImpl" scope="prototype" /> 

当然,scope的值不止这两种,还包括了request、session 等。但用的最多的还是singleton单态与prototype多态。

singleton表示该bean全局只有一个实例,Spring中bean的scope默认也是singleton。

prototype表示该bean在每次被注入的时候,都要重新创建一个实例,这种情况适用于有状态的Bean。

下面是一个有状态的Bean示例

public class TestManagerImpl implements TestManager {  
    private User user;
    public void deleteUser(User e) throws Exception {  
        user = e;    //1
        prepareData(e);
    }
    public void prepareData(User e) throws Exception {  
        user = getUserByID(e.getId());     //2
        //使用user.getId();                //3
    }
}

如果该Bean配置为singleton,会出现什么样的状况呢?

如果有2个用户访问,都调用到了该Bean,假定为user1、user2。

当user1调用到程序中的步骤1的时候,该Bean的私有变量user被赋值为user1,当user1的程序走到步骤2的时候,该Bean的私有变量user被重新赋值为user1_create,理想的状况,当user1走到3步骤的时候,私有变量user应该为user1_create;但如果在user1调用到3步骤之前,user2开始运行到了步骤1了,由于单态的资源共享,则私有变量user被修改为user2;这种情况下,user1的步骤3用到的user.getId()实际用到是user2的对象。

即将有状态的bean配置成singleton会造成资源混乱问题(线程安全问题),而如果是prototype的话,就不会出现资源共享的问题,即不会出现线程安全的问题。

注:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,那么代码就是线程安全的。

通过上面分析,大家已经对有状态和无状态有了一定的理解。无状态的Bean适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式(解决多线程问题),每次对bean的请求都会创建一个新的bean实例。

2、Spring中的单例

Spring中的单例与设计模式里面的单例略有不同,设计模式的单例是在整个应用中只有一个实例,而Spring中的单例是在一个IOC容器中就只有一个实例。

大多数时候客户端都在访问我们应用中的业务对象,为了减少并发控制,在这个时候我们不应该在业务对象中设置那些容易造成出错的成员变量。在并发访问时候,这些成员变量将会是并发线程中的共享对象,那么这个时候就会出现意外情况。

成员变量的解决方式:

  • 方法的参数局部变量(在方法中new)
  • 使用Threadlocal
  • 设置bean的scope=prototype

3、Spring使用ThreadLocal解决线程安全问题案例

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

在使用Spring时,很多人可能对Spring中为什么DAO和Service对象采用单实例方式很迷惑,这些读者是这么认为的。

DAO对象必须包含一个数据库的连接Connection,而这个Connection不是线程安全的,所以每个DAO都要包含一个不同的Connection对象实例,这样一来DAO对象就不能是单实例的了。

上述观点对了一半。对的是“每个DAO都要包含一个不同的Connection对象实例”这句话,错的是“DAO对象就不能是单实例”。其实Spring在实现Service和DAO对象时,使用了ThreadLocal这个类,这个是一切的核心!

  • ThreadLocal
  • 每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
  • 将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。

一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。

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

public class TopicDao {
    //①一个非线程安全的变量
  private Connection conn;
    //②引用非线程安全变量
  public void addTopic() {
        Statement stat = conn.createStatement();
  }
}

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

public class TopicDao {  
    //①使用ThreadLocal保存Connection变量  
    private static ThreadLocal <Connection>connThreadLocal = newThreadLocal<Connection>();  
    public static Connection getConnection() {  
       // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,  
       // 并将其保存到线程本地变量中。  
       if (connThreadLocal.get() == null) {  
           Connection conn = getConnection();  
           connThreadLocal.set(conn);  
           return conn;  
       }
       // ③直接返回线程本地变量
       return connThreadLocal.get();  
    }
    public void addTopic() {  
       // ④从ThreadLocal中获取线程对应的Connection  
       try {
           Statement stat = getConnection().createStatement();  
       } catch (SQLException e) {  
           e.printStackTrace();  
       }
    }
}

不同的线程在使用TopicDao时,先判断connThreadLocal是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。

Spring中DAO和Service都是以单实例的bean形式存在,Spring通过ThreadLocal类将有状态的变量(例如数据库连接Connection)本地线程化,从而做到多线程状况下的安全。在一次请求响应的处理线程中, 该线程贯通展示、服务、数据持久化三层,通过ThreadLocal使得所有关联的对象引用到的都是同一个变量。

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

Web应用划分为展现层、服务层和持久层,controller中引入xxxService作为成员变量,xxxService中又引入xxxDao作为成员变量,这些对象都是单例而且会被多个线程并发访问,可我们访问的是它们里面的方法,这些类里面通常不会含有成员变量,dao实例是在MyBatis等ORM框架里面封装好的,已经被测试,不会出现线程同步问题了。所以出问题的地方就是我们自己系统里面的业务对象,所以我们一定要注意自定义的业务对象里面千万不能出现独立成员变量,否则会有线程安全问题。

通常我们在应用中的业务对象如下例子,controller中拥有成员变量list和paperService。

public class TestPaperController extends BaseController {
    private static final int list = 0;
    @Autowired
    @Qualifier("papersService")
    private TestPaperService papersService ;
    public Page queryPaper(int pageSize, int page,TestPaper paper) throws EicException {
      RowSelection localRowSelection = getRowSelection(pageSize, page);
      List<TestPaper> paperList = papersService.queryPaper(paper,localRowSelection);
      Page localPage = new Page(page, localRowSelection.getTotalRows(), paperList);
      return localPage;
    }
}

service里面又引入了成员变量ibatisEntityDao

@SuppressWarnings("unchecked")
@Service("papersService")
@Transactional(rollbackFor = {Exception.class})
public class TestPaperServiceImpl implements TestPaperService {
    @Autowired
    @Qualifier("ibatisEntityDao")
    private IbatisEntityDao ibatisEntityDao;
    private static final String NAMESPACE_TESTPAPER = "com.its.exam.testpaper.model.TestPaper";
    private static final String BO_NAME[] = { "试卷仓库" };
    private static final String BO_NAME2[] = { "试卷配置试题" };
    private static final String BO_NAME1[] = { "试卷试题类型" };
    private static final String NAMESPACE_TESTQUESTION="com.its.exam.testpaper.model.TestQuestion";
    public List<TestPaper> queryPaper(TestPaper paper,RowSelection paramRowSelection) throws EicException {
      try {
       return (List<TestPaper>) ibatisEntityDao.queryForListWithPage(NAMESPACE_TESTPAPER, "queryPaper", paper,paramRowSelection);
      } catch (Exception e) {
       e.printStackTrace();
       throw new EicException(e, "eic", "0001", BO_NAME);
      }
    }
}

由上面例子可以看出,虽然我们这个应用里面含有成员变量,但是并不会出现线程同步方面的问题,controller里面的成员变量papersService被注入后,是为了访问该service类的方法,papersService里面注入的成员变量ibatisEntityDao是ORM框架封装好的,其线程同步问题已解决。

4、ThreadLocal与线程同步机制的比较

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

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

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

由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用。

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

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

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 使用springboot单例模式与线程安全问题踩的坑

    springboot单例模式与线程安全问题踩的坑 最近有客户反映,使用公司产品时,偶尔会存在崩溃情况,自己测试无问题,然后去查日志,是报空指针. 于是顺藤摸瓜 往上找,好嘛,之前的开发使用了成员变量,感觉问题就是在这里了,因为众所周知,springboot 采用的是单例模式,所以,使用成员变量时一定要谨慎. 下面上一张该类的截图: 大家可能看到了,该类上面加上了@Scope("prototype") 注解,该注解的作用是将该类变成多例模式.讲道理因为变为了多例,应该不会有线程问题了.

  • Spring如何解决单例bean线程不安全的问题

    首先我们应该知道线程安全问题一般发生在成员变量上,这是为什么啦? 因为成员变量是存放在堆内存中,而堆内存又是线程共享的,这就造成了线程安全问题 因为Spring中的Bean默认是单例的,所以在定义成员变量时也有可能会发生线程安全问题.下面我们就来研究下如何解决Spring中单例Bean的线程安全问题 @RestController //@Scope("prototype") public class BeanController { private int content=0; //基

  • 浅谈Spring中单例Bean是线程安全的吗

    Spring容器中的Bean是否线程安全,容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体还是要结合具体scope的Bean去研究. Spring 的 bean 作用域(scope)类型 1.singleton:单例,默认作用域. 2.prototype:原型,每次创建一个新对象. 3.request:请求,每次Http请求创建一个新对象,适用于WebApplicationContext环境下. 4.session:会话,同一个会

  • Spring在SingleTon模式下的线程安全详解

    目录 1.有状态的bean与无状态的bean 2.Spring中的单例 3.Spring使用ThreadLocal解决线程安全问题案例 4.ThreadLocal与线程同步机制的比较 1.有状态的bean与无状态的bean 有状态bean:每个用户有自己特有的一个实例,在用户的生存期内,bean保存了用户的信息,即有状态:一旦用户灭亡(调用结束或实例结束),bean的生命期也告结束.即每个用户最初都会得到一个初始的bean. 无状态bean:bean一旦实例化就被加进会话池中,各个用户都可以共用

  • 虚拟机Linux桥接模式下设置静态IP详解

    本文研究的主要是虚拟机Linux桥接模式下设置静态IP的相关内容,具体介绍如下. 之前一直使用NAT模式,测试时android端远程访问虚拟机的mysql时发现无法连接,但是访问同学拷过来的虚拟机Linux的mysql却成功了,想了下原因是他设置的桥接模式.关于两种模式的区别,网上可以搜到一大堆文章,通俗点讲,NAT模式下,虚拟机从属于主机,也就是访问外部网络必须通过主机来访问,因此虚拟机的IP只有主机才能识别.而桥接模式下,虚拟机和主机是平行关系,共享一张网卡(使用网卡的多个接口),可以直接访

  • 对IPython交互模式下的退出方法详解

    如果进入了shell交互窗口,python的退出方式只能够用函数不能够用命令,这有时候让人感觉到很不习惯.因为函数会比命令多一个括号的输入,这让人有点反感.而Linux的终端.DOS CMD窗口.MATLAB等各种使用基本上都是一个exit退出命令即可.相比之下,Python的这种方式确实是让我觉得有点不舒服. 尽管有着使用上的不便利,但是使用时间长了还是能够养成使用Python的时候加个括号实现相应的退出.换了IPython之后,感觉又有一种回到Linux终端的感觉,刚刚试了一下,居然也支持命

  • Spring 多线程下注入bean问题详解

    本文介绍了Spring 多线程下注入bean问题详解,分享给大家,具体如下: 问题 Spring中多线程注入userThreadService注不进去,显示userThreadService为null异常 代码如下: public class UserThreadTask implements Runnable { @Autowired private UserThreadService userThreadService; @Override public void run() { AdeUs

  • spring对JDBC和orm的支持实例详解

    简介 Spring提供的DAO(数据访问对象)支持主要的目的是便于以标准的方式使用不同的数据访问技术,如JDBC,Hibernate或者JDO等.它不仅可以让你方便地在这些持久化技术间切换, 而且让你在编码的时候不用考虑处理各种技术中特定的异常. 一致的异常层次 Spring提供了一种方便的方法,把特定于某种技术的异常,如SQLException, 转化为自己的异常,这种异常属于以 DataAccessException 为根的异常层次.这些异常封装了原始异常对象,这样就不会有丢失任何错误信息的

  • 从C++单例模式到线程安全详解

    先看一个最简单的教科书式单例模式: class CSingleton { public: static CSingleton* getInstance() { if (NULL == ps) {//tag1 ps = new CSingleton; } return ps; } private: CSingleton(){} CSingleton & operator=(const CSingleton &s); static CSingleton* ps; }; CSingleton*

  • Spring 整合 Hibernate 时启用二级缓存实例详解

    Spring 整合 Hibernate 时启用二级缓存实例详解 写在前面: 1. 本例使用 Hibernate3 + Spring3: 2. 本例的查询使用了 HibernateTemplate: 1. 导入 ehcache-x.x.x.jar 包: 2. 在 applicationContext.xml 文件中找到 sessionFactory 相应的配置信息并在设置 hibernateProperties 中添加如下代码: <!-- 配置使用查询缓存 --> <prop key=&q

  • MVC+DAO设计模式下的设计流程详解

    DAO设计 : DAO层主要是做数据持久层的工作,负责与数据库进行联络的一些任务都封装在此,DAO层的设计首先是设计DAO的接口,然后在Spring的配置文件中定义此接口的实现类,然后就可在模块中调用此接口来进行数据业务的处理,而不用关心此接口的具体实现类是哪个类,显得结构非常清晰,DAO层的数据源配置,以及有关数据库连接的参数都在Spring的配置文件中进行配置. 在该层主要完成对象-关系映射的建立,通过这个映射,再通过访问业务对象即可实现对数据库的访问,使得开发中不必再用SQL语句编写复杂的

  • 基于tomcat的连接数与线程池详解

    前言 在使用tomcat时,经常会遇到连接数.线程数之类的配置问题,要真正理解这些概念,必须先了解Tomcat的连接器(Connector). 在前面的文章 详解Tomcat配置文件server.xml 中写到过:Connector的主要功能,是接收连接请求,创建Request和Response对象用于和请求端交换数据:然后分配线程让Engine(也就是Servlet容器)来处理这个请求,并把产生的Request和Response对象传给Engine.当Engine处理完请求后,也会通过Conn

  • java多线程中线程封闭详解

    线程封闭的概念 访问共享变量时,通常要使用同步,所以避免使用同步的方法就是减少共享数据的使用,这种技术就是线程封闭. 实现线程封闭的方法 1:ad-hoc线程封闭 这是完全靠实现者控制的线程封闭,他的线程封闭完全靠实现者实现.也是最糟糕的一种线程封闭.所以我们直接把他忽略掉吧. 2:栈封闭 栈封闭是我们编程当中遇到的最多的线程封闭.什么是栈封闭呢?简单的说就是局部变量.多个线程访问一个方法,此方法中的局部变量都会被拷贝一分儿到线程栈中.所以局部变量是不被多个线程所共享的,也就不会出现并发问题.所

随机推荐