关注【程序猿电报】微信公众号,回复【设计模式】可获取全套设计模式资料
什么是单例模式?
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式应用场景
1、在应用场景中,某类只要求生成一个对象的时候,如一个班中的班长、每个人的身份证号等。
2、当对象需要被共享的场合。由于单例模式只允许创建一个对象,共享该对象可以节省内存,并加快对象访问速度。如 Web 中的配置对象、数据库的连接池等。
3、当某类需要频繁实例化,而创建的对象又频繁被销毁的时候,如多线程的线程池、网络连接池等。
如何实现单例模式?
实现单例模式有以下几种方式:
-
饿汉模式
-
懒汉模式(线程不安全)
-
懒汉模式(线程安全)
-
双重检查模式(DCL)
-
静态内部类单例模式
-
枚举类单例模式
-
使用容器实现单例模式
-
CAS实现单例模式
下面使用Java代码做示例,实现这8种方式:
1、饿汉模式
public class Singleton {
private final static Singleton INSTANCE = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
这种方式在类加载时就完成了实例化,会影响类的加载速度,但获取对象的速度快。 这种方式基于类加载机制保证实例仅有一个,避免了多线程的同步问题,是线程安全的。
2、懒汉模式(线程不安全)
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
这种写法起到了懒加载的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了空值的判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
3、懒汉模式(线程安全)
针对线程不安全的懒汉模式,对其中的获取单例对象的方法增加同步关键字。代码如下:
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
这种写法保证了线程安全,但是每次调用getInstance()方法获取对象时都需要进行同步等待,造成不必要的同步开销,实际上除了第一次实例化时需要同步,其他时候都是不需要同步的。
4、双重检查模式(DCL)
根据懒汉模式中,只有在第一次实例化是需要同步,优化代码如下:
public class Singleton {
private static volatile Singleton singleton; // 1
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) { // 2
synchronized (Singleton.class) { // 3
if (singleton == null) { // 4
singleton = new Singleton(); // 5
}
}
}
return singleton;
}
}
双重检查模式代码,需要注意代码注释中的5点:
1.声明单例对象时加上volatile关键字,保证多线程的内存可见性,也即当在一个线程中单例对象实例化完成之后,其他线程也同时能够看到。
2.第一次检查单例对象是否为空,未空则还未完成实例化,进行下一步实例化的操作。
3.如果第一次检查发现单例对象为空,那么该线程就要对此单例类进行加锁,准备进行实例化,加锁是为了保证该线程进行实例化的时候没有其他线程同时进行实例化。
4.第二次检查单例对象是否为空,则是为了避免这种情况:此时单例对象为空,两个线程,A线程在第2步,B线程在第5步,A线程发现单例对象为空,紧接着B线程就完成了实例化,然后就会导致A线程又会走一次第5步的实例化过程,即重复实例化。那么加上了第二次检查后,当A线程到第4步的时候就会发现单例对象已经实例化完成,自然不会到第5步。
5.进行实例化操作,且只发生一次。
5、静态内部类单例模式
public class Singleton {
private Singleton() {
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
}
利用类中静态变量唯一性, JVM 本身的机制保证数据线程安全,没有使用 synchronized 效率高,SingletonHolder 是 private 的外部类无法访问。
6、枚举类单例模式
public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}
我们可以通过如下方式为这个单例填充属性:
public enum Singleton {
INSTANCE("name", 18);
private String name;
private int age;
Singleton(String name, int age) {
this.name = name;
this.age = age;
}
public void whateverMethod() {
}
}
默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。
7、使用容器实现单例模式
public class SingletonManager {
private static Map<String, Object> objMap = new HashMap<String,Object>();
private Singleton() {
}
public static void registerService(String key, Objectinstance) {
if (!objMap.containsKey(key) ) {
objMap.put(key, instance) ;
}
}
public static ObjectgetService(String key) {
return objMap.get(key) ;
}
}
在程序的初始化,将多个单例类型注入到一个统一管理的类中,使用时通过key来获取对应类型的对象,这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行操作。这种方式是利用了Map的key唯一性来保证单例。
8、CAS实现单例模式
以上单例模式的实现主要用到了两点来保证单例,一是JVM的类加载机制,另一个就是加锁。除此之外,也有不加锁的线程安全的单例实现那就是使用CAS。CAS是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。代码如下:
public class Singleton {
private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
private Singleton() {}
public static Singleton getInstance() {
for (;;) {
Singleton singleton = INSTANCE.get();
if (null != singleton) {
return singleton;
}
singleton = new Singleton();
if (INSTANCE.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。
关注【程序猿电报】微信公众号,回复【设计模式】可获取全套设计模式资料