Spring单实例、多线程安全、事务解析

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 原文:http://blog.csdn.net/c289054531/article/details/9196053引言:    在使用Spring时,很多人可能对Spring中为什么DAO和Service对象采用单实例方式很迷惑,这些读者是这么认为的:    DAO对象必须包含一个数据库的连接Connection,而这个Connection不是线程安全的,所以每个DAO都要包含一个不同的Connection对象实例,这样一来DAO对象就不能是单实例的了。

原文:http://blog.csdn.net/c289054531/article/details/9196053

引言:

    在使用Spring时,很多人可能对Spring中为什么DAO和Service对象采用单实例方式很迷惑,这些读者是这么认为的:
    DAO对象必须包含一个数据库的连接Connection,而这个Connection不是线程安全的,所以每个DAO都要包含一个不同的Connection对象实例,这样一来DAO对象就不能是单实例的了。
    上述观点对了一半。对的是“每个DAO都要包含一个不同的Connection对象实例”这句话,错的是“DAO对象就不能是单实例”。
    其实Spring在实现Service和DAO对象时,使用了ThreadLocal这个类,这个是一切的核心! 如果你不知道什么事ThreadLocal,请看 深入研究java.lang.ThreadLocal类》 :。请放心,这个类很简单的。
1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。 
2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
 
    要弄明白这一切,又得明白事务管理在Spring中是怎么工作的,所以本文就对Spring中多线程、事务的问题进行解析。

Spring使用ThreadLocal解决线程安全问题:

    Spring中DAO和Service都是以单实例的bean形式存在,Spring通过ThreadLocal类将有状态的变量(例如数据库连接Connection)本地线程化,从而做到多线程状况下的安全。在一次请求响应的处理线程中, 该线程贯通展示、服务、数据持久化三层,通过ThreadLocal使得所有关联的对象引用到的都是同一个变量。 
    参考下面代码,这个是《Spring3.x企业应用开发实战中的例子》,本文后面也会多次用到该书中例子(有修改)。
[java]  view plain copy
  1. <span style="font-family:SimSun;font-size:14px;">public class SqlConnection {  
  2.     //①使用ThreadLocal保存Connection变量  
  3.     privatestatic ThreadLocal <Connection>connThreadLocal = newThreadLocal<Connection>();  
  4.     publicstatic Connection getConnection() {  
  5.        // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,  
  6.        // 并将其保存到线程本地变量中。  
  7.        if (connThreadLocal.get() == null) {  
  8.            Connection conn = getConnection();  
  9.            connThreadLocal.set(conn);  
  10.            return conn;  
  11.        } else {  
  12.            return connThreadLocal.get();  
  13.            // ③直接返回线程本地变量  
  14.        }  
  15.     }  
  16.     public voidaddTopic() {  
  17.        // ④从ThreadLocal中获取线程对应的Connection  
  18.        try {  
  19.            Statement stat = getConnection().createStatement();  
  20.        } catch (SQLException e) {  
  21.            e.printStackTrace();  
  22.        }  
  23.     }  
  24. }</span>  
    这个是例子展示了不同线程使用TopicDao时如何使得每个线程都获得不同的Connection实例副本,同时保持TopicDao本身是单实例。

事务管理器:

    事务管理器用于管理各个事务方法,它产生一个事务管理上下文。下文以SpringJDBC的事务管理器DataSourceTransactionManager类为例子。
    我们知道数据库连接Connection在不同线程中是不能共享的,事务管理器为不同的事务线程利用ThreadLocal类提供独立的Connection副本。事实上,它将Service和Dao中所有线程不安全的变量都提取出来单独放在一个地方,并用ThreadLocal替换。而多线程可以共享的部分则以单实例方式存在。

事务传播行为:

    当我们调用Service的某个事务方法时,如果该方法内部又调用其它Service的事务方法,则会出现事务的嵌套。Spring定义了一套事务传播行为,请参考。这里我们假定都用的REQUIRED这个类型:如果当前没有事务,就新建一个事务,如果已经存在一个事务,则加入到的当前事务。参考下面例子(代码不完整):
[java]  view plain copy
  1. <span style="font-family:SimSun;font-size:14px;">@Service"userService")  
  2. public class UserService extends BaseService {  
  3.     @Autowired  
  4.     private JdbcTemplate jdbcTemplate;  
  5.   
  6.     @Autowired  
  7.     private ScoreService scoreService;  
  8.      
  9.     public void logon(String userName) {  
  10.         updateLastLogonTime(userName);         
  11.         scoreService.addScore(userName, 20);  
  12.     }  
  13.   
  14.     public void updateLastLogonTime(String userName) {  
  15.         String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";  
  16.         jdbcTemplate.update(sql, System. currentTimeMillis(), userName);  
  17.     }  
  18.   
  19.     public static void main(String[] args) {  
  20.         ApplicationContext ctx = new ClassPathXmlApplicationContext("com/baobaotao/nestcall/applicatonContext.xml" );  
  21.         UserService service = (UserService) ctx.getBean("userService" );  
  22.         service.logon( "tom");  
  23.   
  24.     }  
  25. }  
  26.   
  27. @Service"scoreUserService" )  
  28. public class ScoreService extends BaseService{  
  29.     @Autowired  
  30.     private JdbcTemplate jdbcTemplate;  
  31.   
  32.     public void addScore(String userName, int toAdd) {  
  33.         String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";  
  34.         jdbcTemplate.update(sql, toAdd, userName);  
  35.     }  
  36. }</span>  
    同时,在配置文件中指定UserService、ScoreService中的所有方法都开启事务。
    上述例子中UserService.logon()执行开始时Spring创建一个新事务,UserService.updateLastLogonTime()和ScoreService.addScore()会加入这个事务中,好像所有的代码都“直接合并”了!

多线程中事务传播的困惑:

    还是上面那个例子,加入现在我在UserService.logon()方法中手动新开一个线程,然后在新开的线程中执行ScoreService.add()方法,此时事务传播行为会怎么样?飞线程安全的变量,比如Connection会怎样?改动之后的UserService 代码大体是:
[java]  view plain copy
  1. <span style="font-family:SimSun;font-size:14px;">@Service"userService")  
  2. public class UserService extends BaseService {  
  3.     @Autowired  
  4.     private JdbcTemplate jdbcTemplate;  
  5.   
  6.     @Autowired  
  7.     private ScoreService scoreService;  
  8.   
  9.     public void logon(String userName) {  
  10.         updateLastLogonTime(userName);  
  11.         Thread myThread = new MyThread(this.scoreService , userName, 20);//使用一个新线程运行  
  12.         myThread .start();  
  13.     }  
  14.   
  15.     public void updateLastLogonTime(String userName) {  
  16.         String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";  
  17.         jdbcTemplate.update(sql, System. currentTimeMillis(), userName);  
  18.     }  
  19.   
  20.     private class MyThread extends Thread {  
  21.         private ScoreService scoreService;  
  22.         private String userName;  
  23.         private int toAdd;  
  24.         private MyThread(ScoreService scoreService, String userName, int toAdd) {  
  25.             this. scoreService = scoreService;  
  26.             this. userName = userName;  
  27.             this. toAdd = toAdd;  
  28.         }  
  29.   
  30.         public void run() {  
  31.             scoreService.addScore( userName, toAdd);  
  32.         }  
  33.     }  
  34.   
  35.     public static void main(String[] args) {  
  36.         ApplicationContext ctx = new ClassPathXmlApplicationContext("com/baobaotao/multithread/applicatonContext.xml" );  
  37.         UserService service = (UserService) ctx.getBean("userService" );  
  38.         service.logon( "tom");  
  39.        }  
  40. }</span>  
    这个例子中,MyThread会新开一个事务,于是UserService.logon()和UserService.updateLastLogonTime()会在一个事务中,而ScoreService.addScore()在另一个事务中,需要注意的是这两个事务都被事务管理器放在事务上下文中。
    结论是:在事务属性为REQUIRED时,在相同线程中进行相互嵌套调用的事务方法工作于相同的事务中。如果互相嵌套调用的事务方法工作在不同线程中,则不同线程下的事务方法工作在独立的事务中。

底层数据库连接Connection访问问题

    程序只要使用SpringDAO模板,例如JdbcTemplate进行数据访问,一定没有数据库连接泄露问题!如果程序中显式的获取了数据连接Connection,则需要手工关闭它,否则就会泄露!
    当Spring事务方法运行时,事务会放在事务上下文中,这个事务上下文在本事务执行线程中对同一个数据源绑定了唯一一个数据连接,所有被该事务的上下文传播的放发都共享这个数据连接。这一切都在Spring控制下,不会产生泄露。Spring提供了数据资源获取工具类DataSourceUtils来获取这个数据连接.
[java]  view plain copy
  1. <span style="font-family:SimSun;font-size:14px;">@Service"jdbcUserService" )  
  2. public class JdbcUserService {  
  3.     @Autowired  
  4.     private JdbcTemplate jdbcTemplate;  
  5.      
  6.     @Transactional  
  7.     public void logon(String userName) {  
  8.         try {  
  9.             Connection conn = jdbcTemplate.getDataSource().getConnection();             
  10.             String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";  
  11.             jdbcTemplate.update(sql, System. currentTimeMillis(), userName);  
  12.         } catch (Exception e) {  
  13.             e.printStackTrace();  
  14.         }  
  15.   
  16.     }  
  17.   
  18.     public static void asynchrLogon(JdbcUserService userService, String userName) {  
  19.         UserServiceRunner runner = new UserServiceRunner(userService, userName);  
  20.         runner.start();  
  21.     }  
  22.   
  23.     public static void reportConn(BasicDataSource basicDataSource) {  
  24.         System. out.println( "连接数[active:idle]-[" +  
  25.                        basicDataSource.getNumActive()+":" +basicDataSource.getNumIdle()+ "]");  
  26.     }  
  27.   
  28.     private static class UserServiceRunner extends Thread {  
  29.         private JdbcUserService userService;  
  30.         private String userName;  
  31.   
  32.         public UserServiceRunner(JdbcUserService userService, String userName) {  
  33.             this. userService = userService;  
  34.             this. userName = userName;  
  35.         }  
  36.   
  37.         public void run() {  
  38.             userService.logon( userName);  
  39.         }  
  40.     }  
  41.   
  42.     public static void main(String[] args) {  
  43.         ApplicationContext ctx = new ClassPathXmlApplicationContext("com/baobaotao/connleak/applicatonContext.xml" );  
  44.         JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService" );  
  45.         JdbcUserService. asynchrLogon(userService, "tom");  
  46.     }  
  47. }</span>  
    在这个例子中,main线程拿到一个UserService实例,获取一个Connection的副本,它会被Spring管理,不会泄露。UserServiceRunner 线程手动从数据源拿了一个Connection但没有关闭因此会泄露。
    如果希望使UserServiceRunner能拿到UserService中那个Connection们就要使用DataSourceUtils类,DataSourceUtils.getConnection()方法会首先查看当前是否存在事务管理上下文,如果存在就尝试从事务管理上下文拿连接,如果获取失败,直接从数据源中拿。在获取连接后,如果存在事务管理上下文则把连接绑定上去。
    实际上,上面的代码只用改动一行,把login()方法中获取连接那行改成就可以做到:
Connection conn = DataSourceUtils.getConnection( jdbcTemplate .getDataSource());    
   需要注意的是:如果DataSourceUtils在没有事务上下文的方法中使用getConnection()获取连接,依然要手动管理这个连接!
    此外,开启了事务的方法要在整个事务方法结束后才释放事务上下文绑定的Connection连接,而没有开启事务的方法在调用完Spring的Dao模板方法后立刻释放。

多线程一定要与事务挂钩么?

    不是!即便没有开启事务,利用ThreadLocal机制也能保证线程安全,Dao照样可以操作数据。但是事务和多线程确实纠缠不清,上文已经分析了在多线程下事务传播行为、事务对Connection获取的影响。

结论:

  • Spring中DAO和Service都是以单实例的bean形式存在,Spring通过ThreadLocal类将有状态的变量(例如数据库连接Connection)本地线程化,从而做到多线程状况下的安全。在一次请求响应的处理线程中, 该线程贯通展示、服务、数据持久化三层,通过ThreadLocal使得所有关联的对象引用到的都是同一个变量。 
  • 在事务属性为REQUIRED时,在相同线程中进行相互嵌套调用的事务方法工作于相同的事务中。如果互相嵌套调用的事务方法工作在不同线程中,则不同线程下的事务方法工作在独立的事务中。
  • 程序只要使用SpringDAO模板,例如JdbcTemplate进行数据访问,一定没有数据库连接泄露问题!如果程序中显式的获取了数据连接Connection,则需要手工关闭它,否则就会泄露!
  • 当Spring事务方法运行时,就产生一个事务上下文,它在本事务执行线程中对同一个数据源绑定了一个唯一的数据连接,所有被该事务上下文传播的方法都共享这个连接。要获取这个连接,如要使用Spirng的资源获取工具类DataSourceUtils。
  • 事务管理上下文就好比一个盒子,所有的事务都放在里面。如果在某个事务方法中开启一个新线程,新线程中执行另一个事务方法,则由上面第二条可知这两个方法运行于两个独立的事务中,但是:如果使用DataSourcesUtils,则新线程中的方法可以从事务上下文中获取原线程中的数据连接!
目录
相关文章
|
10天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
39 2
|
29天前
|
搜索推荐 Java Spring
Spring Filter深度解析
【10月更文挑战第21天】Spring Filter 是 Spring 框架中非常重要的一部分,它为请求处理提供了灵活的控制和扩展机制。通过合理配置和使用 Filter,可以实现各种个性化的功能,提升应用的安全性、可靠性和性能。还可以结合具体的代码示例和实际应用案例,进一步深入探讨 Spring Filter 的具体应用和优化技巧,使对它的理解更加全面和深入。
|
19天前
|
Java 开发者 Spring
Spring高手之路24——事务类型及传播行为实战指南
本篇文章深入探讨了Spring中的事务管理,特别是事务传播行为(如REQUIRES_NEW和NESTED)的应用与区别。通过详实的示例和优化的时序图,全面解析如何在实际项目中使用这些高级事务控制技巧,以提升开发者的Spring事务管理能力。
33 1
Spring高手之路24——事务类型及传播行为实战指南
|
6天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
12天前
|
XML Java 数据库连接
Spring中的事务是如何实现的
Spring中的事务管理机制通过一系列强大的功能和灵活的配置选项,为开发者提供了高效且可靠的事务处理手段。无论是通过注解还是AOP配置,Spring都能轻松实现复杂的事务管理需求。掌握这些工具和最佳实践,能
18 3
|
22天前
|
安全 程序员 API
|
19天前
|
存储 设计模式 分布式计算
Java中的多线程编程:并发与并行的深度解析####
在当今软件开发领域,多线程编程已成为提升应用性能、响应速度及资源利用率的关键手段之一。本文将深入探讨Java平台上的多线程机制,从基础概念到高级应用,全面解析并发与并行编程的核心理念、实现方式及其在实际项目中的应用策略。不同于常规摘要的简洁概述,本文旨在通过详尽的技术剖析,为读者构建一个系统化的多线程知识框架,辅以生动实例,让抽象概念具体化,复杂问题简单化。 ####
|
30天前
|
安全 Java
Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧
【10月更文挑战第20天】Java多线程通信新解:本文通过生产者-消费者模型案例,深入解析wait()、notify()、notifyAll()方法的实用技巧,包括避免在循环外调用wait()、优先使用notifyAll()、确保线程安全及处理InterruptedException等,帮助读者更好地掌握这些方法的应用。
19 1
|
1月前
|
Java Spring
Spring底层架构源码解析(三)
Spring底层架构源码解析(三)
113 5
|
1月前
|
XML Java 数据格式
Spring底层架构源码解析(二)
Spring底层架构源码解析(二)
下一篇
无影云桌面