单例模式 创建型 设计模式(六)详解程序员

 
单例模式 Singleton
单例就是单一实例, only you  只有一个

意图

保证一个类仅有一个实例,并且提供一个访问他的全局访问点
单例模式的含义简单至极,复杂的是如何能够保障你真的只是创建了一个实例
 
怎样才能保证一个类只有一个实例,并且这个实例对象还易于被访问?
可以借助于全局变量,但是类就在那里,你不能防止实例化多个对象,可能一不小心谁就创建了一个对象
 
所以通常的做法是让类自身负责保存他的唯一实例,通过构造方法私有阻止外部实例对象,并且提供静态公共方法 
所以常说的单例模式有下面三个特点
  • 单例模式的类,只能有一个实例对象
  • 单例模式的类,自身创建自己唯一的实例对象
  • 单例模式的类,必须提供获取这一唯一实例的方式

结构

单例模式 创建型 设计模式(六)详解程序员
Singleton模式的结构简单,实现的步骤一般是:
自身创建并且保存维护这个唯一实例,并且这个唯一实例singleton  是私有的
将构造方法设置为私有,防止创建实例
设置公共的getInstance()方法获取实例,而且,这个方法必然是静态的
 
单例类自身负责创建维护唯一实例,按照实例对象创建的时机,分为两类 
  • 饿汉式:实例在类加载时创建
  • 懒汉式:实例在第一次使用时创建

饿汉式

package singleton; 
/** 
* Created by noteless on 2018/10/11. 
* Description: 
*/ 
public class EagerSingleton { 
private EagerSingleton() { 
} 
private static final EagerSingleton singleton = new EagerSingleton(); 
public static EagerSingleton getInstance() { 
return singleton; 
} 
}

当类加载时,静态成员singleton 会被初始化,对象在此时被创建
饿汉式的缺点很明显:
如果初始化的太早,可能就会造成资源浪费。
在虚拟机相关的文章中,有介绍过,虚拟机的实现会保证:类加载会确保类和对象的初始化方法在多线程场景下能够正确的同步加锁
所以,饿汉式不必担心同步问题
如果对于该对象的使用也是“饿汉式”的,也就是应用程序总是会高频使用,应该优先考虑这种模式 

懒汉式

package singleton; 
/** 
* Created by noteless on 2018/10/11. 
* Description: 
*/ 
public class LazySingleton { 
private LazySingleton() { 
} 
private static LazySingleton singleton = null; 
public static LazySingleton getInstance() { 
if (singleton == null) { 
singleton = new LazySingleton(); 
} 
return singleton; 
} 
}

一个简单的懒汉式实现方式如上
静态singleton 初始为null 
每次通过getInstance()获取时,如果为null,那么创建一个实例,否则就直接返回已存在的实例singleton
同步问题
上述代码在单线程下是没有问题的,但是在多线程场景下,需要同步
假如两个线程都执行到if (singleton == null) ,都判断为空
那么接下来两个线程都会创建对象,就无法保证唯一实例
 
所以可以给方法加上synchronized关键字,变为同步方法
public synchronized static LazySingleton getInstance() { 
if (singleton == null) { 
singleton = new LazySingleton(); 
} 
return singleton; 
}

如果内部逻辑不像上面这般简单,可以根据实际情况使用同步代码块的形式,比如
public static LazySingleton getInstance() { 
synchronized (LazySingleton.class) { 
if (singleton == null) { 
singleton = new LazySingleton(); 
  } 
} 
return singleton; 
}

同步的效率问题
多线程并发场景,并不是必然出现的,只是在第一次创建实例对象时才会出现,概率非常小  
但是使用同步方法或者同步代码块,则会百分百的进行同步
同步就意味着也就是如果多个线程执行到同一地方,其余线程将会等待 
这样虽然可以防止创建多个实例,但是有明显的效率问题 
 
既然同步问题是小概率的,那么就可以尝试降低同步的概率
package singleton; 
/** 
* Created by noteless on 2018/10/11. 
* Description: 
*/ 
public class LazySingleton { 
private LazySingleton() { 
} 
private static LazySingleton singleton = null; 
public static LazySingleton getInstance() { 
if (singleton == null) { 
synchronized (LazySingleton.class) { 
if (singleton == null) { 
singleton = new LazySingleton(); 
} 
} 
} 
return singleton; 
} 
}

上面的方式被称为 双重检查
如果singleton不为空,那么直接返回唯一实例,不会进行同步
如果singleton为空,那么涉及到对象的创建,此时,才会需要同步
只会有一个线程进入同步代码块
他会校验是否的确为null,然后进行实例对象的创建
既解决了同步问题,又没有严重的效率问题
原子操作问题
计算机中不会因为线程调度被打断的操作,也就是不可分割的操作,被称作原子操作 
可以理解为计算机对指令的执行的最小单位
比如 i=1;这就是一个原子操作,要么1被赋值给变量i,要么没有
但是如果是int i = 1;这就不是一个原子操作
他至少需要先创建变量i 然后在进行赋值运算
 
我们实例创建语句,就不是一个原子操作
singleton = new LazySingleton();
他可能需要下面三个步骤

  • 分配对象需要的内存空间
  • 将singleton指向分配的内存空间
  • 调用构造函数来初始化对象
计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整 
也就是上面三个步骤的顺序是不能够保证唯一的
如果先分配对象需要的内存,然后将singleton指向分配的内存空间,最后调用构造方法初始化的话 
 
假如当singleton指向分配的内存空间后,此时被另外线程抢占(由于不是原子操作所以可能被中间抢占)
线程二此时执行到第一个if (singleton == null)
此时不为空,那么不需要等待线程1结束,直接返回singleton了
显然,此时的singleton都还没有完全初始化,就被拿出去使用了
根本问题就在于写操作未结束,就进行了读操作
可以给 singleton 的声明加上volatile关键字,来解决这些问题

可以保障在完成写操作之前,不会调用读操作
 
完整代码如下
package singleton; 
/** 
* Created by noteless on 2018/10/11. 
* Description: 
*/ 
public class LazySingleton { 
    private LazySingleton() { 
    } 
    private static volatile LazySingleton singleton = null; 
        public static LazySingleton getInstance() { 
        if (singleton == null) { 
            synchronized (LazySingleton.class) { 
                if (singleton == null) { 
                singleton = new LazySingleton(); 
                } 
            } 
        } 
    return singleton; 
    } 
}

内部类的懒汉式

上面的这段代码,可以在实际项目中直接使用
但是,双重检查不免看起来有些啰嗦
还有其他的实现方式
内部类是延时加载的,也就是说只会在第一次使用时加载

内部类不使用就不加载的特性,非常适合做单例模式
package singleton; 
  
/** 
* Created by noteless on 2018/10/11. 
* Description: 
* @author 
*/ 
public class Singleton { 
    private Singleton() { 
    } 
    private static class SingletonHolder { 
        private static final Singleton INSTANCE = new Singleton(); 
    } 
    public static Singleton getInstance() { 
        return SingletonHolder.INSTANCE; 
    } 
}

SingletonHolder作为静态内部类,内部持有一个Singleton实例,采用“饿汉式”创建加载
不过内部类在使用时才会被加载
私有的静态内部类,只有在getInstance被调用的时候,才会加载
此时才会创建实例,所以,从整体效果看是懒汉式
不使用不会加载,节省资源开销,也不需要复杂的代码逻辑 
依靠类的初始化保障线程安全问题,依靠内部类特性实现懒加载

枚举单例

《Effective Java》中提到过枚举针对于单例的应用
单例模式 创建型 设计模式(六)详解程序员

使用场景

是否只是需要一个实例,是由业务逻辑决定的
有一些对象本质业务逻辑上就只是需要一个 
比如线程池,windows的任务管理器,计算机的注册表管理器等等
计算机中只需要一个任务管理器,不需要也没必要分开成多个,一个任务管理器管理所有任务简单方便高效
如果qq一个任务管理器idea一个任务管理器,你受得了么
所以说,是否需要单例模式,完全根据你的业务场景决定
比如,如果当你需要一个全局的实例变量时,单例模式或许就是一种很好的解决方案

总结

由于单例模式在内存中只有一个实例,减少了内存开支和系统的性能开销
单例模式与单一职责模式有冲突
承担了实例的创建和逻辑功能提供两种职责
单例模式中没有抽象层,所以单例类的扩展比较困难
单例模式的选用跟业务逻辑息息相关,比如系统只需要一个实例对象时,就可以考虑使用单例模式 
单例模式的重点在于单例的唯一性的保障实现
可以直接复制上面的代码使用
 
单例模式向多个实例的扩展
单例模式的意图是“保证一个类仅有一个实例,并且提供一个访问他的全局访问点” 
单例模式的根本逻辑就是限制实例个数,并且个数限制为1

 

所以,可以仍旧限制实例个数,并且将限制个数设置为大于等于1
这种单例模式的扩展,又被称之为多例模式

  • 多例模式下可以创建多个实例
  • 多例模式自己创建、管理自己的实例,并向外界提供访问方式获取实例
多例模式其实就是单例模式的自然扩展,同单例模式一样,也肯定需要构造方法私有,多例类自己维护等,唯一不同就是实例个数扩展为多
 

自定义类加载器时的问题
在虚拟机相关的介绍中有详细介绍了类加载机制与命名空间以及类加载机制的安全性问题
不同的类加载器维护了各自的命名空间,他们是相互隔离的
不同的类加载器可能会加载同一个类
如果这种事情发生在单例模式上,系统中就可能存在不止一个实例对象
尽管在不同的命名空间中是隔离的
但是在整个应用中就是不止一个,所以如果你自定义了类加载器
你就需要小心,你可以指定同样的类加载器以避免这个问题
如果没有自定义类加载器则不需要关心这个问题
自定义的类都会使用内置的  应用程序   类加载器进行加载
 

原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/6543.html

(0)
上一篇 2021年7月16日
下一篇 2021年7月16日

相关推荐

发表回复

登录后才能评论