单例模式学习笔记

来源:互联网 发布:淘宝电话回访 编辑:程序博客网 时间:2024/06/08 09:49

有一些对象我们只需要一个,比如线程池(threadpool)、缓存(cache)、对话框、注册表(registry)、日志等。这类对象只能有一个实例,如果制造出多个实例,就会产生程序异常、资源使用过量或者结果不一致等问题。
难道不能使用Java的静态变量来达到目的吗?是的,用静态变量有这样一个缺点,如果将对象赋值给一个静态变量,那么你必须在程序一开始就创建好对象,万一对象非常消耗资源,而程序在某次执行中又一直没用到,就形成了浪费。
如果是一个公开类,我们可以多次实例化它;如果不是公开类, 只有同一个包内的类可以实例化它,也依然可以实例化它多次。于是我们想到将类的构造器私有化,这样类内的代码是唯一能调用此构造器的代码:

public class MyClass{    private MyClass(){}}

但是这样做又陷入一个“先有鸡还是先有蛋的问题”:必须拥有MyClass类的实例才能调用MyClass的构造器,但是因为没有其他类能够实例化MyClass类,所以我们得不到这样的实例。即可以在MyClass类的对象上使用MyClass的构造器,但是在此之前必须有一个MyClass实例。
我们继续想到用静态方法去调用私有构造器(构造器本身也属于一种静态方法):

public class MyClass{    private MyClass(){}    public static MyClass getInstance(){        return new MyClass();    }}

于是我们得到单例模式实现的雏形,也就是方法一

public class Singleton{    //利用一个静态变量来记录Singleton类的唯一实例。    private  static Singleton uniqueInstance;    //私有化构造器    private Singleton(){}    //用一个静态方法调用私有构造器获取该类的实例    public static Singleton getInstance(){        if(uniqueInstance == null){            uniqueInstance = new Singleton();        }        return uniqueInstance;    }}

如果我们不需要这个实例,它就永远不会产生,这就是“延迟实例化”(lazy instantiaze)。
我们得到单例模式的定义:确保一个类只有一个实例,并提供一个全局访问点。
在单线程模式下,这种单例模式似乎是完美的,但是当我们考虑到多线程的情况时,由于JVM的多线程的内存模型是:线程具有自己的工作内存,而实例是存放在线程共享的主内存中(Java堆)。如下图:
Java多线程内存模型
如果不进行synchronized关键字等措施的保护,在不同线程中调用上述的静态方法获得,有可能会使uniqueInstance这个静态变量指向不同的Singleton对象。因为两个线程中可能同时进入了getInstance()这个方法,而线程各自的工作内存相互独立,可能在各自的线程中都创建了Singleton的实例。
很自然地我们想到了多线程的下的单例模式的写法,方法二

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

在任何情况下只有一个线程能获得Singleton类的锁进入getInstance()这个方法。但是这个方法有个缺点:只有第一次执行此方法的时候才需要同步,一旦设置好uniqueInstance变量,我们似乎不需要每次都进入synchronized方法,我们知道,synchronized这个方法的代价是十分大的。

让我们继续看一种多线程情况下安全的做法,方法三

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

这种方法属于“急切”(eagerly)创建单例,如果应用程序总是创建并使用单例实例,或者在创建和运行时方面的负担不太繁重,可以选用此方法。这段代码利用JVM在该类类加载时期的初始化这一步骤直接为类变量uniqueInstance创建实例,保证了在任何线程访问uniqueInstance之前,该实例已经创建完毕。

接着我们看利用双重检查加锁(double-checked locking)形成单例的做法,这种做法是对synchronized方法的一种优化,方法四

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

这种方法真正意义上保证了只有在第一次创建实例的时候才会进入synchronized,volatile关键字保证了线程每次使用uniqueInstance时都会获得最新的值(这是JVM的线程机制,volatile关键字保证了变量的可见性和有序性)。这样会大大减少synchronized带来的时间损耗。

另外在网上看到两种方法
利用静态内部类,方法五

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

这种方法可以和“急切法”对比来看,“急切法”利用类加载机制,在Singleton类第一次进入类加载时就激发了初始化步骤,从而保证了在任何线程使用之前就创建好了实例。而这里利用静态内部类却达到了上文提到过的“延迟初始化”的效果——Singleton的类加载并不能激发其静态内部类的初始化这个步骤(也就是private static final Singleton INSTANCE = new Singleton()这句话,其静态内部类确实也进行了类加载的一部分内容),只有显示调用getInstance()方法的时候才会激发其静态内部类的初始化这个步骤。如果实例化Singleton很消耗资源而且不能保证其他地方主动加载Singleton类,那么“急切”实例化Singleton显然不合适。
再看枚举的实现,方法六

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

这种方法利用枚举防止了多次实例化,能避免多线程中的同步问题,自动支持序列化机制,防止反序列化重新创建对象,还有独特的一点是完全避免了反射对类的暴力使用。

末尾,让我们总结一下,单例模式的要点:

  • 单例模式确保程序中一个类最多只有一个实例
  • 单例模式提供这个实例的全局访问点
  • 单例模式需要一个一个静态变量、私有构造器、一个静态方法。
  • 确定在性能呢个和资源上的限制,然后小心选择适当的方案来实现单例。
  • 双锁法在JDK1.5以后才奏效。

单例模式的6种实现方式罗列如下:
1.原始方式
关键字:非线程安全、延迟初始化。

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

2.synchronized方法
关键字:线程安全、延迟初始化、效率低

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

3.“急切”初始化法
关键字:线程安全、类加载时进行初始化、无锁、可能浪费内存

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

4.volatile法
关键字:线程安全、延迟初始化、“双锁”、效率高。

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

5.静态内部类法
关键字:线程安全、延迟初始化、静态内部类、效率高

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

6.枚举法
关键字:线程安全、支持序列化、防止反序列化、阻止反射攻击

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

最后借用别人的经验之谈:
6种方法中,只有第1种方法线程不安全,(抛开第6种方法)只有第3种方法是“急切初始化”。
第2种方法(效率低)是对第1种方法的改进,但这两种都不建议使用。建议使用第3种“急切”初始化法,在有明确要求实现“延迟初始化”的时候,考虑使用第5种方法“静态内部类法”,第4种方法“双锁法”用的很少。如果涉及到反序列化创建对象时,可以考虑使用第6种方法“枚举法”。

参考文章:

  • 《head first java》
  • 《深入理解Java虚拟机》
  • 单例模式总结
1 0
原创粉丝点击