前言:
此时姐妹们对“单例模式的特点”大约比较着重,咱们都需要剖析一些“单例模式的特点”的相关内容。那么小编同时在网上网罗了一些关于“单例模式的特点””的相关内容,希望同学们能喜欢,我们快快来了解一下吧!单例模式(Singleton Pattern)是Java中最简单的设计模式之一,属于创建型模式。它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建,排除线程不安全的风险。这个类提供了一种访问其唯一的对象的方式。
相关阅读:
Spring的设计模式快速入门干货
快速理解设计模式之创建型模式
懒汉式,线程安全
这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。 getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }饿汉式
这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。缺点:类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }
笔者最喜欢用的方式,源码里使用这种形式也很多
静态内部类
这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。 这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }枚举
这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。 不能通过 reflection attack 来调用私有构造方法。
public enum Singleton { INSTANCE; public void whateverMethod() { } }双重校验锁
这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }双重检测的FAQ1. 为何要同步?
多线程情况下,若是A线程调用getInstance,发现instance为null,那么它会开始创建实例,如果此时CPU发生时间片切换,线程B开始执行,调用getInstance,发现instance也null(因为A并没有创建对象),然后B创建对象,然后切换到A,A因为已经检测过了,不会再检测了,A也会去创建对象,两个对象,单例失败。因此要同步。
2. 同步为何不用public synchronized static SingletonClass getInstance(),也就是说为何不同步这个方法,而要同步下面的语句?
因为synchronized修饰的同步块的运行要比一般的代码段慢,如果经常调用getInstance,那么性能问题就得考虑了。
有关synchronized的更详细讲解,请看详解Java多线程锁之synchronized
3. 最外层为何要有if (instance == null)判断?
减少同步代码块的运行次数,从而减少不必要的加锁操作。
4. instance为何要有volatile修饰?
结论:volatile能够防止指令的重排序。
有关volatile的更多知识,请看Java多线程的可见性与有序性
JVM实现可以自由的进行编译器优化。而我们创建变量的步骤:
申请一块内存,调用构造方法进行初始化。分配一个指针指向这块内存。
而这两个操作,JVM并没有规定谁在前谁在后,那么就存在这种情况:线程A开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照我们上面所说的内存模型,A已经把instance指向了那块内存,只是还没有调用构造方法,因此B检测到instance不为null,于是直接把instance返回了——问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果B在A将instance构造完成之前就是用了这个实例,程序就会出现错误了。
在JDK 5之后,Java使用了新的内存模型。volatile关键字有了明确的语义——在JDK1.5之前,volatile是个关键字,但是并没有明确的规定其用途——被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因此,只要我们简单的把instance加上volatile关键字就可以了。
标签: #单例模式的特点 #单例 双锁 #双锁单例 volatile #单例加锁