spring单例,为什么service和dao确能保证线程安全_spring dao为什么线程安全_weixin_39276098的博客-程序员秘密

 在使用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,则新线程中的方法可以从事务上下文中获取原线程中的数据连接!
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weixin_39276098/article/details/78706287

智能推荐

java记事本的实现_liuxu_yunyan的博客-程序员秘密

[url]http://6501542.blog.51cto.com/6491542/1287796[/url]

Pycharm - OpenCV 02人脸识别_马可露露的博客-程序员秘密

2 多张人脸检测2.3 harr分类器参考学习:harr 01harr 022.2 opencv detectMultiScale()默认参数如图所示。image 输入图像objects 表示检测到的人脸目标序列scaleFactor 表示每次图像尺寸减小的比例minNeighbors 表示每一个目标至少要被检测到3次才算是真的目标(因为周围的像素和不同的窗口大小都可以检测到人脸),minSize 为目标的最小尺寸minSize 为目标的最大尺寸2

iOS/swift之path、url、data 、String与Data之间的转换、data和字典之间转换、对象序列化反序列化_swift url转data_liyubao160的博客-程序员秘密

1.**************从path中获取数据************let path=Bundle.main.path(forResource: &quot;shengshiquxianarea.plist&quot;, ofType: nil) let proviceDataArr:NSArray = NSArray(contentsOfFile:path!)!2.String与d...

sql语句中插入数据的三种常用方法及小贴士_guilin_gavin的博客-程序员秘密

    我们在插入数据到数据库中的时候,常用的语句如下:    INSERT INTO table1(id, name, address) VALUES(1, ygl, ‘beijing’)    适用于T-sql和PL/SQL    SELECT id, name, address INTO table2 FROM table1    自动创建table2,T-sql

AirServer2023免费无线Mac和PC电脑屏幕镜像投屏工具_CoCo玛奇朵的博客-程序员秘密

AirServer2023是适用于 Mac 和 PC 的先进的屏幕镜像接收器。 它允许您接收 AirPlay 和 Google Cast 流,类似于 Apple TV 或 Chromecast 设备。AirServer 可以将一个简单的大屏幕或投影仪变成一个通用的屏幕镜像接收器 ,是一款十分强大的投屏软件。

Storm中slot数的扩展_storm slot 数_鲁鲁517的博客-程序员秘密

在Storm程序中,用下面语句指定同时处理的worker数:conf.setNumWorkers(5); 这个worker数受限于slot数量,一个worker消耗一个slot,当slot全部分配完,就不能再加载新的topology。slot数量是在 supervisor.slots.ports 中设置,每个端口提供一个slot,这样supervisor数量乘以ports数量,就是stor...

随便推点

如何做决定_qq_36049873的博客-程序员秘密

自从,2017年写了一篇博客后,到今天过去四年了,四年里经历了太多,从进入编程世界,到去到广州工作,工作一年,离开广州到了成都,工作一年半以后,回到昆明。找到自己的女朋友,对近四年生活的一些感悟,对编程世界最近的一些思考。有了一些新的感悟。 今天先感悟一下,从广州离开到成都这段经历吧。 程序员这个行业,大家都觉得跳槽很正常,其实我想说的是,如果跳槽是为了接触更高层次的东西,或者融入更好的氛围是无可厚非的,但是如果单纯是因为所谓的朋友,放弃当下充满斗志的心境,那么千万不要做所谓的跳槽,...

把 Excel 表转换成 json 对象,竟然如此简单_萌眼牛牛 Lah的博客-程序员秘密

大家好,我是章鱼猫。今天给大家推荐的这个项目是「excel2json」,把 Excel 表转换成 json 对象,并保存到一个文本文件中。在游戏项目中一般都需要由策划制作大量的游戏内容,其中很大一部分是使用 Excel 表来制作的。于是程序就需要把 Excel 文件转换成程序方便读取的格式。之前项目使用的 Excel 表导入工具都是通过 Office Excel 组件来实现数据访问的,效率十分令人...

图像处理基本概念——卷积,滤波,平滑_图像卷积数学表达形式_John9ML的博客-程序员秘密

 1.图像卷积(模板)(1).使用模板处理图像相关概念:          模板:矩阵方块,其数学含义是一种卷积运算。           卷积运算:可看作是加权求和的过程,使用到的图像区域中的每个像素分别于卷积核(权矩阵)的每个元素对应相乘,所有乘积之和作为区域中心像素的新值。     卷积核:卷积时使用到的权用一个矩阵表示,该矩阵是一个权矩阵。     卷积示例:    ...

如何封装一个服务器的网络库_narlon的博客-程序员秘密

为什么需要封装隐藏TCP数据黏连 隐藏TCP参数(超时时间,连接等待时间) 方便支持高级功能(心跳,重连,统计,加密,压缩,错误日志) 让对象关系更加明确(而不是一个Socket打遍天下无敌手)绝对不应该封装的内容序列化,这不应该归网络库来管,很容易把两者耦合起来,设计时必须要考虑 服务器架构网络的代码。比如,服务器组之间的数据通信,用来建立起服务器网络的握手或是服务器组内部的消...

判断是否为完全二叉树_空树是不是完全二叉树_iboylee的博客-程序员秘密

完全二叉树空树是完全二叉树对于深度为K的,有n个结点的二叉树,当且仅当其每一个结点都与深度为K的满二叉树中编号从1至n的结点一一对应时称之为完全二叉树。算法思想判断树是否是空树,若为空树则为完全二叉树若树为非空完全二叉树,则借助队列层序遍历二叉树(空分支也入队),若出队碰到空分支,则说明之前的结点都不为空,那么队列中的结点都应该是空分支。若不满足上述,则不是完全二叉树伪代码b...

Python 接口测试或爬虫中Http请求的Header头快速构造,解决“Fiddler中请求可以,Python发送请求失败等问题排查和问题原因说明”_python2.7 传递headers 失败_BenjaminQA的博客-程序员秘密

前言 “1 + 1 = 2” 耳濡目染,从而引出下方:url + request headers + request method + request body(data) = response headers + response data 遇见问题,发现问题,定位问题并解决问题,静下心看看公式慢慢调试,理解其“构造原理”。常见问题:1、Python 3.6.4 调用requests库中headers定义字段值发生变化 “梗”待解...

推荐文章

热门文章

相关标签