单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
单例模式主要有两种类型:
-
懒汉式:在真正需要使用对象时才去创建该单例类对象
-
饿汉式:在类加载时已经创建好该单例对象,等待被程序使用
除此之外还有登记式等形式针对特定的业务场景,但没有前两种常用。
=========================================================================
1.饿汉式:
饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。我们可以简单认为在程序启动时,这个单例对象就已经创建好了。
饿汉式常见的有两种创建方式,第一种就是通过静态变量创建:
package Singleton;
/**
* @author admin
* @version 1.0.0
* @ClassName EagerSingleton.java
* @Description 单例饿汉模式
* @createTime 2022年02月18日 16:43:00
*/
public class EagerSingleton {
/**
* @Description 定义静态变量 eagerSingleton 的时候实例化单例类 EagerSingleton
* 因此饿汉模式在类被加载时,静态变量 eagerSingleton 就会被初始化,
* 此时类的私有构造函数会被调用,单例类的唯一实例会被创建。
* */
private static final EagerSingleton EAGER_SINGLETON = new EagerSingleton();
/**
* @Description 私有构造接口,防止其他类直接创建对象
*/
private EagerSingleton(){
}
/**
* @Description public接口对外开放
* @return EAGER_SINGLETON
**/
public static EagerSingleton getEagerSingleton() {
return EAGER_SINGLETON;
}
}
饿汉式的单例模式我们可以理解为迫切加载。也就是从一开始,我们的这个单例对象就被加载并创建好了,因为在类加载的时候,静态变量EagerSingleton就会初始化,这个类的构造方法就会被调用。之后的操作都是对这个已经创建的单例对象进行操作。
比懒汉模式,饿汉模式速度更快,但是在资源利用的层面来说,不论我们是否使用这个对象,这个单例对象都会被加载出来,不如懒汉模式节省资源。
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
除了使用私有静态变量,我们也可以通过静态代码块来实现饿汉模式:
package Singleton;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName EagerSingletonStatic.java
* @Description 通过静态块实例化对象来实现饿汉模式
* @createTime 2022年02月22日 11:21:00
*/
public class EagerSingletonStatic {
/**
* @Description 定义静态变量 EAGER_SINGLETON_STATIC
* */
private static final EagerSingletonStatic EAGER_SINGLETON_STATIC;
/*在类中,可以将某一块代码声明为静态的,这样的程序块叫静态初始化段。
静态代码块在main方法之前就被执行,从而实现类被加载时,
静态变量 EAGER_SINGLETON_STATIC 就会被初始化,实现单例模式*/
static{
EAGER_SINGLETON_STATIC = new EagerSingletonStatic();
}
/**
* @Description 私有构造接口,防止其他类直接创建对象
*/
private EagerSingletonStatic(){
}
/**
* @Description public接口对外开放
* @return EAGER_SINGLETON_STATIC
**/
public static EagerSingletonStatic getInstance(){
return EAGER_SINGLETON_STATIC;
}
}
实现原理就在于静态代码块在main方法之前就会被执行,从而实现类被加载时,静态变量 EAGER_SINGLETON_STATIC 就会被初始化。实现单例模式。
测试一下:
package Singleton.test;
import Singleton.EagerSingleton;
import Singleton.EagerSingletonStatic;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName EagerSingleton.java
* @Description 饿汉式单例模式测试类
* @createTime 2022年02月18日 16:52:00
*/
public class EagerSingletonTest {
public static void main(String[] args) {
/*创建两个对象,如果不是单例模式,他们的在内存中的地址是不同的*/
EagerSingleton eagerSingleton1 = EagerSingleton.getEagerSingleton();
EagerSingleton eagerSingleton2 = EagerSingleton.getEagerSingleton();
System.out.println(eagerSingleton1==eagerSingleton2);
/*创建两个对象,如果不是单例模式,他们的在内存中的地址是不同的*/
EagerSingletonStatic eagerSingletonStatic1 = EagerSingletonStatic.getInstance();
EagerSingletonStatic eagerSingletonStatic2 = EagerSingletonStatic.getInstance();
System.out.println(eagerSingletonStatic1==eagerSingletonStatic2);
}
}
=========================================================================
2.懒汉式:
相比较于恶汉式,懒汉式的实现方法更多。懒汉式和饿汉式的核心区别在于懒汉式是在我们需要的时候再去创建这个单例对象,而不是从一开始就加载出来。首先看代码:
package Singleton;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName LazySingleton.java
* @Description 懒汉式单例模式(线程不安全):
* 这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。
* 如果在多线程下,一个线程进入了if (singleton == null)判断语句块,
* 还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。
* 所以在多线程环境下不可使用这种方式。
* @createTime 2022年02月22日 17:11:00
*/
public class LazySingleton {
/**
* @Description: 区别在于不再是final,不会在类加载时初始化
*/
private static LazySingleton lazySingleton;
/**
* @Description: 私有化构造方法,防止其他类直接调用
*/
private LazySingleton(){
}
/**
* @Description: public接口对外开放
* @return lazySingleton
* @author Zeyu Wan
*/
public static LazySingleton getInstance(){
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
从代码我们可以看出,单例对象不再被 final 关键字修饰,表示我们只是在需要对象时,通过getInstance()方法创建并获取单例对象。但是这样的模式只能在单线程的情况下使用。在多线程模式下会出现线程安全问题。在此基础上,我们通过synchronized加锁来保证线程的安全:
改进版代码(加锁):
package Singleton;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName LazySingletonLock.java
* @Description 懒汉式,线程安全但是效率很低
* 必须加锁 synchronized 才能保证单例,但加锁会影响效率。
* @createTime 2022年02月23日 13:51:00
*/
public class LazySingletonLock {
private static LazySingletonLock instance;
private LazySingletonLock() {
}
public static synchronized LazySingletonLock getInstance() {
if (instance==null){
instance= new LazySingletonLock();
}
return instance;
}
}
我们使用synchronized来加锁,保证了多线程下的单例,但是这个代码锁太过于重了。因为我们每次调用,不论是否发生线程安全问题都会对代码进行加锁,会导致效率变低,对于这个问题我们使用双重锁验证的方式来进行优化:
改进版代码(双重判断):
package Singleton;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName LazySingletonLock.java
* @Description 懒汉式单例模式(线程安全,双重校验锁):
* 双重加锁机制,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。
* 这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象。
* @createTime 2022年02月22日 17:23:00
*/
public class LazySingletonDCL {
/**
* @Description: 使用volatile关键字,避免指令重排导致错误
*/
private static volatile LazySingletonDCL instance = null;
/**
* @Description: 私有化构造方法,防止其他类直接调用
*/
private LazySingletonDCL(){
}
/**
*@Description: 运行时加载对象
*/
public static LazySingletonDCL getInstance(){
//如果对象被创建
if (instance==null){
//对类进行加锁
synchronized (LazySingletonDCL.class){
//二次判空,避免出现初始化两次的情况
if (instance==null){
//为空的话就创建实例
instance = new LazySingletonDCL();
}
}
}
return instance;
}
}
以上代码是使用了一个双重锁的思路来对线程安全问题进行了一个解决,两次判空为了防止多线程调用导致类被二次加载,并且只在可能出现线程安全问题的时候才会加锁。保证了一个对象只会被创建一次,并且增加了效率。使用synchronized对类加锁,保证线程安全。并且对单例对象使用volatile关键字,保证单例对象的原子性,可见性和有序性。
测试一下:
package Singleton.test;
import Singleton.*;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName LazySingletonTest.java
* @Description 懒汉式单例模式测试类
* @createTime 2022年02月22日 17:16:00
*/
public class LazySingletonTest {
public static void main(String[] args) {
/*创建两个对象,如果不是单例模式,他们的在内存中的地址是不同的*/
LazySingleton lazySingleton1 = LazySingleton.getInstance();
LazySingleton lazySingleton2 = LazySingleton.getInstance();
System.out.println(lazySingleton1==lazySingleton2);
/*创建两个对象,如果不是单例模式,他们的在内存中的地址是不同的*/
LazySingletonLock lazySingletonLock1 = LazySingletonLock.getInstance();
LazySingletonLock lazySingletonLock2 = LazySingletonLock.getInstance();
System.out.println(lazySingletonLock1==lazySingletonLock2);
/*创建两个对象,如果不是单例模式,他们的在内存中的地址是不同的*/
LazySingletonDCL lazySingletonDCL1 = LazySingletonDCL.getInstance();
LazySingletonDCL lazySingletonDCL2 = LazySingletonDCL.getInstance();
System.out.println(lazySingletonDCL1==lazySingletonDCL2);
}
}
=========================================================================
3.登记式:
不论是饿汉式还是懒汉式,我们使用了private来修饰构造方法,一方面保护了单例对象不会被其他方法直接访问,一方面导致自己不能被子类继承,损失了扩展性。而登记式的单例模式就是解决了不能被继承的问题。核心原理是通过map的特殊数据结构,KV键值对只会出现一次的性质来进行去重,保证了对象只会被创建一次。当一个子类被创建的时候,我们会到父类的map中加入,想获取实例就直接到父类的MAP中查找返回这个唯一实例。
父类:
package Singleton;
import java.util.HashMap;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName RegSingleton.java
* @Description 登记式单例模式父类
* 登记式的单例模式中父类中有一个集合,用来存储所有的子类的实例,
* 当一个子类创建时,必须在父类的中登记,也就是把自己的实例加入到父类的集合中,
* 当其他类想要获取子类的实例时,就到父类的集合中查找,找到了就返回,
* 如果找不到就创建这个子类的唯一实例。
* @createTime 2022年02月23日 15:55:00
*/
public class RegSingleton {
/**
* @Description 建立一个HashMap来存储子类的完整雷鸣和子类的实例
*/
private static HashMap<String, RegSingleton> regMap = new HashMap<String, RegSingleton>();
/*
@Description 本类的实例加入到 Hash Map
*/
static {
RegSingleton singleton = new RegSingleton();
regMap.put(singleton.getClass().getName(),singleton);
}
/**
* @Description 构造方法不再是private的了,子类可以继承
*/
protected RegSingleton(){
}
/**
*
* @param name 想要获得的类的完整类名
* @return 存储子类的完整雷鸣和子类的实例的HashMap
*/
public static RegSingleton getInstance(String name) {
if (name == null){
name = RegSingleton.class.getName();
}
if (regMap.get(name)==null){
try {
regMap.put(name, (RegSingleton) Class.forName(name).newInstance());
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
e.printStackTrace();
}
}
return regMap.get(name);
}
}
子类:
package Singleton;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName RegSingletonChild.java
* @Description 登记式的单例模式子类
* @createTime 2022年02月23日 17:17:00
*/
public class RegSingletonChild extends RegSingleton{
/**
* @Description 公有构造方法,产生子类对象
*/
public RegSingletonChild(){
}
/**
* 工厂方法,获取本类的唯一实例,实际上是借助了父类的getInstance方法
* @return 返回实例
*/
public static RegSingletonChild getInstance() {
return (RegSingletonChild) RegSingleton.getInstance("Singleton.RegSingletonChild");
}
}
登记式的单例模式解决了懒汉式和饿汉式不能继承的缺点,但是子类中的构造方法变为了public的,所以其他类可以直接通过构造方法创建类的实例而不用向父类中登记,这是登记式单例模式最大的缺点。
=========================================================================
4.枚举:
举跟普通类一样,可以用自己的变量、方法和构造函数,构造函数只能使用 private 访问修饰符,所以外部无法调用。这保证了枚举类可以实现线程安全、自由串行化和单一实例。
package Singleton;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName Enum.java
* @Description 枚举构建单例模式
* @createTime 2022年02月25日 13:02:00
*/
public enum Enum {
/**
* 获取单例对象
* */
INSTANCE;
}
测试一下:
package Singleton.test;
import Singleton.Enum;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName EnumTest.java
* @Description 枚举单例模式测试
* @createTime 2022年02月25日 13:05:00
*/
public class EnumTest {
public static void main(String[] args) {
Enum instance1 = Enum.INSTANCE;
Enum instance2 = Enum.INSTANCE;
System.out.println(instance1==instance2);
}
}
=========================================================================
通过单例配置properties
我们通过单例模式来读取properties,一般我们的配置对象都是单一的,只会对配置对象里面的值进行更改。所以这种业务场景很适合单例模式。
首先创建一个sample.properties的配置类文件
name = OverWatch
type= fps
单例配置类:
package Singleton;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;
/**
* @author Zeyu Wan
* @version 1.0.0
* @ClassName PropSingleton.java
* @Description 单例模式实例配置 Properties
* @createTime 2022年02月24日 09:48:00
*/
public class PropSingleton {
/**
* @Description 设置配置文件地址
*/
private static final String filepath = "src/sample.properties";
/**
* @Description 文件名
*/
private File myFile = null;
/**
* @Description 最后一次更新时间
*/
private long lastUpdateTime = 0;
/**
* @Description 读取的配置文件
*/
private Properties myProp = null;
/**
* @Description 配置文件
*/
private String fName;
/**
* @Description 配置文件
*/
private String fType;
/**
* @Description 设置静态变量
*/
private static PropSingleton propSingleton = null;
/**
* @Description 创建实例
*/
private static class Singleton{
public static PropSingleton getInstance(){
return new PropSingleton();
}
}
/**
* @Description 获取信息
*/
public String getName(){
return fName;
}
/**
* @Description 获取信息
*/
public String getType() {
return fType;
}
/**
* @Description 私有化构造函数
*/
private PropSingleton(){
//读取文件
myFile = new File(filepath);
//设置修改时间
lastUpdateTime = myFile.lastModified();
if (lastUpdateTime==0){
System.out.println("file:"+filepath+"文件不存在");
}
//创建properties
myProp = new Properties();
try {
//加载prop
myProp.load(new FileInputStream(filepath));
//设置配置信息
fName = myProp.getProperty("name");
fType = myProp.getProperty("type");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* @Description 开放一个公有方法,判断是否已经存在实例,有返回,没有新建一个在返回
*/
public static PropSingleton getInstance(){
PropSingleton newPropSingleton = null;
if (propSingleton==null){
//没有实例,就调用内部方法返回实例
propSingleton = PropSingleton.Singleton.getInstance();
}
//有实例就直接返回
newPropSingleton= propSingleton;
return newPropSingleton;
}
}
连着上面的测试一下:
/*创建两个对象,如果不是单例模式,他们的在内存中的地址是不同的*/
RegSingletonChild regSingletonChild1 = RegSingletonChild.getInstance();
RegSingletonChild regSingletonChild2 = RegSingletonChild.getInstance();
System.out.println(regSingletonChild1==regSingletonChild2);
/*创建两个对象,如果不是单例模式,他们的在内存中的地址是不同的*/
PropSingleton propSingleton1 = PropSingleton.getInstance();
PropSingleton propSingleton2 = PropSingleton.getInstance();
System.out.println( propSingleton1== propSingleton2);
=========================================================================
优点:
- 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
缺点:
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了“单一职责原则”。
- 可能会因为触发GC导致对象状态的丢失。