Java设计模式学习记录-单例模式

时间:2022-05-04 15:46:17

前言

已经介绍和学习了两个创建型模式了,今天来学习一下另一个非常常见的创建型模式,单例模式。

单例模式也被称为单件模式(或单体模式),主要作用是控制某个类型的实例数量是一个,而且只有一个。

单例模式

单例模式的实现方式

实现单例模式的方式有很多种,大体上可以划分为如下两种。

外部方式

在使用某些全局对象时,做一些“try-Use”的工作。就是如果要使用的这个全局对象不存在,就自己创建一个,把它放到全局的位置上;如果本来就有,则直接拿来使用。

内部实现方式

类型自己控制正常实例的数量,无论客户程序是否尝试过了,类型自己自己控制只提供一个实例,客户程序使用的都是这个现成的唯一实例。

目前随着集群、多核技术的普遍应用,想通过简单的类型内部控制失效真正的Singleton越来越难,试图通过经典单例模式实现分布式环境下的“单例”并不现实。所以目前介绍的这个单例是有语义限制的。

单例模式的特点

虽然单例模式也属于创建型模式,淡水它是有自己独特的特点的。

  • 单例类只有一个实例。
  • 单例类自行创建该实例,在该类内部创建自身的实例对象。
  • 向整个系统公开这个实例接口。

还有需要注意的一点,单例模式只关心类实例的创建问题,并不关心具体的业务功能。

单例模式的范围

目前Java里面实现的单例是一个ClassLoader及其子ClassLoader的范围。因为ClassLoader在装载饿汉式实现的单例类时,会响应地创建一个类的实例。这也说明,如果一个虚拟机里有多个ClassLoader(虽然说ClassLoader遵循双亲委派模型,但是也会有父加载器处理不了,然后自定义的加载器执行类加载的情况。),而且这些ClassLoader都装载着某一个类的话,就算这个类是单例,它也会产生很多个实例。如果一个机器上有多个虚拟机,那么每个虚拟机里面都应该至少有一个这个类的实例,也就是说整个机器上就有很多个实例,更不会是单例了。

还有一点再次强调,目前讨论的单例范围不适用于集群环境。

单例模式的类型

饿汉式单例

饿汉式单例是指在类被加载的时候,唯一实例已经被创建。

如下代码的例子:

/**
* 饿汉式单例模式
*
*/
public class HungrySingleton {
/**
* 定义一个静态变量用来存储实例,在类加载的时候创建,只会创建一次。
*/
private static HungrySingleton hungrySingleton = new HungrySingleton(); /**
* 私有化构造方法,禁止外部创建实例。
*/
private HungrySingleton(){
System.out.println("创建实例");
} /**
* 外部获取唯一实例的方法
* @return
*/
public static HungrySingleton getInstance(){
return hungrySingleton;
} }

懒汉式单例

懒汉式单例是指在类加载的时候不创建单例的对象,只有在第一次使用的时候创建,并且在第一次创建后,以后不再创建该类的实例。

如下代码的例子:

/**
* 懒汉式单例
*/
public class LazySingleton {
/**
* 定义一个静态变量用来存储实例。
*/
private static LazySingleton lazySingleton = null; /**
* 私有化构造方法,禁止外部创建实例。
*/
private LazySingleton(){} /**
* 外部获取唯一实例的方法
* 当发现没有初始化的时候,才初始化静态变量。
* @return
*/
public static LazySingleton getInstance(){
if(null==lazySingleton){
lazySingleton = new LazySingleton();
}
return lazySingleton;
} }

登记式单例

登记式单例实际上维护的是一组单例类的实例,将这些实例存在在一个登记薄(例如Map)中,使用已经登记过的实例,直接从登记簿上返回,没有登记的,则先登记,后返回。

如下代码例子:

/**
* 登记式单例
*/
public class RegisterSingleton { /**
* 创建一个登记簿,用来存放所有单例对象
*/
private static Map<String,RegisterSingleton> registerBook = new HashMap<>(); /**
* 私有化构造方法,禁止外部创建实例
*/
private RegisterSingleton(){} /**
* 注册实例
* @param name 登记簿上的名字
* @param registerSingleton 登记簿上的实例
*/
public static void registerInstance(String name,RegisterSingleton registerSingleton){ if(!registerBook.containsKey(name)){
registerBook.put(name,registerSingleton);
}
} /**
* 获取实例,如果在未注册时调用将返回null
* @param name 登记簿上的名字
* @return
*/
public static RegisterSingleton getInstance(String name){
return registerBook.get(name);
}
}

由于饿汉式的单例在类加载的时候就创建了一个实例,所以这个实例一直都不会变,因此也是线程安全的。但是懒汉式单例就不是线程安全的了,在懒汉式单例中有可能会出现两个线程创建了两个不同的实例,因为懒汉式单例中的getInstance()方法不是线程安全的。所以如果想让懒汉式变成线程安全的,需要在getInstance()方法中加锁。

如下所示:

   /**
* 外部获取唯一实例的方法
* 当发现没有被初始化的时候,才初始化静态变量
* @return
*/
public static synchronized LazySingleton getInstance(){
if(null==lazySingleton){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}

但是这样增加的资源消耗,延迟加载的效果虽然达到了,但是在使用的时候资源消耗确更大了,所以不建议这样用。既要实现线程安全,又要保证延迟加载。基于这样的问题就出现了另一种方式的单例模式,静态内部类式单例

静态内部类式单例

静态内部类式单例饿汉式和懒汉式的结合。

如下代码例子:

/**
* 内部静态类式单例
*/
public class StaticClassSingleton { /**
* 私有化构造方法,禁止外部创建实例。
*/
private StaticClassSingleton(){
System.out.println("创建实例了");
} /**
* 私有静态内部类,只能通过内部调用。
*/
private static class SingleClass{
private static StaticClassSingleton singleton = new StaticClassSingleton();
} /**
* 外部获取唯一实例的方法
* @return
*/
public static StaticClassSingleton getInstance(){
return SingleClass.singleton;
} }

双重检查加锁式单例

上面静态内部类的方式通过结合饿汉式和懒汉式来实现了即延迟加载了又线程安全了。下面也来介绍另一种即实现了延迟加载有保证了线程安全的方式的单例。

如下代码例子:

/**
* 双重检查加锁式单例
*/
public class DoubleCheckLockSingleton { /**
* 静态变量,用来存放实例。
*/
private volatile static DoubleCheckLockSingleton doubleCheckLockSingleton = null; /**
* 私有化构造方法,禁止外部创建实例。
*/
private DoubleCheckLockSingleton(){} /**
* 双重检查加锁的方式保证线程安全又能获得到唯一实例
* @return
*/
public static DoubleCheckLockSingleton getInstance(){
//先检查实例是否已经存在,不存在则进入代码块
if(null == doubleCheckLockSingleton){
synchronized (DoubleCheckLockSingleton.class){
//由于synchronized也是重入锁,即一个线程有可能多次进入到此同步块中如果第一次进入时已经创建了实例,那么第二次进入时就不创建了。
if(null==doubleCheckLockSingleton){
doubleCheckLockSingleton = new DoubleCheckLockSingleton();
}
}
} return doubleCheckLockSingleton;
} }

如上所示,所谓“双重检查加锁”机制,并不是每次进入getInstance()方法都需要加锁,而是当进入方法后,先检查实例是否已经存在,如果不存在才进行下面的同步块,这是第一重检查,进入同步块后,再次检查实例是否已经存在,如果不存在,就在同步块中创建一个实例,这是第二重检查。这个过程是只需要同步一次的。

还需要注意的一点是,在使用“双重检查加锁”时,需要在变量上使用关键字volatile,这个关键字的作用是,被volatile修饰的变量的值不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确地处理该变量。可能不了解Java内存模式的朋友不太好理解这句话的意思,可以去看看(JVM学习记录-Java内存模型(一)JVM学习记录-Java内存模型(二))了解一下Java内存模型,我简单说明一下,volatile这个关键字可以保证每个线程操作的变量都会被其他线程所看到,就是说如果第一个线程已经创建了实例,但是把创建的这个实例只放在了自己的这个线程中,其他线程是看不到的,这个时候如果其他线程再去判断实例是否已经存在了实例的时候,发现没有还是没有实例就会又创建了一个实例,然后也放在了自己的线程中,如果这样的话我们写的单例模式就没意义了。在JDK1.5以前的版本中对volatile的支持存在问题,可能会导致“双重检查加锁”失败,所以如果要使用“双重检查加锁”式单例,只能使用JDK1.5以上的版本。

枚举式单例

在JDK1.5中引入了一个新的特性,枚举,通过枚举来实现单例,在目前看来是最佳的方法了。Java的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法。

还是通过代码示例来解释吧。

如下代码例子:

/**
* 单元素枚举实现单例模式
*/
public enum EnumSingleton { /**
* 必须是单元素,因为一个元素就是一个实例。
*/
INSTANCE; /**
* 测试方法1
* @return
*/
public void doSomeThing() { System.out.println("#####测试方法######");
} /**
* 测试方法2
* @return
*/
public String getSomeThing(){
return "获得到了一些内容";
}
}

上面例子中EnumSingleton.INSTANCE就可以获得到想要的实例了,调用单例的方法可以种EnumSingleotn.INSTANCE.doSomeThing()等方法。

下面来看看枚举是如何保证单例的:

首先枚举的构造方法明确是私有的,在使用枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,表明枚举实例只能被赋值一次,这样在类初始化的时候就会把实例创建出来,这也说明了枚举单例,其实是饿汉式单例方式。这样就用最简单的代码既保证了线程安全,又保证了代码的简洁。

还有一点很值得注意的是,枚举实现的单例保证了序列化后的单例安全。除了枚举式的单例,其他方式的单例,都可能会通过反射或反序列化来创建多个实例。

所以在使用单例的时候最好的办法就是用枚举的方式。既简洁又安全。