设计模式——单例模式

本文介绍了设计模式中的单例模式,包括其重要性和使用场景。详细讲解了单例模式的特点,如构造方法私有化、提供静态获取方法等,并通过UML图展示其结构。接着,文章对比了饿汉式、懒汉式、双重检查锁等多种单例模式的变种,讨论了它们的优缺点,特别强调了双重检查锁在解决线程安全和性能问题上的优势。最后,提出了根据实际场景选择合适单例模式的建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    这几天笔者刚开始看设计模式,这个东西虽然是一个比较抽象的东西,但是它更注重的是一种框架设计的思想,不只是Android,很多领域都会涉及到这个,感觉很重要。而且在Android中有很多类都是通过各种模式去实现的,也是为了以后能够更好地看懂源码和框架吧。

    然而前几天在刚开始看单例模式的是就卡壳了-。+,原因是在看的时候,发现有很多涉及到的知识点已经忘记了,或者当时就没有好好的学,结果现在学设计模式的时候很懵逼。。。为此本人写了几篇文章,是在设计模式中运用到的但可能已经忘记的知识点(反正对于本人来说已经是忘记了0.+),当然这些文章会不断的更新的。


本片中可能涉及到的知识点:

设计模式前篇——多线程同步synchronized、volatile详解

设计模式前篇——枚举


单例模式介绍

首先简单介绍一下什么是单例模式,为什么要用这种模式。

我来举个栗子:

我们有一个类A,我们如果想要使用,那就直接new一个A出来吧。

public static void main(String[] args) {
	// TODO Auto-generated method stub
	A a = new A();
}

很简单,根本无难度,对吧。


假如说这个类A,是一个功能类的类(例如Android中布局加载器LayoutInflater),我们创建A出来只是为了实现一个功能,也就是说不管多少个A对象被创建出来,他们谁都能够完成这个功能,而且互不影响。你懂我意思吧。

这只是在main方法中创建了一个A对象然后使用,加入现在有1000个方法中需要通过A完成这个功能,如果说不是同时运行的还好,如果这1000个方法都同时创建一个A对象,要是所有的android程序都这么写,我估计手机内存扩展到10个G都不够用吧-。+。


上面这个案例,明确的阐述了单例模式的重要性,同时也说明了它的运用场景:

  • 在程序中多处地方大量使用
  • 被创建的多个对象都能够完成一功能,而且互不影响。

作用:为了节约内存开销,避免不必要的内存开辟。提高效率,提高资源利用率。

单例模式顾名思义,就是让这个类只产生一个对象。因为这种情况下需要这个对象的情况太多了,不如我就给你创建一个,这一个对象可以让全局都调用,反正一个和一百个都能完成任务,这样还节约了内存。


单例模式特点

一个类满足单例模式,主要有一下几个特点:

  1. 构造方法私有化
  2. 定义静态方法,且返回当前的对象
  3. 确保对象是唯一的!
  4. 确保在序列化和反序列化操作之后,保证还是一个对象
  5. 不允许被继承

前几点相信大家都能够看得懂,简单说一下就好:

因为只允许创建一个对象,那么我们在调用实例的时候肯定不会用到new方法去创建对象了。而是通过一个static方法,所以我们要保证在外部无法通过new方法去创建对象。而创建对象的过程,在类构造中或者在static方法中实现。

不允许被继承,这是一个标准的单例模式的规范,我们知道就好。

重要说一下第四点:关于序列化和反序列化笔者之前写过一篇文章,大家可以去看一下:Android中序列化与反序列化。也就是说,我们在将对象进行序列化写入、反序列化读取,通过字节序列重新恢复成的对象,一定要保证是一个对象。


单例模式UML图结构

我觉得单例模式可以说是这里面最简单的一个设计模式了,因为他的UML类图实在是太好画了。。。。


只是一个依赖关系,这里只是那MainActivity举例了,当然还有很多地方调用到SingleTon。


模式变种

对于单例模式,相信大家之前有看到过一些,也有用过,其实那只是单例模式的一种或两种。下面是几种常用到的单例模式的变种:

1.饿汉式

相信大家之前最常用过得就是这种单例模式了,我们来看一下他的构造:

/*
 * 饿汉式
 * */

public class SingleTon {
	private static SingleTon instance = new SingleTon();
	
	private SingleTon() {
		System.out.println(Thread.currentThread().getName() + "  创建成功");
	}
	
	public static SingleTon getInstance() {
		return instance;
	}
}

优点:线程安全。因为这种情况下在类加载的时候就已经完成了初始化,我们不用去担心在多个线程调用时产生重复创建的问题(现在说可能不太明白,我们等会对比下面的情况一起说)

缺点:

1.可能会占用不必要的内存空间。

    由于是在类加载的过程中new的对象(这里涉及到类的生命周期的一些知识,笔者之后会补一篇关于这个的文章),所以无论我们用不用,他都会创建,这就有一种情况了。确实我们通过这样实现了唯一的对象,但是我们在不用他的时候,他还是会在占用着内存,如果所有的单例都这样写,一个项目中有成百上千个单例,全部都占用着一块内存,我想这也是我们不愿意看到的。

2.类加载先后顺序与声明之前的问题。

   现在我们只需要知道:饿汉式可能会因为声明顺序,而导致创建初始化数据出错。造成异常。之后本人会出一篇文章细说这个问题。


有没有这样一种情况呢:让他在需要的时候创建,不需要的时候先不要创建,以此来达到节约内存的效果。


2.懒汉式
/*
 * 懒汉式
 * */

public class SingleTon {

	private static SingleTon instance;
	
	private SingleTon() {
		System.out.println(Thread.currentThread().getName() + "  创建成功");
	}
	
	public static SingleTon getInstance() {
		if(instance == null) {
			instance = new SingleTon();
		}else {
			System.out.println("已经有实例");
		}
		
		return instance;
	}
}


与饿汉式相比,懒汉式只是在其基础上做了一点小小的变动:在声明时不创建对象,而是在获取的方法中判断是否已经创建,如果没有创建就new对象并返回。

优点:这种情况相比于饿汉式来说,他的性能更高一些,因为他避免了在没有使用的时候占用了不必要的内存。

缺点:

问题1:线程安全问题。

    我们喜欢拿这两种变种进行对比,因为他们是在是太像了。而且优缺点貌似还是正好反过来的。在懒汉式情况下,我们需要考虑线程安全的情况:在多个线程并发进行的时候,如果都要用到SingleTon对象的时候,那他们都在调用getInstance方法,这样会导致什么结果出现呢?

public static void main(String[] args) {
	// TODO Auto-generated method stub
	for(int i = 0;i < 10;i ++) {
		new Thread(new Runnable() {		
			@Override
			public void run() {
				// TODO Auto-generated method stub
				SingleTon.getInstance();
			}
		}).start();
	}
}
我们在main方法中开启了10个线程,里面都获取SingleTon实例,看一下结果会发生什么:
Thread-0  创建成功
Thread-1  创建成功
Thread-2  创建成功
已经有实例
已经有实例
已经有实例
已经有实例
已经有实例
已经有实例
已经有实例

发现有三个线程同时创建了对象,这是因为线程是并发进行的,他们同时调用了getInstance方法,在创建还没有完成的时候,另一个线程获取到的信息是instance == null,所以他也会创建,这就导致了多个实例被创建,这是我们不希望看到的。

那我们想一个办法——添加synchronized同步锁吧。(关于同步锁以及下面重排序的相关知识:设计模式前篇——多线程同步synchronized、volatile详解

/*
 * 懒汉式
 * */

public class SingleTon {

	private static SingleTon instance;
	
	private SingleTon() {
		System.out.println(Thread.currentThread().getName() + "  创建成功");
	}
	
	public static synchronized SingleTon getInstance() {
		if(instance == null) {
			instance = new SingleTon();
		}else {
			System.out.println("已经有实例");
		}
		
		return instance;
	}
}

这种情况下在同一时刻只能有一个线程使用getInstance方法,这样也就不存在同时创建的问题了。

但是它真的是完美的吗?


问题2:添加同步锁后,出现阻塞,耗费性能问题。

由于我们添加了同步锁,所以同一时刻只有一个线程可以使用getInstance方法,其他线程在没有获取到同步锁的时候会进行阻塞,就是在那里等着。

如果这一个同步的方法需要创建1分钟,那和100个线程就在那里傻等着,这种情况真的要死了。。。。。

所以这样也不是很完美。接下来我们看一下第三种变种。


3.双重检查锁

先看一下这种方式的半成品代码:

/*
 * 双重检查锁(半成品)
 * */

public class SingleTon {
	private static SingleTon instance = null;

	private SingleTon() {
		System.out.println(Thread.currentThread().getName() + "   创建成功");
	}

	public static SingleTon getInstance() {
		if (instance == null) {
			synchronized (SingleTon.class) {
				if (instance == null) {
					instance = new SingleTon();
				}
			}
		}

		return instance;
	}
}

这里面主要修改的还是getInstance方法,他相比与饿汉式跟懒汉式,都要优越一些,因为他即保证了线程安全,实现了单例,又一定程度上解决了性能耗费的问题。也实现了延时初始化。

这里主要说一下为什么相比于懒汉式,它的性能更优越呢:在懒汉式中我们是给getInstance方法整个加锁,这就导致了可能会出现多个线程同时阻塞状态,造成了性能耗费,而在双重检查中,我们用同步代码块代替了同步静态方法:

  • 给同步代码块外面留了一个第一个if判断:是否已经创建了SingleTon对象,如果他已经创建了,那么其他线程就不用在这个方法中傻等着了,直接return获取。这样就避免了在创建过程中全部线程阻塞的问题。
  • 再看一下同步代码块中第二个if判断:如果为空就创建对象。由于添加了synchronized同步锁,所以这段代码具有原子性,会在执行完毕,在主存中同步数据后,释放锁。

这个东西理解起来比较抽象,大家还是要自己推敲一番啊。。。

简单概括一下:懒汉式是在多个线程中等待获取同步锁,而双重检查给这些线程留了一道门(外层if),给了他们一个不用频繁获、取释放同步锁的机会。这样一对比性能是优越了不少吧。


但这种方法也不是十分完美的,他很可能会出现一个问题——双重检查锁失效。

原因1:重排序问题

我们跳回到这部分代码:

public static SingleTon getInstance() {
	if (instance == null) {
		synchronized (SingleTon.class) {
			if (instance == null) {
				instance = new SingleTon();
			}
		}
	}
	return instance;
}

其实主要造成这个问题的,是这一行代码:

instance = new SingleTon();

这是一个new创建对象的过程。我觉得我们又有必要补充一些基础知识了:

这个操作大概可以拆分为几个部分:

  1. 栈开辟空间给instance引用存放。
  2. 堆开辟空间给SingleTon对象做准备。
  3. 初始化对象(构造方法)
  4. 让instance引用指向堆中的内存空间。

关于重排序问题在同步锁的篇中大概说到了,大家可以翻到上面看一下链接。

简单来说,重排序是系统为执行效率更高搞出来的东西,它让我们的指令顺序变乱。

比如上面的1->2->3->4,他很有可能出现1->2->4->3,在执行到4的时候,我们的instance已经不为空了,但是还没有调用构造方法初始化,如果说这个时候其他线程在第一层if判断,会是不为null的结果,那么他就直接返回instance了,这样一来会导致我们的数据出现问题,最后可能会出现一颗老鼠屎,坏了一锅汤的情况。

而对于这种情况我们也知道, 只要给instance属性添加volatile关键字,就可以避免重排序的问题了。

所以最完美的双重检查锁代码如下:

/*
 * 双重检查锁(完成品)
 * */

public class SingleTon {
	private static volatile SingleTon instance = null;

	private SingleTon() {
		System.out.println(Thread.currentThread().getName() + "   创建成功");
	}

	public static SingleTon getInstance() {
		if (instance == null) {
			synchronized (SingleTon.class) {
				if (instance == null) {
					instance = new SingleTon();
				}
			}
		}

		return instance;
	}
}
但是注意,在j5之前是没有volatile关键字的,所以说在此之前还是会出现双重检查失效的问题。


4.静态内部类

静态内部类可以说相对于上面几种最简单而且最完美的一种情况了。而且是官方推荐的一种写法。

代码如下:

/*
 * 静态内部类
 * */

public class SingleTon {

	public static SingleTon getInstance() {
		return SingleTonHolder.instance;
	}
	
	static class SingleTonHolder{
		private static SingleTon instance = new SingleTon();
	}
}
这种写法看上去太舒服了,而且他既做到了延时初始化,节约不必要的内存开销(因为静态内部类的类加载过程是在调用时候执行,如果不使用就不会加载,也就不会创建instance对象了),又保证了线程的安全。


5.枚举

这个是一种比较新型的写法:

/*
 * 枚举
 * */

public enum SingleTon {
	INSTANCE;
	
	private String asd = "我只是一个普通的属性!!!";
	
	public void asd() {
		System.out.println("我只是一个普通的方法!!!   "
				+ asd);
	}
}

我们什么都不用操作,只需要创建一个INSTANCE实例就好。因为编译过后回味INSTANCE生成一个对应的SingleTon对象,而且枚举类型经过编译后生成的class对象还实现了序列化,又保证了线程安全,岂不是美滋滋。(关于枚举的文章请见:设计模式前篇——枚举


6.集合——对象管理

对于这种变种,已经不能算是标准的单利了,但我觉得还是有必要了解一下:

/*
 * 集合
 * */

public class SingleTonManager {

	private static Map<String, SingleTon> map = new HashMap<>();
	
	public static void putValue(String key,SingleTon value) {
		SingleTon instance = map.get(key);
		if(instance == null)
			map.put(key, value);
	}
	
	public static SingleTon getVaule(String key) {
		return map.get(key);
	}
}

class SingleTon{
	
}


使用场景(推荐)

我们列举了这么多变种,发现前面几个标准的单利中,几乎每个变种都有好坏,难道就没有一个完美的模式吗?

其实这种想法多虑了,我们只需要针对不同的情况去应用不同种类的单例就好了,假如说你只是在一个线程中使用单例,你搞一个双重检查?没事找事。

在多线程下:推荐使用静态内部类的方式。

在单线程下:推荐使用懒汉式。


今天的文章就到这里了,如果有意见或者探讨的朋友欢迎在下方留言,喜欢的朋友点赞关注一波哦。

谢谢大家的支持!!



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值