这篇文章是对网上一些文章的整理,这里学习记录一下

首先,我们先了解一下开发框架

springBoot开发框架

springBoot作为一个轻量级的java开发框架,在许多的方面提出了相应的解决方案。

一般来说基于springBoot的项目基本分为以下几个层次(在项目文件夹中体现为一个层次对应一个package)

  • model层(entity层)
  • Dao层(mapper层)
  • service层(业务层)
  • controller层(控制层)

各层的作用

MODEL层

即数据库实体层,也被称为entity层,pojo层,存放的是实体类,属性值与数据库中的属性值保持一致。 实现set和get方法。

一般数据库一张表对应一个实体类,类属性同表字段一一对应

Dao层

即数据持久层,对数据做持久化操作。也被称为mapper层。声明为接口。

dao层的作用为访问数据库,向数据库发送sql语句,完成数据的增删改查任务。

Service层

业务层,service层的作用为完成功能设计。存放业务逻辑处理,不直接对数据库进行操作,有接口和接口实现类,提供controller层调用的方法。

调用dao层接口,接收dao层返回的数据,完成项目的基本功能设计。(也就是说对于项目中的功能的需求就是在这里完成的)

(对Dao层接口的实现)

Controller层

控制器层,controller层的功能为请求和响应控制。

controller层负责前后端交互,接受前端请求,调用service层,接收service层返回的数据,最后返回具体的页面和数据到客户端。

因此对于一个Web项目,从发起请求到给与响应的流程是这样的:

image-20201117113111785

 

1.Spring与线程安全

首先问@Controller @Service是不是线程安全的?

答:默认配置下不是的。为啥呢?因为默认情况下@Controller没有加上@Scope,没有加@Scope就是默认值singleton,单例的。意思就是系统只会初始化一次Controller容器,所以每次请求的都是同一个Controller容器,当然是非线程安全的

说明:

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

Spring对每个bean提供了一个scope属性来表示该bean的作用域。它是bean的生命周期。例如,一个scope为singleton的bean,在第一次被注入时,会创建为一个单例对象,该对象会一直被复用到应用结束。

使用方式:@Scope(BeanDefinition.SCOPE_PROTOTYPE)

  • singleton:默认的scope,每个scope为singleton的bean都会被定义为一个单例对象,该对象的生命周期是与Spring IOC容器一致的(但在第一次被注入时才会创建)。

  • prototype:bean被定义为在每次注入时都会创建一个新的对象。

  • request:bean被定义为在每个HTTP请求中创建一个单例对象,也就是说在单个请求中都会复用这一个单例对象。

  • session:bean被定义为在一个session的生命周期内创建一个单例对象。

  • application:bean被定义为在ServletContext的生命周期中复用一个单例对象。

  • websocket:bean被定义为在websocket的生命周期中复用一个单例对象。

       我们交由Spring管理的大多数对象其实都是一些无状态的对象,这种不会因为多线程而导致状态被破坏的对象很适合Spring的默认scope,每个单例的无状态对象都是线程安全的(也可以说只要是无状态的对象,不管单例多例都是线程安全的,不过单例毕竟节省了不断创建对象与GC的开销)。

       无状态的对象即是自身没有状态的对象,自然也就不会因为多个线程的交替调度而破坏自身状态导致线程安全问题。无状态对象包括我们经常使用的DO、DTO、VO这些只作为数据的实体模型的贫血对象,还有Controller、Service、DAO,这些对象并没有自己的状态,它们只是用来执行某些操作的。例如,每个DAO提供的函数都只是对数据库的CRUD,而且每个数据库Connection都作为函数的局部变量(局部变量是在用户栈中的,而且用户栈本身就是线程私有的内存区域,所以不存在线程安全问题),用完即关(或交还给连接池)。

       有人可能会认为,我使用request作用域不就可以避免每个请求之间的安全问题了吗?这是完全错误的,因为Controller默认是单例的,一个HTTP请求是会被多个线程执行的,这就又回到了线程的安全问题。当然,你也可以把Controller的scope改成prototype,实际上Struts2就是这么做的,但有一点要注意,Spring MVC对请求的拦截粒度是基于每个方法的,而Struts2是基于每个类的,所以把Controller设为多例将会频繁的创建与回收对象,严重影响到了性能

       通过阅读上文其实已经说的很清楚了,Spring根本就没有对bean的多线程安全问题做出任何保证与措施。对于每个bean的线程安全问题,根本原因是每个bean自身的设计。不要在bean中声明任何有状态的实例变量或类变量,如果必须如此,那么就使用ThreadLocal把变量变为线程私有的,如果bean的实例变量或类变量需要在多个线程之间共享,那么就只能使用synchronized、lock、CAS等这些实现线程同步的方法了

       下面将通过解析ThreadLocal的源码来了解它的实现与作用,ThreadLocal是一个很好用的工具类,它在某些情况下解决了线程安全问题(在变量不需要被多个线程共享时)。

 

2. spring中的并发访问题:

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

2.1 有状态:

如果用有状态的bean,也可以使用用prototype模式,每次在注入的时候就重新创建一个bean,在多线程中互不影响,但是增加了GC,影响性能

有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式。Prototype: 每次对bean的请求都会创建一个新的bean实例。

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

2.2 无状态:

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

无状态的Bean适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。

 

Servlet体系结构是建立在Java多线程机制之上的,它的生命周期是由Web 容器负责的。一个Servlet类在Application中只有一个实例存在,也就是有多个线程在使用这个实例。这是单例模式的应用。如Service层、Dao层用默认singleton就行,虽然Service类也有dao这样的属性,但dao这些类都是没有状态信息的,也就是相当于不变(immutable)类,所以不影响。Struts2中的Action因为会有User、BizEntity这样的实例对象,是有状态信息的,在多线程环境下是不安全的,所以Struts2默认的实现是Prototype模式。在Spring中,Struts2的Action中,scope要配成prototype作用域。 (单例模式-单例注册表实现和threadLocal-可以处理有状态的bean之间的关系)

还有我们的实体bean,从客户端传递到后台的controller–>service–>Dao,这一个流程中,他们这些对象都是单例的,那么这些单例的对象在处理我们的传递到后台的实体bean不会出问题吗?
答:[实体bean不是单例的],并没有交给spring来管理,每次我们都手动的New出来的【如EMakeType et = new EMakeType();】,所以即使是那些处理我们提交数据的业务处理类是被多线程共享的,但是他们处理的数据并不是共享的,数据时每一个线程都有自己的一份,所以在数据这个方面是不会出现线程同步方面的问题的。

在这里补充下自己在项目开发中对于实体bean在多线程中的处理:

1.对于实体bean一般通过方法参数的的形式传递(参数是局部变量),所以多线程之间不会有影响。

2.有的地方对于有状态的bean直接使用prototype原型模式来进行解决(当然要考虑GC性能问题)

3.对于使用bean的地方可以通过new的方式来创建(当然要考虑GC性能问题)

 

但是那些的在Dao中的xxxDao,或controller中的xxxService,这些对象都是单例那么就会出现线程同步的问题。但是话又说回来了,这些对象虽然会被多个进程并发访问,可我们访问的是他们里面的方法,这些类里面通常不会含有成员变量,那个Dao里面的ibatisDao是框架里面封装好的,已经被测试,不会出现线程同步问题了。所以出问题的地方就是我们自己系统里面的业务对象,所以我们一定要注意这些业务对象里面千万不能要独立成员变量,否则会出错。

spring对那些个有状态bean使用ThreadLocal维护变量[仅仅是变量,因为线程同步的问题就是成员变量的互斥访问出问题]时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

对spring并发访问线程安全的两篇博客汇总,可以得出上述结论…………..

由于Spring MVC默认是Singleton的,所以会产生一个潜在的安全隐患。根本核心是instance**的变量保持状态**的问题。这意味着每个request过来,系统都会用原有的instance去处理,这样导致了两个结果(单例的好处):

一是我们不用每次创建Controller,
二是减少了对象创建和垃圾收集的时间;
由于只有一个Controller的instance,当多个线程同时调用它的时候,它里面的instance**变量**(可以理解为私有变量)就不是线程安全的了,会发生窜数据的问题。
当然大多数情况下,我们根本不需要考虑线程安全的问题,比如service、dao等,除非在bean中声明了实例变量。因此,我们在使用spring mvc 的contrller时,应避免在controller中定义实例变量(singleton唯一的不好是单例的变量容易出现问题,下面有解决的方案)
如:

public  class  Controller  extends  AbstractCommandController  {
......
protected  ModelAndView handle(HttpServletRequest request,HttpServletResponse response,
  Object command,BindException errors)  throws  Exception  {
company =  ................;
}
protected  Company company;
}

在这里有声明一个变量company,这里就存在并发线程安全的问题。
如果控制器是使用单例形式,且controller中有一个私有的变量a,所有请求到同一个controller时,使用的a变量是共用的,即若是某个请求中修改了这个变量a,则,在别的请求中能够读到这个修改的内容。。

2.3 有几种解决方法

1、在控制器中不使用实例变量(可以使用方法参数的形式解决,参考博文 Spring Bean Scope 有状态的Bean 无状态的Bean
2、将控制器的作用域从单例改为原型,即在spring配置文件Controller中声明 scope=”prototype”,每次都创建新的controller
3、在Controller中使用ThreadLocal变量

这几种做法有好有坏:

第一种,需要开发人员拥有较高的编程水平与思想意识,在编码过程中力求避免出现这种BUG,

第二种则是容器自动的对每个请求产生一个实例,由JVM进行垃圾回收,因此做到了线程安全。

使用第一种方式的好处是实例对象只有一个,所有的请求都调用该实例对象,速度和性能上要优于第二种,不好的地方,就是需要程序员自己去控制实例变量的状态保持问题。第二种由于每次请求都创建一个实例,所以会消耗较多的内存空间。

所以在使用spring开发web 时要注意,默认Controller、Dao、Service都是单例的。

 

研究一下,Spring中的源码,它对常用的开源框架做了大量封装,如,Hibernate中的sessionFactory,就使用的是 org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean,而在 AnnotationSessionFactoryBean的父类LocalSessionFactoryBean中,定义了大量的ThreadLocal来保证多线程的安全性

public class LocalSessionFactoryBean extends AbstractSessionFactoryBean implements BeanClassLoaderAware {
    private static final ThreadLocal<DataSource> configTimeDataSourceHolder = new ThreadLocal<DataSource>();
    private static final ThreadLocal<TransactionManager> configTimeTransactionManagerHolder = new ThreadLocal<TransactionManager>();
    private static final ThreadLocal<Object> configTimeRegionFactoryHolder = new ThreadLocal<Object>();
    private static final ThreadLocal<CacheProvider> configTimeCacheProviderHolder = new ThreadLocal<CacheProvider>();
    private static final ThreadLocal<LobHandler> configTimeLobHandlerHolder = new ThreadLocal<LobHandler>();
}

4. Spring中的多线程疑惑

我们需要认清:

  1. Application添加了@EnableAsync注释将使用异步模式,@Controller到@Service是线程异步的。
  2. web容器本身就是多线程的,每一个HTTP请求都会产生一个独立的线程(或者从线程池中取得创建好的线程);
  3. Spring中的bean(用@Repository、@Service、@Component和@Controller注册的bean)都是单例的,即整个程序、所有线程共享一个实例
  4. 虽然bean都是单例的,但是Spring提供的模板类(XXXTemplate),在Spring容器的管理下(使用@Autowired注入),会自动使用ThreadLocal以实现多线程;
  5. 即类是单例的,但是其中有可能出现并发问题的变量使用ThreadLocal实现了多线程。
  6. 注意除了Spring本身提供的类以外,在Bean中定义“有状态的变量”(即有存储数据的变量),其会被所有线程共享,很可能导致并发问题,需要自行另外使用ThreadLocal进行处理,或者将Bean声明为prototype型
  7. 一个类中的方法实际上是独立,方法内定义的局部变量在每次调用方法时都是独立的,不会有并发问题。只有类的“有状态的”全局变量会有并发问题

结论:

  1. 使用Spring提供的template等类没有多线程问题!
  2. 一般来说只有类的属性/全局变量会导致多线程问题,而方法内的局部变量不会有并发问题
  3. 单例模式肯定是线程不安全的! spring的Bean中的自定义的成员变量除非进行threadlocal封装,否则都是非线程安全的!

5、Spring中的prototype和@Autowired

5.1、使用@Autowired没有实现多个实例

注意即使使用@Scope(BeanDefinition.SCOPE_PROTOTYPE)将Bean声明为prototype,如果:

  1. 外层的类是singleton
  2. 使用@Autowired注入

这样的话仍然只会有一个实例。例如:

使用@Scope(BeanDefinition.SCOPE_PROTOTYPE)声明的Bean

@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ClassB {

    private Integer num = Integer.valueOf(0);

    public Integer getNum() {
        return num;
    }

    public void setNum(Integer num) {
        this.num = num;
    }
}

使用@Autowired注入到ClassA中

/**
 * Created by ASUS on 2017/5/24.
 */
@Component
public class ClassA {
    @Autowired
    private ClassB classB;

    public Integer addNum(){
        classB.setNum(classB.getNum()+1);
        System.out.println(classB.getNum());
        return classB.getNum();
    }
}

通过Controller调用,用@Autowire将ClassA注入。 


@RestController
public class Controller {

    @Autowired
    private ClassA classA;

    @RequestMapping("/")
    public String print(){
        return classA.addNum().toString();
    }
}

每一次访问都会导致num+1,访问8次后:

并没有实现“多个实例”的效果,一次都是在操作同一个ClassB实例。这是因为使用@Autowired实际上和直接new是一个效果,只是交由Spring容器实现而已。而ClassA本身是一个单例,单例只会实例化一次,这样其属性自然也就只会被实例化一次。

5.2、解决方法

5.2.1、直接在方法中声明局部变量

在Java或者其他语言中,每个“方法”在被调用时,都会重新声明一遍方法中的局部变量。如果想要classB为多实例,直接在方法中声明即可。比如:

@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ClassA {
    public Integer addNum(){
        //不使用@Autowired,直接在方法中声明
        ClassB classB = new ClassB();
        classB.setNum(classB.getNum()+1);
        System.out.println(classB.getNum());
        return classB.getNum();
    }
}

注意:

这样做的问题在于,如果ClassB中需要使用@Autowired,则这个@Autowired会失效

比如想在ClassB中使用Spring管理的JdbcTemplate,就需要使用@Autowired。如果ClassB不是通过@Autowired实例化的,ClassB中的JdbcTemplate就会注入失败,导致NullPointerException。

5.2.2、使用ThreadLocal管理属性

还要一个方法实现和多实例“类似”的功能,即使用ThreadLocal来管理类中的属性。例如对于上面的例子:

@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ClassB {

    //使用ThreadLocal管理属性,每个线程都操作一个新的副本
    private static ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public ThreadLocal<Integer> getIntegerThreadLocal() {
        return integerThreadLocal;
    }
}

在ClassB中就可以正常使用@Autowired进行注入。 

@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ClassA {

    @Autowired
    ClassB classB;

    public Integer addNum(){
        Integer integer = classB.getIntegerThreadLocal().get();
        integer++;
        return integer;
    }
}

 

参考:

https://sylvanassun.github.io/2017/11/06/2017-11-06-spring_and_thread-safe/

https://www.javatt.com/p/28066

https://my.oschina.net/pierrecai/blog/870714

https://blog.csdn.net/qq_28323373/article/details/102857176

 

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐