单例模式的几种简单实现以及理解

学过设计模式的都知道,在创建型模式中,有个单例模式,很简单的设计模式,但是,这里面也设计了比较多的小的细节问题,此处根据代码,简单的讨论一下个人的理解。

什么是单例模式?

难道我学习单例模式之前,就必须去学习所谓的设计模式吗?哈哈,我就不学可以吗?

作者负责的告诉你,你即使根本不知道什么是设计模式,也可以很快的精通单例模式,哈哈,开个玩笑,活跃一下气氛!

所谓的单例模式,顾名思义,其本质就是单例,那什么是单例呢?很简单,就是这个类在程序中只能被创建一次,也就是说,无论你有多少个该类的引用,但是该类的对象实例只能存在一个!这里多说一句,所谓的设计模式,不过是前辈们总结出来的一些开发经验,或者是一些开发技巧更好的去处理程序个模块间的耦合性,所以,说到底,设计模式,就是已经有人把一条条开发经验以结论的形式总结了下来,供后辈直接使用!哈哈,这个总结其实还不错~~

那我怎么才能保证这个类的对象实例只有一个呢?

带着这个问题去思考,先不要着急去看现成的代码,如果现在没有单例模式,没有所谓的设计模式,你现在要满足一个单例模式的逻辑,你会怎么办?顺着这个逻辑,我们会想到,当我在任意阶段创建该类的对象实例的时候,我都判断一下该类是不是已经被创建过了。好的,能想到这里,我们其实已经有了一条死路,哪怕此时的我们并不知道是否行得通。那我们怎么去判断这个类有没有实例对象呢?这好像有点难办,仔细想想,我们第一次创建一个类实例,有了一个指向该实例的引用,那第二次创建的时候呢?我们怎么在第二次创建的时候,去判断一下第一次的这个引用呢?这时候,我们有个简单的做法,就是开始就有一个引用,然后之后程序中所有使用该类实例的地方,都从这个引用中获取。这个思路,就好像一个主内存,和无数个副本。好了,这个问题已经有答案了,也就是在类内部维护一个自身引用的成员变量,起到上述所说的目的。那这样就结束了吗?那我们怎么控制这些所谓的副本在创建对象的时候,就必须去访问主内存呢?这个问题,很快又得到了解决,你程序中要创建对象是吧,好,没问题,但是我这个单例类不让你外部创建,什么意思呢?我把自己的构造方法设置为私有的,哈哈,你外部创建不了,而我内部的引用可以创建,那这时候你所有的外部的引用想要指向该类实例,就必须从我这个主内存中获取。而为了更好的编写习惯,我们又习惯于把类的成员变量封装,然后利用一个方法来获取这个成员变量。

如果你很耐心的读了上述文字描述,你已经可以自己写一个单例模式了,而如果你没有读,那么,也毫无所谓,因为上述的描述只不过是一种引导,是个人在已经了解单例模式的情况下,所提供的一种学习方法!

单例模式(男主角登场,哇塞)

简单的思考过后,我们已经知道,单例模式的三要素:自身引用成员变量,私有构造方法,获取引用的出口

1.第一种最简单的实现方法

public class Singleton {
	private static Singleton singleton = null;
	private Singleton(){}
	public static Singleton getSingleton()
	{
		if(singleton == null)
			singleton = new Singleton();
		return singleton;
	}
}

 我们来理解一下细节:首先,对于这三个要素不多说,上述其实已经叙述过了。我们来看一下这几个问题:

为什么singleton是private static的?很简单,private保证了一种封装性,外部无法直接对singleton引用做出修改,为什么非要封装呢?考虑这样一个问题,现在singleton已经指向了一个对象实例,并且外部已经已经有很多引用通过这个内部的singleton引用也都指向了这个唯一的实例,但是,此时如果外部把singleton赋值为null,哈哈,这时候,调用getSingleton方法竟然又可以创建这样的一个对象实例!而至于为什么是static的,其实很简单,这和方法有关,我们看到这个方法提供了一个向外面返回引用的出口,而我们又知道,单例模式外部引用想要获取这个引用的时候,就必须调用这个方法,怎么才能调用这个方法呢?很简单,直接使用static的,用类去直接调用,而不是用实例去调用。所以说,方法是static的,那Singleton也就必须是static的了。

2.第二种实现方法

class Singleton2
{	
	//初始化的时候就创建,这时候需要理解一下JVM的类加载过程!
	private static Singleton2 singleton2 = new Singleton2();
	private Singleton2(){}
	public static Singleton2 getSingleton()
	{
		return singleton2;
	}
}

我们来看这种方法和之前的唯一不同之处,就是在声明引用的时候,就进行了创建实例。这样做是为什么呢?其实很简单,如果你了解JVM的类加载过程,你会知道,对于static的类变量,在其类加载到内存的过程中,就已经把这个类变量按照代码意愿(也就是代码中的new一个对象实例)进行了赋值(详细信息,请自行学习类加载的初始化过程,也就是所谓的JVM自动构建的clinit()方法),也这样做的好处是什么呢?哈哈,类只会加载一次,而创建对象的语句只会在类加载的初始化阶段被执行,说明这种情况,不会存在多线程的同步问题。哈哈,是不是很神奇,没错,这种情况下,整个new这个语句永远只会被执行一次。

3.第三种实现方法

//第三种方法
class Singleton3
{
	private static Singleton3 singleton3 = null;
	private Singleton3(){}
	//对方法同步,保证该方法在同一时刻只能由同一线程执行
	public static synchronized Singleton3 getSingleton3()
	{
		if(singleton3 == null)
			singleton3 = new Singleton3();
		return singleton3;
	}
}

这种方法,没什么好说的,简单粗暴的使用synchronized直接对方法进行同步,也是在解决并发同步问题。

4.第四种实现方法

class Singleton4
{
	//volatile保证了变量的多线程下的可见性!
	private volatile static Singleton4 singleton4 = null;
	private Singleton4(){}
	public static Singleton4 getSingleton4()
	{
		//理解第一个判断
		//很简单,避免除第一次进入该方法后的其他任意调用该方法时候的synchronized的性能消耗
		if(singleton4 == null)
			synchronized (Singleton4.class)
			{
				//第二个判断,就是常规的保证其只能实例化一次
				if(singleton4 == null)
					singleton4 = new Singleton4();
			}
		return singleton4;
	}
}

这种方法,有时候,被称为双检查实现,很容易看到,代码进行了两次是否为空的判断。

我们来理解一些细节:volatile保证了多线程下的可见性,也就是只要有线程更改了引用,其他线程再读引用的时候,得到的应该是修改后的值,volatile就保证了这一点。

那双检查又是为什么呢?其实并不是这个两个检查有什么关联,或者说共同决定了某种逻辑,而是这两个判断,分别有自己的存在意义。我们先来看第二个,很容易理解,为了保证唯一性,每次调用的时候,就要看看是否已经创建过了。那第一个怎么理解呢,其实,也很简单,假如我们没有第一个,那么假如我们现在已经创建了唯一实例,那任何外部引用再调用这个静态方法的时候,就一定会先执行synchronized上锁,然后再去判断有没有创建过,这样一来,除了第一次synchronized真正发挥了作用,其余所有后续的次数synchronized其实是毫无意义的,这样就浪费了很多次synchronized的性能消耗,所以说先来一下判断,如果现在引用已经不为空了,那就根本没必要上锁,再去检查了,因为上锁检查的结果也还是空,哈哈,每一个小的细节真正理解就,都会很感慨我们人类的聪明智慧!

结语:

看到这里我们也就明白了一件事情,学习不要死记硬背,有一定的理解,其实更容易去使用,去记忆!上述例子,很简单,但很多小的细节问题,在设计的时候或许也很巧妙。所以说成长是相对的,但学习是绝对的!

PS:这篇文章只是作为最基础的了解单例模式的一种学习思路,基于多线程安全的单利模式进阶实现,参考另外一篇博客:

https://blog.csdn.net/romantic_jie/article/details/103891855

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章