一、作用以及优点
单例模式能够保证某一类型对象在系统中的唯一性,即某类在系统中只有一个实例。
- 在内存中只有一个对象,节省内存空间;
- 避免频繁的创建销毁对象,可以提高性能;
- 避免对共享资源的多重占用,简化访问;
- 为整个系统提供一个全局访问点。
二、特点及使用场景
特点
1. 私有构造方法。
2. 内部创建静态对象。
3. 提供静态方法返回该静态对象
使用场景
- 有状态的工具类对象;
- 频繁访问数据库或文件的对象;
三、分类
单例模式分为:饿汉式和懒汉式
饿汉式:在类加载的时候就已经创建该单例实例。
懒汉式:在需要用到的时候才进行创建单例实例。
四、饿汉式
预先声明Singleton对象,这样带来的一个缺点就是:如果构造的单例很大,构造完又迟迟不使用,会导致资源浪费。这种方式是线程安全的。
五、懒汉式
5.1 懒汉式1-线程不安全的
在多线程的情况下是不安全的,假设有两个线程A、B,A线程执行进入到if块里,此时还没调用 instance = new Singleton();假设此时CPU分配给A线程的时间片刚好用完,A线程阻塞。
B线程调用getInstance方法,因为此时A线程还没创建Singleton()对象,所以B判断instance==null 为真,可以进入if块,然后调用new Singleton()创建对象1。
A线程被CPU分配资源,A线程继续执行(此时A线程已经在if块里),调用new Singleton() 创建对象2。
5.2 懒汉式2-synchronize方法
为了解决线程安全问题,可以用加锁的方式来实现互斥,从而保证单例的实现。
缺点:锁的粒度过大,每次调用getInstance()方法是都会被加锁,而我们只需要在第一次调用getInstance()的时候加锁就可以了,因为在第一次获取的时候才需要创建对象,之后只需直接返回即可。这显然影响了程序的性能。
5.3 懒汉式3-双检锁
既解决了线程安全问题,又不至于每次调用getInstance()都会加锁导致降低性能。
synchronized里在进行判断检验instance是否为空的原因是:原因跟懒汉式1的一样,两个线程都进入到第一个 if 块后,虽然加了同步块,假如在同步块里面不进行二次检验,线程A创建完单例对象后,释放同步锁;B线程进入同步锁,会再次创建一个单例对象。
该方法还是存在一个问题,就是instance = new Singleton()这一步可能会出错,由于这一步并不是一个原子操作。它分为以下三个步骤:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:使 instance 指向刚分配的内存地址
由于该步骤不是原子操作,所以可能会出现指令重排序问题,可能会出现以下情况:
memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:使instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
假如有两个线程 A,B,A线程进入到同步块里执行 new Singleton(),刚好出现了指令重排序问题,情景如上。当线程A执行到 instance = memory;,此时刚好线程A的时间片用完,线程A阻塞。
此时线程B,调用getInstance()方法,由于线程A中 instance 引用已经指向了一个内存,只是它还未初始化(仅仅只是一个内存对象),所以此时的 if( instance == null) 判断不成立,instance 此时不为空,不进入if 语句块直接返回这个内存对象,但是此时这个对象还没初始化,然后直接使用该对象,从而会导致错误。
解决办法:
给 instance 变量加上 volatile 关键字修饰。
volatile 的作用:
①当一个线程在工作内存对volatile修饰的变量做修改时,该变量立即会被刷新到主存中,从而使得所有线程对该变量的修改可见。
②禁止指令重排序
完善后的代码:
public class Singleton3 {
private static volatile Singleton3 singleton3 = null;
private Singleton3(){
}
public static Singleton3 newInstance(){
if (singleton3 == null){
synchronized(Singleton3.class){
if (singleton3 == null){
return new Singleton3();
}
}
}
return singleton3;
}
}
5.4 懒汉式4-内部类
避免上面的资源浪费、线程安全、代码简单。因为java机制规定,内部类SingletonHolder只有在getInstance()方法第一次调用的时候才会被加载(实现了lazy),而且其加载过程是线程安全的(实现线程安全)。内部类加载的时候实例化一次instance。以下说明一下为什么加载过程是安全的(涉及类加载过程,有兴趣的可以去了解该方面的知识)
首先默认加载类时会调用 Classloader 类加载器的 loadClass() 方法
继续看到 loadClass(name, false) 方法
由此可以知道,该加载方式是线程安全的。(这是 JDK1.8 的源码,1.8之前的synchronized 是加在 loadClass 方法上的)
5.5 懒加载5-枚举法
单例的枚举实现在《Effective Java》中有提到,因为其功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化等优点,单元素的枚举类型被作者认为是实现Singleton的最佳方法。