设计者模式-单例模式和多例模式的区别

设计者模式-单例模式和多例模式的区别

1、什么是单例和多例

单例:所有请求用同一个对象来处理。通过单例模式,可以保证系统中一个类只有一个实例。,比如我们常用的service和dao层的对象通常都是单例的。
多例:每个请求用一个新的对象来处理。比如action。

2、为什么用单例多例

1).之所以用单例,是因为没必要每个请求都新建一个对象,这样子既浪费CPU又浪费内存;

2).之所以用多例,是为了防止并发问题;即一个请求改变了对象的状态,此时对象又处理另一个请求,而之前请求对对象状态的改变导致了对象对另一个请求做了错误的处理;

3、用单例和多例的标准只有一个

当对象含有可改变的状态时(更精确的说就是在实际应用中该状态会改变),则多例,否则单例;

4、何时用单例?何时用多例?

对于struts2来说,action必须用多例,因为action本身含有请求参数的值,即可改变的状态;
而对于STRUTS1来说,action则可用单例,因为请求参数的值是放在actionForm中,而非action中的;
另外要说一下,并不是说service或dao一定是单例,标准同第3点所讲的,就曾见过有的service中也包含了可改变的状态,同时执行方法也依赖该状态,但一样用的单例,这样就会出现隐藏的BUG,而并发的BUG通常很难重现和查找;

5、Spring中的单例与多例

spring ioc容器的bean都是默认单例的,即spring依赖注入Bean实例默认是单例的。

spring提供了5中scope,分别是singleton,prototype,request,session,global session,常用是前两种。点此查看官网介绍。

spring中的@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)

6、单例bean与多例(原型)bean的区别

如果一个bean被声明为单例的时候,在处理多次请求的时候,在spring容器里只实例化出一个bean,后续的请求都公用这个对象,这个对象会保存在一个map里面。当有请求来的时候,会先从缓存(map)里查看有没有,有的话直接使用这个对象,没有的话才实例化一个新的对象,所以这是个单例的。但是对于原型(prototype)bean来说,当每次请求来的时候,会直接实例化新的bean,没有缓存以及缓存查询的过程。

7、单例的优势与劣势

优势

由于不会创建新的对象,所以有以下几个性能上的优势:

减少新生成实例的消耗。新生成实例包括两个方面,第一,spring会通过反射或者cglib来生成bean实例,这都是耗性能的操作。第二,给对象分配内存也会涉及负责算法。
减少jvm垃圾回收。由于不会给每个请求都生成bean实例,所以回收的对象就少了。
可以快速获取到bean。因为单例获取bean操作,除了第一次生成之外,其余都是从缓存里获取的,所以很快。

劣势

一个很大的劣势是它不能做到线程安全。由于所有请求都共享一个bean实例,那么如果这个bean是一个有状态的bean的话,在并发场景下就有可能出现问题。

8、单例的写法

第一种(懒汉,线程不安全):

 public class Singleton {  
   private static Singleton instance;  
   private Singleton (){}   
   public static Singleton getInstance() {  
   if (instance == null) {  
       instance = new Singleton();  
   }  
   return instance;  
 }  

这种写法lazy loading很明显,但是致命的是在多线程不能正常工作。

第二种(懒汉,线程安全):

public class Singleton {
   private static Singleton instance;
   private Singleton (){}
   public static synchronized Singleton getInstance() {
     if (instance == null) {
         instance = new Singleton();
     }
     return instance;
   }  
} 

这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是,遗憾的是,效率很低,99%情况下不需要同步。

第三种(饿汉):

public class Singleton {
	private static Singleton instance = new Singleton();
	private Singleton (){}
	public static Singleton getInstance() {
		return instance;
	}  
} 

这种方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。

第四种(饿汉,变种):

public class Singleton {
    private Singleton instance = null;
    static {
        instance = new Singleton();
    }
    private Singleton (){}
    public static Singleton getInstance() {
        return this.instance;
    }  
} 

表面上看起来差别挺大,其实更第三种方式差不多,都是在类初始化即实例化instance。

第五种(静态内部类):

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
} 

这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟第三种和第四种方式不同的是(很细微的差别):第三种和第四种方式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理

第六种(枚举):

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
    }
}

这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过

第七种(双重校验锁):

public class Singleton {  
    private volatile static Singleton singleton;
    private Singleton (){}   
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }  
} 

这个是第二种方式的升级版,俗称双重检查锁定

总结

有两个问题需要注意:

1、如果单例由不同的类装载器装入,那便有可能存在多个单例类的实例。假定不是远端存取,例如一些servlet容器对每个servlet使用完全不同的类装载器,这样的话如果有两个servlet访问一个单例类,它们就都会有各自的实例。

2、如果singleton实现了java.io.Serializable接口,那么这个类的实例就可能被序列化和复原。不管怎样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那你就会有多个单例类的实例。

对第一个问题修复的办法是:

private static Class getClass(String classname) throws ClassNotFoundException {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    if(classLoader == null)
        classLoader = Singleton.class.getClassLoader();
    return (classLoader.loadClass(classname));
} 

对第二个问题修复的办法是:

public class Singleton implements java.io.Serializable { 
    public static Singleton INSTANCE = new Singleton();  
    protected Singleton() {
    }  
    private Object readResolve() {
        return INSTANCE;
    }
}

9、spring单例模式与线程安全

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

线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行的结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多线程之间的切换不会导致该接口的执行结果存在二义性,就是线程安全的。

线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量,静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若多个线程同时执行写操作,一般都需要考虑线程同步,否则就可能影响线程安全。

常量始终是线程安全的,因为只存在读操作;
每次调用方法前都新建一个实例是线程安全的,因为不会访问共享的资源;
局部变量是线程安全的。因为每执行一个方法,都会在独立的空间创建局部变量,它不是共享资源。局部变量包括方法的参数变量和方法内的变量。
在关于spring单例与线程安全的很多文章中,会提到一个概念,即有状态bean和无状态bean。
无状态bean:无状态,就是一次操作,不能保存数据。无状态bean,就是没有实例变量的对象,不能保存数据,是不变类,在线程安全的。
有状态bean:有状态,就是有数据存储功能。有状态bean,就是有实例变量的对象,可以保存数据,是非线程安全的。

如何解决线程安全问题?
(1)使用线程同步机制:通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序缜密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂问题,程序设计和编写难度相对较大。
(2)使用ThreadLocal:为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。

概括起来就是:对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

参考文章链接:

https://blog.csdn.net/qq_43652509/article/details/84330784

https://blog.csdn.net/tanwenfang/article/details/100591821

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值