在分享今天的笔记之前,我得先澄清一下。关于设计模式的学习,我都是看刘伟博士 博客上的资料,因为关于Java实现的设计模式的资料比较少,而刘伟博士整理的又非常的好,不得不膜拜。
学习知识最好的方法,就是自己去尝试,然后自己整理出来,跟别人分享,一旦你能够分享出来了,就真的掌握了知识。所以我用这种方式,学习其他博客的东西,整理出来,自己组织语言分享出来,这样是最高效的。所以,我有摘取其他博客的东西,但是也会参杂许多自己的理解,请广大学习者海涵。
前三节我分享了三种不同工厂模式的笔记,分别是简单工厂模式、工厂方法模式和抽象工厂模式,它们都有不同的使用范围,有不同的优势劣势。
今天我们继续来学习另一种对象创建的模式——单例模式。单例模式,说到底就是要确保对象的唯一性。
p{font-size:20}
span{font-size:24;color:red}
h3{font-size:28;color:green}
strong{font-size:22;color:red}
概述
我们有时候会碰到这么一种情况,我们希望类只能创建一个唯一的对象,这个对象要对外隐藏,只对外提供一个访问的接口,这个很容易实现,把对象创建封装在内部,再private,接着写一个访问的方法就可以了。
就拿Windows下的任务管理器来说,在Windows系统下,我们通常都只能打开一个任务管理器,这个任务管理器具有唯一性。Windows系统下的任务管理器就是单例模式。
从以上的话你能抓到几点关键的东西?
单例模式实现有三个重要的点。
- 这个单例类只有一个示例;
- 这个单例类需要对外隐藏;
- 对全局提供访问唯一对象的接口。
看起来,实现单例类具体要解决的两个问题:
- 解决资源浪费的问题,如果一个电脑能好多好多个任务管理器,这意味着要有很多个实例;
- 解决一个时差问题,不同时期访问对象的状态是不一致的,如果不同的时刻去访问任务管理器,它们的信息有可能是不一样的,那还有什么意义吗?
初探
首先我们来实现简单的单例模式,由浅到深循环渐进。
import java.util.*;
class TaskManager {
private static TaskManager tm = null; //对外实现隐藏,保证自己具有唯一性
private TaskManager()
{
//初始化窗口
System.out.println("初始化窗口");
}
public void displayProcesses()
{
//显示进程
System.out.println("显示进程");
}
public static TaskManager getInstance()
{
if (tm == null)
{
tm = new TaskManager();
}
return tm;
}
}
在这里,我们实现了三个重要的点。我们写了一个唯一的静态对象,并且实现了private(隐藏)。我们还对外提供了一个访问方法getInstance,可以拿到该对象。
遗难
现在有这么一个问题,在我们获取这个对象的方法(getInstance)里,我们增加了很多很多的代码。也就是在创建这个唯一的对象之前,我们还有一大堆代码要去执行。
这个时候,如果有多线程的存在,另一个地方也调用了这个方法。一检测,发现还没创建这个唯一的对象,又开始创建一个新的对象。那么,两个不同的线程,最终创建出了两个对象,这就有问题了。
如果不太清楚,我再举个例子。
public static TaskManager getInstance()
{
if (tm == null)
{
process();
tm = new TaskManager();
}
return tm;
}
当判断这个对象还没有创建之后,会调用一个process()方法,然后再创建对象。假设这个方法执行的时候需要1秒,而在这1秒之前,由于有多线程的存在,又调用了这个getInstance()方法。这个时候检测发现,还没有创建对象(因为还在执行process()方法),就创建了一个新的对象。
这样一来,就有两个对象了,对象的唯一性被破坏。那怎么解决呢?有三种解决方法,分别是饿汉式、懒汉式和Holder技术。
饿汉式
饿汉式是比较简单的,一开始就创建了该对象的示例,这样就不用去判断是否创建对象了。
import java.util.*;
class TaskManager {
private static TaskManager tm = new TaskManager(); //对外实现隐藏,保证自己具有唯一性
private TaskManager()
{
//初始化窗口
System.out.println("初始化窗口");
}
public void displayProcesses()
{
//显示进程
System.out.println("显示进程");
}
public static TaskManager getInstance()
{
return tm;
}
}
这个解决方案相对比较简单,我们继续往下看,看看懒汉式。
懒汉式
由于多线程的存在,所以会产生时间的差异,所以我们直接把进程”锁”起来,每一次都只允许一个线程调用该方法。
import java.util.*;
class TaskManager {
private static TaskManager tm = null; //对外实现隐藏,保证自己具有唯一性
private TaskManager()
{
//初始化窗口
System.out.println("初始化窗口");
}
public void displayProcesses()
{
//显示进程
System.out.println("显示进程");
}
public static TaskManager getInstance()
{
if (tm == null)
{
synchronized (TaskManager.class) {
tm = new TaskManager();
}
}
return tm;
}
}
我们将对象创建的一小段代码给”锁”起来了,每一次都只有一个线程能执行。看上去貌似解决了,但其实还是存在问题的,也是刚刚的问题,两个线程都调用了这个方法,一判断,对象还没有创建,结果就排队创建对象,最终还是创建了两个对象。
那我们能不能直接把整个方法都”锁”起来?当然可以,但是这样做,程序的性能就会下降。那还能怎么办?我们能使用双重检测。
import java.util.*;
class TaskManager {
private volatile static TaskManager tm = null; //对外实现隐藏,保证自己具有唯一性
private TaskManager()
{
//初始化窗口
System.out.println("初始化窗口");
}
public void displayProcesses()
{
//显示进程
System.out.println("显示进程");
}
public static TaskManager getInstance()
{
if (tm == null) //第一重检测
{
synchronized (TaskManager.class) {
if(tm == null) //第二重检测
{
tm = new TaskManager();
}
}
}
return tm;
}
}
这种方法确实可以,但是实在是“难看”,增加了代码量不说,还出现了重复的代码,这不是一种良好的编程风格。
两种方式的比较
饿汉模式看起来比懒汉模式简单,但是却有一个缺点。当我们加载TaskManager类时,就会自动创建这个对象。但是,有时候我们并不需要创建这个对象,这就造成了资源浪费的问题。
而懒汉模式,很”难看”,有重复的代码,同时也比较复杂。
有木有综合两者优势的解决方案呢?当然有——Holder技术。
Holder技术
import java.util.*;
class TaskManager {
private volatile static TaskManager tm = null; //对外实现隐藏,保证自己具有唯一性
private TaskManager()
{
//初始化窗口
System.out.println("初始化窗口");
}
public void displayProcesses()
{
//显示进程
System.out.println("显示进程");
}
private static class HolderTaskManager{
private final static TaskManager tm = new TaskManager();
}
public static TaskManager getInstance()
{
return HolderTaskManager.tm;
}
}
我们增加了一个静态内部类,当我们需要这个对象的时候,就会自动加载这个静态内部类HolderTaskManager,加载之后才会创建这个对象。这样我们就不会用到线程锁的知识,降低了程序的复杂度,同时,我们也不会浪费资源,解决了资源浪费的问题。皆大欢喜。
总结
单例模式,说到底就是确保对象的唯一性,在这个过程中,解决两个重要的问题。一个是时差引发的问题,另一个是资源浪费。通过Holder技术,我们可以同时解决这两个问题,这个模式就暂时到这里,以后如果还有什么需要补充的,我会继续更新。
2017/10/7 22:15:40 @Author:云都小生