spring单例模式与线程安全问题的解决方案

一句话总结:1:spring容器创建对象的方式是单例的

 2:spring单例模式的安全问题是使用ThreadLocal解决的


单例模式的意思就是只有一个实例。单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。

当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求多对应的业务逻辑(成员方法),此时就要注意了,如果该处理逻辑中有对该单列状态的修改(体现为该单列的成员属性),则必须考虑线程同步问题

 在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。 
 
  而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。 
 
  由于ThreadLocal中可以持有任何类型的对象,低版本JDK所提供的get()返回的是Object对象,需要强制类型转换。但JDK 5.0通过泛型很好的解决了这个问题,在一定程度地简化ThreadLocal的使用
 概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。 
Spring使用ThreadLocal解决线程安全问题 
 
  我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。 
 
  一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。 
如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。 或者说:一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。   线程安全问题都是由全局变量及静态变量引起的。  
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。
1) 常量始终是线程安全的,因为只存在读操作。 
2)每次调用方法前都新建一个实例是线程安全的,因为不会访问共享的资源。
3)局部变量是线程安全的。因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享的资源。局部变量包括方法的参数变量和方法内变量。
有状态就是有数据存储功能。有状态对象(Stateful Bean),就是有实例变量的对象  ,可以保存数据,是非线程安全的。在不同方法调用间不保留任何状态。
无状态就是一次操作,不能保存数据。无状态对象(Stateless Bean),就是没有实例变量的对象  .不能保存数据,是不变类,是线程安全的。
有状态对象:
无状态的Bean适合用不变模式,技术就是单例模式,这样可以共享实例,提高性能。有状态的Bean,多线程环境下不安全,那么适合用Prototype原型模式。Prototype: 每次对bean的请求都会创建一个新的bean实例。
Struts2默认的实现是Prototype模式。也就是每个请求都新生成一个Action实例,所以不存在线程安全问题。需要注意的是,如果由Spring管理action的生命周期, scope要配成prototype作用域。

 

二、线程安全案例:

SimpleDateFormat(下面简称sdf)类内部有一个Calendar对象引用,它用来储存和这个sdf相关的日期信息,例如sdf.parse(dateStr), sdf.format(date) 诸如此类的方法参数传入的日期相关String, Date等等, 都是交友Calendar引用来储存的.这样就会导致一个问题,如果你的sdf是个static的, 那么多个thread 之间就会共享这个sdf, 同时也是共享这个Calendar引用, 并且, 观察 sdf.parse() 方法,你会发现有如下的调用:
Date parse() {
 
  calendar.clear(); // 清理calendar
 
  ... // 执行一些操作, 设置 calendar 的日期什么的
 
  calendar.getTime(); // 获取calendar的时间
 
}
 
这里会导致的问题就是, 如果 线程A 调用了 sdf.parse(), 并且进行了 calendar.clear()后还未执行calendar.getTime()的时候,线程B又调用了sdf.parse(), 这时候线程B也执行了sdf.clear()方法, 这样就导致线程A的的calendar数据被清空了(实际上A,B的同时被清空了). 又或者当 A 执行了calendar.clear() 后被挂起, 这时候B 开始调用sdf.parse()并顺利i结束, 这样 A 的 calendar内存储的的date 变成了后来B设置的calendar的date
这个问题背后隐藏着一个更为重要的问题--无状态:无状态方法的好处之一,就是它在各种环境下,都可以安全的调用。衡量一个方法是否是有状态的,就看它是否改动了其它的东西,比如全局变量,比如实例的字段。format方法在运行过程中改动了SimpleDateFormat的calendar字段,所以,它是有状态的。
 
  这也同时提醒我们在开发和设计系统的时候注意下一下三点:
 
  1.自己写公用类的时候,要对多线程调用情况下的后果在注释里进行明确说明
  2.对线程环境下,对每一个共享的可变变量都要注意其线程安全性
  3.我们的类和方法在做设计的时候,要尽量设计成无状态的
 三.解决办法
 
  1.需要的时候创建新实例:
 
  说明:在需要用到SimpleDateFormat 的地方新建一个实例,不管什么时候,将有线程安全问题的对象由共享变为局部私有都能避免多线程问题,不过也加重了创建对象的负担。在一般情况下,这样其实对性能影响比不是很明显的。
 
  2.使用同步:同步SimpleDateFormat对象
 
public class DateSyncUtil {
 
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      
    public static String formatDate(Date date)throws ParseException{
        synchronized(sdf){
            return sdf.format(date);
        }  
    }
    
    public static Date parse(String strDate) throws ParseException{
        synchronized(sdf){
            return sdf.parse(strDate);
        }
    } 
}
  说明:当线程较多时,当一个线程调用该方法时,其他想要调用此方法的线程就要block,多线程并发量大的时候会对性能有一定的影响。
 
  3.使用ThreadLocal: 
 
public class ConcurrentDateUtil {
 
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>() {
        @Override
        protected DateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };
 
    public static Date parse(String dateStr) throws ParseException {
        return threadLocal.get().parse(dateStr);
    }
 
    public static String format(Date date) {
        return threadLocal.get().format(date);
    }
}
public class ThreadLocalDateUtil {
    private static final String date_format = "yyyy-MM-dd HH:mm:ss";
    private static ThreadLocal<DateFormat> threadLocal = new ThreadLocal<DateFormat>(); 
 
    public static DateFormat getDateFormat()   
    {  
        DateFormat df = threadLocal.get();  
        if(df==null){  
            df = new SimpleDateFormat(date_format);  
            threadLocal.set(df);  
        }  
        return df;  
    }  
 
    public static String formatDate(Date date) throws ParseException {
        return getDateFormat().format(date);
    }
 
    public static Date parse(String strDate) throws ParseException {
        return getDateFormat().parse(strDate);
    }   
}
  说明:使用ThreadLocal, 也是将共享变量变为独享,线程独享肯定能比方法独享在并发环境中能减少不少创建对象的开销。如果对性能要求比较高的情况下,一般推荐使用这种方法。
 
  4.抛弃JDK,使用其他类库中的时间格式化类:
 
  1.使用Apache commons 里的FastDateFormat,宣称是既快又线程安全的SimpleDateFormat, 可惜它只能对日期进行format, 不能对日期串进行解析。
 
  2.使用Joda-Time类库来处理时间相关问题
 
  做一个简单的压力测试,方法一最慢,方法三最快,但是就算是最慢的方法一性能也不差,一般系统方法一和方法二就可以满足,所以说在这个点很难成为你系统的瓶颈所在。从简单的角度来说,建议使用方法一或者方法二,如果在必要的时候,追求那么一点性能提升的话,可以考虑用方法三,用ThreadLocal做缓存。
 
  Joda-Time类库对时间处理方式比较完美,建议使用。

  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1. 单元测试:(掌握) 1. 写一个单元测试类,命名方式:XxxTest(测试类没有main方法) 2. 导入包,Junit4包 选中项目,右键 => Build Path => Add Library => 选中Junit 选中Junit4 => finish 3. 在测试类中,设计测试方法,命名方式:testXxx() 在方法上方会自动添加一个@Test注解 4. 运行,右键run as JUnit方式 如果是全部执行,直接选中 @Test注解,右键run as JUnit方式 如果是某一个方法,直接选中方法名,右键run as JUnit方式 @Before// 预执行注解,每次执行方法前都会执行该注解 @After// 每次执行方法后都会执行该注解 5. 运行后结果解释: 绿条:表示成功执行 红条:表示执行失败 1. 代码有异常 2. 执行失败,逻辑异常 6. 自己扩展(测试类中的断言assert) 2. 工具类:(掌握) 1. 概念:就是类中只有(静态、非静态)方法,就是为了调用方法。 2. 作用:就是一个轮子 3. 分类: 1. 静态的 方法全部都是static修饰的,调用方式:类名.方法名(...); 2. 非静态的 通过单例模式获取到对象后。就是只有一个对象的类 方法全部都是非static修饰的,调用方式:对象名.方法名(...); 4. 工具类命名: XxxUtil、XxxUtils、XxxTool、XxxTools 3. jar 包 (掌握) 1. 什么是jar包:就是别人封装好的字节码文件 2. 作用:就是提高开发效率的。使用轮子 3. 使用: 3.1 使用别人写好的jar包 step1:在当前项目下,创建一文件夹folder,叫lib step2:将jar包复制到当前的lib文件夹中 step3:将所有的jar包选中右键,build path add library step4:在我们代码中通过创建对象或者类名的方式使用即可 3.2 使用自己的jar包 (造轮子) step1:选中自己想要封装成jar包的类,右键export导出 在输入框中输入 jar: 1. jar File : 普通的jar包,没有main方法(最常用) 在下面写一个jar包的名字,然后选择生成jar包的路径,finish即可 2. Runnable jar File:可以自己执行的jar包,有main方法 (几乎不用) 要封装为可以执行的jar包,必须先运行一下当前类。 然后在下面写一个jar包的名字,然后选择生成jar包的路径,finish即可 在cmd控制台中,切换路径到jar文件所在的目录。 输入 :java -jar jar包名.jar step2:剩下的就是按照 3.1中的步骤使用即可 4. Properties资源(配置)文件的解析(重点) ---------------------------------(注意:重点)------------------------------------- 以后配置文件都要放到项目中的一个或者几个单独的文件夹中,为了好管理。 该文件夹类型是source folder类型,源文件夹,eclipse工具会自动编译 --------------------------------------------------------------------------------- 传统方式获取流是new创建的,而在web开发中有可能获取不到配置文件,为了避免这种情况,用以下方式获取流: 1. 通过当前类字节码文件的方式,一般是用当前类的字节码文件 当前类.class.getResourceAsStream("/文件路径/文件名"); /:如果是resource文件夹,直接写文件名,如果是普通文件夹写文件夹名/文件名 2. 通过类加载器的方式 1.同当前类的加载器获取 1.当前类.class.getClassLoader();//获取当前类加载器 2.classLoader.getResourceAsStream("文件路径/文件名");//通过类加载器获取流对象,如果是源文件夹,直接文件名 2.通过当前线程的类加载器获取流 1、ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); //获取当前线程类加载器 2、InputStream is = classLoader.getResourceAsStream("文件路径/文件名");//通过当前线程的类加载器获取流对象,如果是源文件夹,直接文件名 建议用当前线程类的加载器方式获取流,线程安全问题 类加载器:( 扩展,5个月后会讲) 加载字节码的一个类而已: 1. 启动类加载器(Bootstrap classLoader) c++ 本地代码实现的类加载器,它负责将 <JAVA_HOME>/lib下面的核心类库 或 -Xbootclasspath选项指定的jar包等 虚拟机识别的类库 加载到内存中。 由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用, 所以 不允许直接通过引用进行操作。 2. 拓展类加载器(Extension classLoader): 扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader) 实现的,它负责将 <JAVA_HOME >/lib/ext或者由系统变量-Djava.ext.dir指定位置 中的类库 加载到内存中。开发者可以直接使用标准扩展类加载器。 3. 系统类 (应用类)加载器(Application classLoader): 系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的, 它负责将 用户类路径(java -classpath或-Djava.class.path变量所指的目录, 即当前类所在路径及其引用的第三方类库的路径,如第四节中的问题6所述)下的类库 加载到内存中。 开发者可以直接使用系统类加载器。 5. 设计模式:(框架中使用,是程序设计的高级思想) 1. 单例模式:(重点) 1. 概念:设计一个类,这个类只能创建一个对象。(限制创建对象对象数量) 2. 怎么设计? 控制创建对象的数量 => 创建对象通过new 调用构造方法 => 控制构造方法就能控制创建对象 控制调用构造方法 => 用private修饰 => 需要给外部提供一个对象 => 先在类中创建一个对象 (联想到封装) => 提供一个公共的 getInstance给外部返回一个对象 3. 步骤: 1. 私有化构造方法 2. 在类中创建一个对象,并且用private、static、final修饰 private为了避免在外部直接访问当前对象 static是为了在静态方法中可以返回当前类中的对象 final:可加可不加,加了可以保证不可修改,且提供获取效率 3. 提供一个public修饰的方法getInstance给外部返回一个对象 4. 单例模式获取方式: 1. 饿汉模式:预加载模式 (优化方式:静态内部类) 优点:在类加载的时候,就创建好对象放到静态区了,获取对象效率高。线程安全 缺点:类加载效率低,并且static修饰的成员占用资源。 2. 懒汉模式:懒加载模式 (优化方式:双重校验锁) 优点:节省资源,在需要的时候创建对象。 缺点:线程安全。获取对象的时候,效率低 最简单的线程安全的方式:同步方法,效率低 更好的的线程安全的方式:双重校验锁 3. 枚举: public enum Singleton{ INSTANCE; } 5. 作用: 1. 全局配置文件 2. 全局日志文件 3. 节省资源,例如,回收站、资源管理器、游戏单开窗口 6. 使用选择: 一般用饿汉模式,如果有序列化(自己百度扩展)要求,用枚举。 7. 不需要频繁创建对象的时候;不允许多个对象 用单例 2. 装饰者模式:(了解) 装饰者模式指的是在不必改变原类(Input)文件和使用继承的情况下,动态地扩展一个对象的功能。 它是通过创建一个包装对象,也就是装饰来包裹真实的对象。 实现步骤 :通过对原类文件继承,对原有方法功能的基础上,增强新的功能 使用场景: 不必改变原类,并且对原有功能进行加强的时候,就需要用装饰者模式 3. 适配器模式:(了解) 适配器类: 命名方式: XxxXxxAdaptor 解决了两个功能的协同工作。(苹果充电器和华为手机充电的问题) 扩展性强,成本低廉 使用场景: 不同的类需要配合完成功能 4. 简单工厂模式:(了解)Spring框架中有用到 简单工厂模式:(后面可以用反射优化代码) 优点: 1.将生产和消费分离(解耦),即创建对象和使用对象的功能分离,便于管理 缺点: 1.扩展性弱,如果添加了新的产品,则需要修改工厂方法 使用场景: 适用于产品数量较少,且不经常改变的情况 6. ThreadLocal类 (掌握简单使用): 解决了线程安全问题,通过线程隔离有安全问题的数据实现的,底层是通过map保存线程id和值的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值