设计模式之单例模式
目录:
- 什么是单例模式
- 单例模式的应用场景
- 单例模式的优缺点
- 单例模式的实现
- 总借
一、什么是单例模式
单例模式顾名思义就是只存在一个实例,也就是系统代码中只需要一个对象的实例应用到全局代码中,有点类似全局变量。例如,在系统运行时,系统需要读取配置文件中的参数,在设计系统的时候读取配置文件的类往往设计成单例类。因为系统从启动运行到结束,只需要读取一次配置文件,这个读取配置文件全部由该类负责读取,在全局代码中只需要使用该类即可。这样不仅简化了配置文件的管理,也避免了代码读取配置文件数据的不一致性。
单例模式的特点:
1、该类的构造方法声明为private,这样其他类无法初始花该类,只能通过该类的public方法获取该类的对象。
2、里面有个私有的对象成员,该成员对象是类本身,用于public方法返回该类的实例。
3、该类中提供一个public的静态方法,返回该类的私有成员对象。
二、单例的应用场景
举一个小例子,在我们的windows桌面上,我们打开了一个回收站,当我们试图再次打开一个新的回收站时,Windows系统并不会为你弹出一个新的回收站窗口。,也就是说在整个系统运行的过程中,系统只维护一个回收站的实例。这就是一个典型的单例模式运用。
继续说回收站,我们在实际使用中并不存在需要同时打开两个回收站窗口的必要性。假如我每次创建回收站时都需要消耗大量的资源,而每个回收站之间资源是共享的,那么在没有必要多次重复创建该实例的情况下,创建了多个实例,这样做就会给系统造成不必要的负担,造成资源浪费。
再举一个例子,网站的计数器,一般也是采用单例模式实现,如果你存在多个计数器,每一个用户的访问都刷新计数器的值,这样的话你的实计数的值是难以同步的。但是如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。同样多线程的线程池的设计一般也是采用单例模式,这是由于线程池需要方便对池中的线程进行控制
同样,对于一些应用程序的日志应用,或者web开发中读取配置文件都适合使用单例模式,如HttpApplication 就是单例的典型应用。
从上述的例子中我们可以总结出适合使用单例模式的场景和优缺点:
适用场景:
1.需要生成唯一序列的环境
2.需要频繁实例化然后销毁的对象。
3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
4.方便资源相互通信的环境
三、单例模式的优缺点
优点:
1、在内存中只有一个对象,节省内存空间;
2、避免频繁的创建销毁对象,可以提高性能;
3、避免对共享资源的多重占用,简化访问;
4、为整个系统提供一个全局访问点。
缺点:
1、不适用于变化频繁的对象;
2、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
3、如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;
四、单例模式的实现
1、饿汉式
public class Mgr{ //创建自己的实例,并初始化私有静态final成员 private static final Mgr mgr = new Mgr(); //私有构造方法 private Mgr() {}; //公共方法,返回自己的实例化成员 public static Mgr getMgr() { return mgr; } }
备注:该单例实现方法简单明了,推荐使用。该类被JVM加到内存的时候,只会加载一次,并且只实例化一个单例,优点是具有线程安全性,缺点是:不用他也在内存中实例化,浪费内存。所以提出了懒散式实现方式。
2、懒汉式
public class Mgr{ //声明私有静态对象成员,作为返回值 private static Mgr mgr; //私有构造函数 private Mgr() {}; //懒汉的特点,提供公共静态方法,如果该成员对象为空,实例化并返回 public static Mgr getMgr() { if(mgr == null){ mgr = = new Mgr(); } return mgr; } }
备注:(不推荐用)这种实现方法只有程序在调用该类的getMgr方法才实例话对象并返回,特点就是调用的时候再实例化并返回,延迟加载的被动形式。但是该实现方法不是线程安全的,因为当同时有有两个线程执行到if(mgr==null)语句的时候,由于某些原因其中一个线程先一步执行下一句,实例化了对象并返回;两一个线程再实例化对象在返回,这两个线程返回的对象不是同一个对象(这难道还是单例吗!),所以该实现方法的缺点也很明显。那为了避免线程不安全问题,在懒汉写法上提出加锁的实现方式。
3、给公共方法加锁
public class Mgr{ //声明私有静态对象成员,作为返回值 private static Mgr mgr; //私有构造函数 private Mgr() {}; //给公共方法加锁,只有一个线程第一次获得锁实例化对象并返回 public static syncnronized Mgr getMgr() { if(mgr == null){ mgr = new Mgr(); } return mgr; } }
备注:(推荐使用)这种实现方式比较完善,既保证了懒散式的延迟加载方式,也保证了线程安全。缺点是在整个方法上加锁,导致性能下降。因为只有第一次获得锁的线程实例化对象并返回,以后的线程获得锁的时候执行 if(mgr == null)语句的时候,由于mgr已经实例化了不为空,直接跳过返回实例。整个过程要竞争锁,不能并发执行导致性能下降。那为优化性能下降问题,那我只在mgr = new Mgr()上加锁,保证锁粒度最小化的同时保证单例实例化。
4、给实例化加锁
public class Mgr{ //私有静态成员对象 private static Mgr mgr; //私有构造函数 private Mgr() {}; //公共方法,在实例化语句块加锁,保证单例 public static Mgr getMgr() { if(mgr == null){ syncnronized(Mgr.class){ mgr = new Mgr(); } } return mgr; } }
备注:(不推荐使用)该实现方法虽然相较方法3性能有所提升,但并不能保证线程安全。因为当两个线程同时执行if(mgr == null)语句时,其中线程1获取锁,实例化对象并返回,线程2在获得锁又在实例化对象并返回。线程1和线程2获取的对象并不是同一个。所以在此基础上提出了双重判断方式。
5、双重判断加锁
public class Mgr{ //私有静态成员对象 private static Mgr mgr; //私有构造函数 private Mgr() {}; //公共方法提供双重判断并在实例化代码块加锁 public static Mgr getMgr() { if(mgr == null){ //第一次判断 syncnronized(Mgr.class){ if(mgr == null){ //第二次判断 mgr = = new Mgr(); } } } return mgr; } }
备注:(推荐使用)相较于方法4,该方法双重判定,如果多线程同时执行到第一次判断语句位置,其中一个线程获得锁,由于是第一次该对象成员为空,实例化后并返回。其后其它线程调用公共方法的时候,由于实例化了,在第一次判断自接返回实例,不在产生锁竞争。大大提高了效率,保证了线程的安全性,也保证了延迟加载的特性。
6、静态内部类
public class Mgr{ private Mgr() {}; //定义静态内部类 private static class MgrHolder{ private final static Mgr mgr = new Mgr(); } //公共方法直接返回静态内部类的实例对象 public static Mgr getMgr() { return MgrHolder.mgr; } }
备注:(可使用)该实现方法通过JVM来保证线程安全性,静态内部类MgrHolder来New一个Mgr对象,JVM只会加载一次Mgr类(静态内部类不会加载),当类调用getMgr方法的时候,也只会调用一次,公共方法调用静态内部类,获取一个对象(也是实现懒加载)。所以也是线程安全的。
7、枚举类单例模式
public enum Mgr{ mgr; public void m(){} //业务方法 }
备注:(推荐使用)jdk1.5之后才能正常达到单例效果,参考来自《Effective Java》。注意枚举类的枚举变量必须写在第一行,后面实现业务代码。调用方式是:Mgr.mgr.Function_Name();具备枚举类型的特点,有点是:线程同步,防止反序列化。
五、总结
通过上面几种单例模式的实现方式的列举,但是在实际应用中其中的2,3,4三种方式并不适用,列出来只是让读者更好的理解方式5的由来,起到抛砖引玉的作用,更好的理解单例模式。总之常用的四种,懒汉,双重校验锁,静态内部类,枚举单例。
饿汉:类加载的时候就创建实例,所以是线程安全的,但不能延迟加载。
双重校验锁:线程安全,效率较高,延迟加载。
静态内部类:实现起来比较麻烦,在不同的编译器上会出现不可预知的错误。
枚举单例:很好,不仅避免了多线程同步问题,而且能反正反序列化重新创建对象,但是不能延迟加载,用的人少。
- 读者发现有什么有问题的地方谢谢留言指正。部分参考自:https://www.cnblogs.com/xuwendong/p/9633985.html#_label0