个人随笔
目录
三、创建型-单例(Singleton)模式
2020-09-09 21:30:19

单例模式(Singleton)是一种常用的设计模式。在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:

  1. 某些类创建比较频繁,对一些大型的对象,这是一笔很大的开销。
  2. 省去了new操作符,降低了系统内存的使用频率,减轻GC的压力。
  3. 有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以多个创建的话,系统完全乱了。(比如一个军队有多个司令),所以只有使用单例模式,才能保证核心交易服务器独立控制着整个流程。

单例模式的实现方式有两种,饿汉式和懒汉式,饿汉式是在类加载的时候就初始化了实例,它们的优缺点如下:

  1. 饿汉式优点: 在多线程模式下是安全的
  2. 缺点: 没有调用方法前就被加载,会占用内存
  3. 懒汉式优点:只有调用方法才创建对象,不会占用内存
  4. 缺点:在多线程模式下不安全

当然下面的例子会提供懒汉式的线、线程安全问题的解决办法,让我们一起来学习吧。

1、饿汉式单例模式实例

  1. /**
  2. * 线程安全的饿汉式单利模式,虽然线程安全,但是没有调用方法前就被加载,会占用内存
  3. * 所以懒汉式和饿汉式都有优缺点
  4. * 饿汉式优点: 在多线程模式下是安全的
  5. * 缺点: 没有调用方法前就被加载,会占用内存
  6. * 懒汉式优点:只有调用方法才创建对象,不会占用内存
  7. * 缺点:在多线程模式下不安全(这个要解决)
  8. * @author suibibk.com
  9. */
  10. public class FirstSingleton{
  11. private static FirstSingleton singleton = new FirstSingleton();
  12. private FirstSingleton() {
  13. System.out.println("创建实例成功");
  14. }
  15. public static FirstSingleton getInstance() {
  16. System.out.println("调用单例模式的方法");
  17. return singleton;
  18. }
  19. public static void main(String[] args) {
  20. FirstSingleton.getInstance();
  21. }
  22. }

运行上面例子,会发现先打印的是:创建实例成功,后打印的是:调用单例模式的方法,表明 没有调用方法前就被加载。如果我们不考虑内存占用,用这种模式是最好的,但是如果要考虑内存占用,就只能够用懒汉式啦。

2、懒汉式-简单懒汉式

  1. /**
  2. * 懒汉式单例模式,第一种:这种只是简单的符合饿汉式单例模式,在高并发的情况下线程不安全
  3. * @author suibibk.com
  4. */
  5. public class SecondSingleton{
  6. private static SecondSingleton singleton = null;
  7. private SecondSingleton() {
  8. System.out.println("创建实例成功");
  9. }
  10. public static SecondSingleton getInstance() {
  11. System.out.println("调用单例模式的方法");
  12. if(singleton==null) {
  13. singleton = new SecondSingleton();
  14. }
  15. return singleton;
  16. }
  17. public static void main(String[] args) {
  18. SecondSingleton.getInstance();
  19. }
  20. }

运行例子会先打印:调用单例模式的方法,后打印创建实例成功,但是这种会有线程安全问题。

3、懒汉式-同步方法懒汉式

  1. /**
  2. * 懒汉式单例模式,在getInstance方法上加锁,解决线程安全问题
  3. * synchronized关键字锁住的是这个对象,这样的用法,在性能上会有所下降,
  4. * 因为每次调用getInstance(),都要对对象上锁,事实上,
  5. * 只有在第一次创建对象的时候需要加上锁,之后就不需要了,所以这个地方需要改进。
  6. * @author suibibk.com
  7. *
  8. */
  9. public class SecondSingleton2{
  10. private static SecondSingleton2 singleton = null;
  11. private SecondSingleton2() {
  12. System.out.println("创建实例成功");
  13. }
  14. public static synchronized SecondSingleton2 getInstance() {
  15. System.out.println("调用单例模式的方法");
  16. if(singleton==null) {
  17. singleton = new SecondSingleton2();
  18. }
  19. return singleton;
  20. }
  21. public static void main(String[] args) {
  22. SecondSingleton2.getInstance();
  23. }
  24. }

运行例子会先打印:调用单例模式的方法,后打印创建实例成功,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加上锁,之后就不需要了,所以这个地方需要改进。

4、懒汉式-同步双重检验机制模式

  1. /**
  2. * 懒汉式单例模式,对加锁后的改进方法1,用双重检验机制,提高效率;
  3. * 但是在Java指令中创建对象和赋值操作是分开进行的,也就是说singleton = new SecondSingleton3();
  4. * 语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,
  5. * 然后直接赋值给singleton成功,然后再去初始化这个SecondSingleton3实例。这样就有可能出错了
  6. * @author suibibk.com
  7. */
  8. public class SecondSingleton3{
  9. private static SecondSingleton3 singleton = null;
  10. private SecondSingleton3() {
  11. System.out.println("创建实例成功");
  12. }
  13. public static SecondSingleton3 getInstance() {
  14. System.out.println("调用单例模式的方法");
  15. if(singleton==null) {
  16. synchronized(SecondSingleton3.class) {
  17. if(singleton==null) {
  18. singleton = new SecondSingleton3();
  19. }
  20. }
  21. }
  22. return singleton;
  23. }
  24. public static void main(String[] args) {
  25. SecondSingleton3.getInstance();
  26. }
  27. }

运行例子会先打印:调用单例模式的方法,后打印创建实例成功,一般来说,做到上面这一步,基本上都会认为已经完美解决了问题,将synchronized关键字加在了内部也就说当调用的时候是不需要加锁的,只有在singleton为null,并创建对象的时候才需要加锁,并且用双重检查机制,性能有一定的提升。

但是,这样的情况,可能还是有问题的,看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,也就是说singleton = new SecondSingleton3();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的SecondSingleton3实例分配空间,然后直接赋值给singleton成功,然后再去初始化这个SecondSingleton3实例。这样就有可能出错了,我们以A,B两个线程为例:

  1. AB线程同时进入了第一个if判断;
  2. A首先进入synchronized块,由于singletonnull,所以它执行new SecondSingleton3();
  3. 由于JVM的内部优化机制,JVM先画出了一些分配给SecondSingleton3实例的空白内存,并复赋值给singleton成员(此时,JVM并没有初始化这个实例),然后A离开了synchronized块。
  4. B进入synchronized块,由于singleton此时不是null,因此它马上离开synchronized块并将结果返回给调用程序。
  5. 此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。(此时A还在初始化对象)

所以程序还是有可能发生错误的,其实程序在运行过程很复杂,从这点我们可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。我们应该对程序做进一步优化。

5、懒汉式-内部类模式

  1. /**
  2. * 懒汉式单例模式,懒汉式单例模式最终解决方案
  3. * 使用内部类来维护单例的实现,JVM内部机制能够保证一个类在被加载的时候,这个类的加载过程是线程互斥的。
  4. * @author suibibk.com
  5. */
  6. public class SecondSingleton4{
  7. private SecondSingleton4() {
  8. System.out.println("创建实例成功");
  9. }
  10. private static class SingleFactory{
  11. private static SecondSingleton4 instance = new SecondSingleton4();
  12. }
  13. public static SecondSingleton4 getInstance() {
  14. System.out.println("调用单例模式的方法");
  15. return SingleFactory.instance;
  16. }
  17. public static void main(String[] args) {
  18. SecondSingleton4.getInstance();
  19. }
  20. }

运行例子会先打印:调用单例模式的方法,后打印创建实例成功,单例模式使用内部类来维护单例的实现,JVM内部机制能够保证一个类在被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次,并且会保证把赋值给instance内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能的问题。这样我们暂时总结了一个完美的单例模式。
其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。

总结

任何技术都要考虑到方方面面,我们这其实只需要做到双重检查哪个机制就可以了。

 271

啊!这个可能是世界上最丑的留言输入框功能~


当然,也是最丑的留言列表

有疑问发邮件到 : suibibk@qq.com 侵权立删
Copyright : 个人随笔   备案号 : 粤ICP备18099399号-2