Spring中Bean的单例、多例

问题一: Spring哪里用到了单例?

1 springboot 采用的是单例模式
2 @Component注解默认实例化的对象是单例,如果想声明成多例对象可以使用@Scope(“prototype”)

@Component
@Scope(“prototype”)

3 @Repository默认单例
4 @Service默认单例
5 @Controller默认单例

问题二:Spring单例Bean与单例模式的区别?

java单例模式
java单例模式确保一个类只有一个实例,自行提供这个实例并向整个系统提供这个实例。
特点:
1,一个类只能有一个实例;
2,自己创建这个实例;
3,整个系统都要使用这个实例。

Singleton模式主要作用是保证在Java应用程序中,一个类Class只有一个实例存在。在很多操作中,比如建立目录 数据库连接都需要这样的单线程操作。一些资源管理器常常设计成单例模式。
外部资源:譬如每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干个通信端口,系统应当集中管理这些通信端口,以避免一个通信端口被两个请求同时调用。
内部资源,譬如,大多数的软件都有一个(甚至多个)属性文件存放系统配置。这样的系统应当由一个对象来管理这些属性文件。

单例模式,能避免实例重复创建;
单例模式,应用于避免存在多个实例引起程序逻辑错误的场合;
单例模式,较节约内存。

区别
Spring单例Bean与单例模式的区别在于它们关联的环境不一样,单例模式是指在一个JVM进程中仅有一个实例,而Spring单例是指一个Spring Bean容器(ApplicationContext)中仅有一个实例。

首先看单例模式,在一个JVM进程中(理论上,一个运行的JAVA程序就必定有自己一个独立的JVM)仅有一个实例,于是无论在程序中的何处获取实例,始终都返回同一个对象,以Java内置的Runtime为例(现在枚举是单例模式的最佳实践),无论何时何处获取,下面的判断始终为真:

//  基于懒汉模式实现
//  在一个JVM实例中始终只有一个实例
Runtime.getRuntime() == Runtime.getRuntime()

与此相比,Spring的单例Bean是与其容器(ApplicationContext)密切相关的,所以在一个JVM进程中,如果有多个Spring容器,即使是单例bean,也一定会创建多个实例,代码示例如下:

//  第一个Spring Bean容器
ApplicationContext context_1 = new FileSystemXmlApplicationContext("classpath:/ApplicationContext.xml");
Person yiifaa_1 = context_1.getBean("yiifaa", Person.class);
//  第二个Spring Bean容器
ApplicationContext context_2 = new FileSystemXmlApplicationContext("classpath:/ApplicationContext.xml");
Person yiifaa_2 = context_2.getBean("yiifaa", Person.class);
//  这里绝对不会相等,因为创建了多个实例
System.out.println(yiifaa_1 == yiifaa_2);

以下是Spring的配置文件:

<!-- 即使声明了为单例,只要有多个容器,也一定会创建多个实例 -->
<bean id="yiifaa" class="com.stixu.anno.Person" scope="singleton">
    <constructor-arg name="username">
        <value>yiifaa</value>
    </constructor-arg>
</bean>

总结
Spring的单例bean与Spring bean管理容器密切相关,每个容器都会创建自己独有的实例,所以与GOF设计模式中的单例模式相差极大,但在实际应用中,如果将对象的生命周期完全交给Spring管理(不在其他地方通过new、反射等方式创建),其实也能达到单例模式的效果。

问题三: 如何注入Bean

方法:在WebMvcConfigurerAdapter的子类中添加@Bean,返回实例对象即可

package cn.com.showclear.plan.impl.plan;
 
/**
 * 测试
 *
 * @author YF-XIACHAOYANG
 * @date 2017/12/13 18:04
 */
public class TestBean {
 
    private String name;
 
    /*可以自定义构造器*/
    public TestBean(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public TestBean setName(String name) {
        this.name = name;
        return this;
    }
 
    public void hello() {
        System.out.println(this.name);
    }
}
package cn.com.showclear.config;
 
@SpringBootApplication
@ComponentScan(basePackages = "cn.com.showclear")
@EnableScheduling
public class WebMvcConfig extends WebMvcConfigurerAdapter {
 
    ...
 
    @Bean //这里直接把TestBean类属性方法注入进来,对外提供调用
    public TestBean getTestBean() {
        return new TestBean("hello bean1!");
    }
}
@RestController
@RequestMapping("/data/plan/config/")
public class PlanConfigController {
 
    @Autowired  //
    private TestBean testBean;
    
     /**
     * 加载预案应急事件标签组[含有组内标签信息]
     *
     * @return
     */
    @RequestMapping(value = "loadTagGroupList", method = RequestMethod.POST)
    public RespMapJson loadTagGroupList(String groupName) {
        testBean.hello();
        return planConfigService.load(planHandleDeliver.getTagConfigHandle().init(this, groupName));
    }

问题四:SpringBoot为何是单例

问题五:Spring单例,为什么service和dao确能保证线程安全

在使用Spring时,很多人可能对Spring中为什么DAO和Service对象采用单实例方式很迷惑,这些读者是这么认为的:
DAO对象必须包含一个数据库的连接Connection,而这个Connection不是线程安全的,所以每个DAO都要包含一个不同的Connection对象实例,这样一来DAO对象就不能是单实例的了。
上述观点对了一半。对的是“每个DAO都要包含一个不同的Connection对象实例”这句话,错的是“DAO对象就不能是单实例”。
其实Spring在实现Service和DAO对象时,使用了ThreadLocal这个类,这个是一切的核心! 如果你不知道什么事ThreadLocal,请看《深入研究java.lang.ThreadLocal类》:。请放心,这个类很简单的。
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 plaincopy

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

这个是例子展示了不同线程使用TopicDao时如何使得每个线程都获得不同的Connection实例副本,同时保持TopicDao本身是单实例。
事务管理器:
事务管理器用于管理各个事务方法,它产生一个事务管理上下文。下文以SpringJDBC的事务管理器DataSourceTransactionManager类为例子。
我们知道数据库连接Connection在不同线程中是不能共享的,事务管理器为不同的事务线程利用ThreadLocal类提供独立的Connection副本。事实上,它将Service和Dao中所有线程不安全的变量都提取出来单独放在一个地方,并用ThreadLocal替换。而多线程可以共享的部分则以单实例方式存在。
事务传播行为:
当我们调用Service的某个事务方法时,如果该方法内部又调用其它Service的事务方法,则会出现事务的嵌套。Spring定义了一套事务传播行为,请参考。这里我们假定都用的REQUIRED这个类型:如果当前没有事务,就新建一个事务,如果已经存在一个事务,则加入到的当前事务。参考下面例子(代码不完整):
[java] view plaincopy

@Service( "userService")  
public class UserService extends BaseService {  
    @Autowired  
    private JdbcTemplate jdbcTemplate;  
  
@Autowired  
private ScoreService scoreService;  
 
public void logon(String userName) {  
    updateLastLogonTime(userName);         
    scoreService.addScore(userName, 20);  
}  
  
public void updateLastLogonTime(String userName) {  
    String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";  
    jdbcTemplate.update(sql, System. currentTimeMillis(), userName);  
}  
  
public static void main(String[] args) {  
    ApplicationContext ctx = new ClassPathXmlApplicationContext("com/baobaotao/nestcall/applicatonContext.xml" );  
    UserService service = (UserService) ctx.getBean("userService" );  
    service.logon( "tom");  
  
}  
}  

  
@Service( "scoreUserService" )  
public class ScoreService extends BaseService{  
    @Autowired  
    private JdbcTemplate jdbcTemplate;  
  
public void addScore(String userName, int toAdd) {  
    String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";  
    jdbcTemplate.update(sql, toAdd, userName);  
}  
}
 

同时,在配置文件中指定UserService、ScoreService中的所有方法都开启事务。
上述例子中UserService.logon()执行开始时Spring创建一个新事务,UserService.updateLastLogonTime()和ScoreService.addScore()会加入这个事务中,好像所有的代码都“直接合并”了!
多线程中事务传播的困惑:
还是上面那个例子,加入现在我在UserService.logon()方法中手动新开一个线程,然后在新开的线程中执行ScoreService.add()方法,此时事务传播行为会怎么样?飞线程安全的变量,比如Connection会怎样?改动之后的UserService 代码大体是:
[java] view plaincopy

@Service( "userService")  
public class UserService extends BaseService {  
    @Autowired  
    private JdbcTemplate jdbcTemplate;  
  
@Autowired  
private ScoreService scoreService;  
  
public void logon(String userName) {  
    updateLastLogonTime(userName);  
    Thread myThread = new MyThread(this.scoreService , userName, 20);//使用一个新线程运行  
    myThread .start();  
}  
  
public void updateLastLogonTime(String userName) {  
    String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";  
    jdbcTemplate.update(sql, System. currentTimeMillis(), userName);  
}  
  
private class MyThread extends Thread {  
    private ScoreService scoreService;  
    private String userName;  
    private int toAdd;  
    private MyThread(ScoreService scoreService, String userName, int toAdd) {  
        this. scoreService = scoreService;  
        this. userName = userName;  
        this. toAdd = toAdd;  
    }  
  
    public void run() {  
        scoreService.addScore( userName, toAdd);  
    }  
}  
  
public static void main(String[] args) {  
    ApplicationContext ctx = new ClassPathXmlApplicationContext("com/baobaotao/multithread/applicatonContext.xml" );  
    UserService service = (UserService) ctx.getBean("userService" );  
    service.logon( "tom");  
   }  
   }
  

这个例子中,MyThread会新开一个事务,于是UserService.logon()和UserService.updateLastLogonTime()会在一个事务中,而ScoreService.addScore()在另一个事务中,需要注意的是这两个事务都被事务管理器放在事务上下文中。
结论是:在事务属性为REQUIRED时,在相同线程中进行相互嵌套调用的事务方法工作于相同的事务中。如果互相嵌套调用的事务方法工作在不同线程中,则不同线程下的事务方法工作在独立的事务中。

底层数据库连接Connection访问问题
程序只要使用SpringDAO模板,例如JdbcTemplate进行数据访问,一定没有数据库连接泄露问题!如果程序中显式的获取了数据连接Connection,则需要手工关闭它,否则就会泄露!
当Spring事务方法运行时,事务会放在事务上下文中,这个事务上下文在本事务执行线程中对同一个数据源绑定了唯一一个数据连接,所有被该事务的上下文传播的放发都共享这个数据连接。这一切都在Spring控制下,不会产生泄露。Spring提供了数据资源获取工具类DataSourceUtils来获取这个数据连接.
[java] view plaincopy

@Service( "jdbcUserService" )  
	public class JdbcUserService {  
	    @Autowired  
	    private JdbcTemplate jdbcTemplate;  
     
    @Transactional  
    public void logon(String userName) {  
        try {  
            Connection conn = jdbcTemplate.getDataSource().getConnection();             
            String sql = "UPDATE t_user SET last_logon_time=? WHERE user_name =?";  
            jdbcTemplate.update(sql, System. currentTimeMillis(), userName);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
  
    }  
  
    public static void asynchrLogon(JdbcUserService userService, String userName) {  
        UserServiceRunner runner = new UserServiceRunner(userService, userName);  
        runner.start();  
    }  
  
    public static void reportConn(BasicDataSource basicDataSource) {  
        System. out.println( "连接数[active:idle]-[" +  
                       basicDataSource.getNumActive()+":" +basicDataSource.getNumIdle()+ "]");  
    }  
  
    private static class UserServiceRunner extends Thread {  
        private JdbcUserService userService;  
        private String userName;  
  
        public UserServiceRunner(JdbcUserService userService, String userName) {  
            this. userService = userService;  
            this. userName = userName;  
        }  
  
        public void run() {  
            userService.logon( userName);  
        }  
    }  
  
    public static void main(String[] args) {  
        ApplicationContext ctx = new ClassPathXmlApplicationContext("com/baobaotao/connleak/applicatonContext.xml" );  
        JdbcUserService userService = (JdbcUserService) ctx.getBean("jdbcUserService" );  
        JdbcUserService. asynchrLogon(userService, "tom");  
    }  
    }

在这个例子中,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,则新线程中的方法可以从事务上下文中获取原线程中的数据连接!

引用
https://blog.csdn.net/yiifaa/article/details/74852425
https://blog.csdn.net/weixin_39276098/article/details/78706287

  • 42
    点赞
  • 103
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值