所谓单例模式:保证一个类仅有一个实例 并提供一个访问它的全局访问点。
简单的说:单例设计模式就是一个类有且仅有一个对象。
通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的方法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。
单例模式分为:1.饿汉式 2.懒汉式
饿汉式:在类加载的时候,直接将唯一的对象创建出来。
懒汉式:在类加载的时候,并不直接创建这个唯一的对象。而是在调用方法时,去判断是否创建过对象。这里分为两种情况:1.如果是已经创建过对象,那么直接返回。2.如果并没有创建一个对象,则创建一个对象 将其地址值返回。
饿汉式:
1.将构造方法私有化,使其不能在类的外部通过new关键字来创建出该类对象。
2.在类的内部产生一个唯一的实例化对象,并且将其封装成private static类型。
3.通过一个静态方法返回这个唯一对象
饿汉式代码如下:
public class Singleton {
// 将自身实例化对象设置为一个属性,并用static、final修饰
private static final Singleton instance = new Singleton();
// 构造方法私有化
private Singleton() {}
// 静态方法返回该实例
public static Singleton getInstance() {
return instance;
}
}
饿汉式的优点:实现起来简单,没有多线程同步问题。
饿汉式的缺点:提前占用系统资源。(不管你用不用这个实例化对象,随着类的加载他已经被创建出来了。因此会占用内存)当类被卸载时,静态变量被摧毁,并释放所占用的内存,因而在某些特定情况下会耗费内存。
懒汉式:
懒汉式是延迟加载,在调用get()方法时,实例才被创建。(先不着急实例化对象,等要用的时候才给你创建出来。不着急,所以称为"懒汉式"),常见的实现懒汉式方法就是在get()方法中new一个实例化对象。
懒汉式代码如下:
public class Singleton {
// 将自身实例化对象设置为一个属性,并用static修饰
private static Singleton instance;
// 构造方法私有化
private Singleton() {}
// 静态方法返回该实例
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
在这里 我们会先进行一个判断 如果对象不为null,那么直接返回。如果对象为null,那么使用new关键字创建出这个对象。
懒汉式优点:在第一次调用时才创建对象,节约内存。
懒汉式缺点:在多线程的环境下,这种实现方法根本无法保证单例的状态。
线程安全的懒汉式代码如下:
public class Singleton {
// 将自身实例化对象设置为一个属性,并用static修饰
private static Singleton instance;
// 构造方法私有化
private Singleton() {}
// 静态方法返回该实例,加synchronized关键字实现同步
public static synchronized Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
优点:在多线程的环境下,保证了”懒汉式”的线程安全问题。
缺点:总所周知在多线程环境下,synchronized方法通常效率低。因此这并不是最佳的实现方案。
DCL双检查锁机制(DCL:double checked locking)
public class Singleton {
// 将自身实例化对象设置为一个属性,并用static修饰
private static Singleton instance;
// 构造方法私有化
private Singleton() {}
// 静态方法返回该实例
public static Singleton getInstance() {
// 第一次检查instance是否被实例化出来,如果没有进入if块
if(instance == null) {
synchronized (Singleton.class) {
// 某个线程取得了类锁,实例化对象前第二次检查instance是否已经被实例化出来,如果没有,才最终实例出对象
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
关于为什么要做两次判断的解释: 对于instance存在的情况,就直接返回,这里没有问题。但是当instance为null并且有两个线程同时调用getInstance()方法时,这里它们都能通过第一次 instance == null 判断。然后由于同步代码块 synchronized,这两个线程只能有一个进入,另一个在外排队等待,必须要等第一个进去并出来以后,第二个才能进入。这里如果没有了第二次 instance==null的判断,则第一个线程进入创建了实例,在它出来之后,第二个线程依然可以进入创建实例。这样就没有达到单例的目的。
经过分析,貌似上面缩写的DCL代码是没有问题了。但是,如果我们深入分析,那么还是可能存在一定的问题。
如果我们深入到JVM的层面去探讨DCL这段代码,那么就有可能(仅仅是有可能)会产生问题。
这里从JVM创建对象所经历的步骤开始分析。因为虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创建一个新的对象并非是原子性操作。在有些JVM中上述做法是没有问题的,但是有些情况下是会造成莫名的错误。
JVM创建对象需要经过三步:
1.分配内存
2.初始化构造器
3.将对象指向分配的内存的地址
这种顺序针对上述DCL代码中是完全可以执行的,因为JVM执行了整个流程创建了对象才将内存地址交给了对象。但是,如果2.3调换了顺序那么会有问题吗?答案是肯定的!(2.3可能调换顺序的原因是JVM会针对字节码调优,其中有一项就是调整指令的执行顺序)
如果2.3调换了顺序,那么内存地址首先就会交给对象。针对上面的DCL代码,内存地址会先交给instance 对象,然后再进行初始化构造器。这时,后面的线程去请求getInstance()方法时,会认为instance对象已经是被创建了,然后就会返回一个引用。但是如果在初始化构造器之前,这个线程去使用了instance对象,就会产生莫名的错误。
推荐方案: 静态内部类实现单例模式
静态内部类代码如下:
public class SingLeton {
private static class Inner{
private static final SingLeton instance=new SingLeton();
}
private SingLeton(){
}
private static final SingLeton getInstance(){
return Inner.instance;
}
}
这种静态内部类实现单例模式是可以完全避免上述错误的!一个类的静态属性只会在第一次加载类时初始化。同时,JVM保证在类加载的过程中static代码块在多线程或者单线程下都正确执行,且仅执行一次。在初始化进行一半的时候,别的线程是无法使用的,因为JVM会帮我们强行同步这个过程。另外由于静态变量只初始化一次,所以Singleton仍然是单例的。解决了延迟加载以及线程安全的问题。
关于单例模式的分享到此结束,感谢各位观看,欢迎交流,谢谢!
最后 学友哥镇楼: