创建型模式-单例模式
学习目标:
- 什么是单例模式?
- 为什么会有单例模式?即他的用处,以及它解决了什么问题
- 怎样事项单例模式,即它的设计思想
- 单例模式有哪些写法?
- 单例模式在面试中有哪些注意事项?
1、为什么会有单例模式?
- 我们都知道,单例模模式是在开发中最常用的一种设计模式,
- 例模式主要是为了避免因为创建多个实例造成的资源浪费,且多个实力因为多次调用容易导致结果出现错误,而使用单例模式可以保证整个应用中有且只有一个实例。
- 可以解决的问题:可以保证一个类在内存中对象的唯一性,在一些常用的工具类、线程池、缓存、数据库、账户登录、配置文件等程序中可能只允许我们创建一个对象;一方面,如果创建多个对象可能引起程序错误,另一方面,创建多个对象也造成了系统资源的浪费,在这种需求下,单例模式就诞生了。
加入有这么一个需求,有一个类A 和 类B,以及他们的共享配置文件信息,在这个配置文件中有很多数据如下图所示:
如上图所示:在类ConfigFile中,存在共享数据 Num1, Num2, Num3... ... 加入在类A 中修改ConfigFile中的数据,在类A 中应该有如下代码,
ConfigFile configFile = new ConfigFile();
configFile.num1 = 2;
此时,configFile 中的Num1 = 2, 但是请注意,这里是new ConfigFile(),是一个对象,在进行了上述操作后,类B 中进行如下操作
ConfigFile configFile = new ConfigFile();
System.out.println( "configFile.Num1 = " + congilFile.Num1 );
即直接new ConfigFile();然后打印,Num1,大家思考一下,打印的是即?
我们应该知道,打印结果是这样的: configFile.Num1 = 1;
也就是说每次调用都创建了一个ConfigFile对象,所以导致了在类A 中修改并不会真正改变ConfigFile中的值,它所更改的只是在类A 中创建的那个对象的值。
加入现在在类A 中修改数据后要通知B , 即在类A 和类B 中操作的是同一个数据,类A 中改变数据,类B 中也会得到该数据,并在类A 修改后的基础上进行操作,到这里可能会说将ConfigFile 类中的所有数据改成静态的就可以了,虽然可以,但是这样对内存是一个极大的消耗,。所以将变量设置成静态的虽然可行,但并不是一个很好的解决方案。这里,就需要用到单例模式了!!
2、单例模式的思想
在上面的问题中,我们说到,要解决这样的问题的关键就是保证应用中只用一个对象就行了,那么,怎么保证只用一个呢?
其实,只要三步,就可以保证一个对象的唯一性:
- 不允许其他程序new 对象
- 因为new 就是开辟新的空间,在这里更改数据只是更改所创建的对象的数据,如果可以new 的话,每次new 都产生一个新的对象,这样肯定保证不了对象的唯一性。
- 在该类中创建对象,
- 因为不允许其他程序new 对象,所以这里的对象需要在本类中new出来,
- 对外提供一个可以让其他程序获取该对象的方法
- 因为对象是在本类中创建,所以需要提供一个方法,让其他类获取这个对象。
那么,上述这省三步在代码中是怎样实现的呢?将上述三步转换成代码描述是这样的,
1> 私有化该类的构造函数;
2> 通过new 在本类中创建一个本类对象;
3> 定义一个共有的方法,将在该类中创建的对象返回;
3、单例模式的写法
经过2 中的分析,我们了解了单例模式所解决的问题以及他的思想,接下来看看他的代码实现方式,单例模式的写法大致可分为5种
- 懒汉式
- 饿汉式
- 双重检验锁
- 静态内部类
- 枚举
接下来就来看看这几种模式的代码实现,以及他们的优缺点:
-
饿汉式
public class Singleton{ public static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance (){ return instance; } }
访问方式:
Singleton instance = Singleton.getInstance();
得到这个对象之后,既可以访问类中的方法了,
优点:从他的实现种我们可以看到,这种实现的方式比较简单,在类加载的时候就完成了实例化,避免了线程的同步;
缺点:由于在加载的时候就实例化了,所以没有达到 Lazy Loading (懒加载) 的效果,也就是说,我可能没用到这个对象,但它也会加载,会造成内存的浪费(但是可以忽略不记,这种方式也是推荐使用的)
-
饿汉式
public class Singleton{ public static Singleton instance = null; static { instance = new Singleton(); } private Singleton(){} public static Singleton getInstance(){ return instance; } }
访问方式,
Singleton instance = Singleton.getInstance();
得到这个实例后,就可以访问类中的方法了,
可以看到,上面的代码,在类创建的时候就已经实例化了对象。
-
懒汉式【线程不安全,不可用】
public class SingletonA { private static SingletonA instance = null; private SingletonA(){}; /** * 方式一: * 有线程安全问题 * @return */ public static SingletonA getInstance() { if ( instance == null ){ instance = new SingletonA(); } return instance; } }
这种方式式在调用getInstance 方法的时候将对象实例化的,因为比较懒,所以被称为懒汉式;
在上述写法中,懒汉式确实存在线程安全的问题,那么到底存在怎样线程安全的问题?怎样导致这种问题的?接下来说说:
在运行过程中存在这么一种情况,有多个线程去电泳getInstance 方法来获得Singleton 实例,那么就可能发生这样一种情况:当线程A 去执行 =if ( instance == null )= 时,此时 instance 为 null 进入语句,因为这个时候,线程还没有执行 instance = new Singleton(), 此时线程B 也进入了if语句,所以线程A、线程B 都会 执行instance = new Singleton() 来实例化对象。这样就导致实例化了两个Singleton 对象,所以单例模式的懒汉式是存在线程安全问题的,既然它存在问题,那么就应该有解决问题的方法,怎么解决呢,有人可能想到加锁去解决这个问题,于是有了下面的写法。
-
懒汉式线程安全【线程安全,效率低下不推荐使用】
public class SingletonA { private static SingletonA instance = null; private SingletonA(){}; /** * 方式二: * 解决懒汉线程安全【不推荐使用】 */ public static synchronized SingletonA getInstance(){ if( null == instance ){ instance = new SingletonA(); } return instance; } public static void main(String[] args) { SingletonA instance = SingletonA.getInstance(); System.out.println(instance); } }
-
懒汉式【双重检验锁,推荐使用】
public class Singleton{ /** * 懒汉式变种,属于懒汉式中最好的写法,保证了:延迟加载和线程安全 **/ private static Singleton instance = null; private Singleton(){}; private static Singleton getInstance(){ if( instance == null ){ synchronized ( Singleton.class ){ if( instance == null ){ instance = new Singleton(); } } } return instance; } }
访问方式:
Singleton instance = Singleton.getInstance();
Double-Check 对于线程开发者来说并不会陌生,如代码中所示,我们进行了两次if( instance == null ) 检查,这样就可以保证线程安全;
优点:线程安全;延迟加载;效率较高;
-
内部类【推荐使用】
/** * 内部类方式 */ public class SingletonB { private SingletonB(){}; private static class SingletonHolder{ private static SingletonB instance = new SingletonB(); } public static SingletonB getInstance(){ return SingletonHolder.instance; } public static void main(String[] args) { SingletonB instance = SingletonB.getInstance(); System.out.println(instance); } }
访问方式:
SingletonB instance = SingletonB.getInstance();
这种方式和饿汉式采用的方法类似,但又有些不同,两者都是采用类装载机制,保证初始化实例时只有一个线程,不同的时,饿汉式只要Singleton 类被装在就会实例化,没有lazy-loading的作用,而静态内部类方式在Singleton 类被装载时并不会被实例化,而是在需要实例化时,调用getInstance 方式,才会装载SingletonHolder 类,从而完成Singleton 的实例化,类的静态属性只会在第一次加载类的时候被实例化,在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时别的线程时无法进入的。
优点:避免了线程不安全;延迟加载;效率高。
-
枚举【极推荐使用】
public enum SingletonEnum { instance, ; private SingletonEnum(){} public SingletonEnum method(){ return instance; } public static void main(String[] args) { SingletonEnum.instance.method(); } }
访问方式
SingletonEnum.instance.method();
可以看到,枚举的使用方式非常简单;
借助JDK1.5中的枚举来实现单例模式。不仅能避免线程同步问题,而且还能防止反序列化来重新创建新的对象。这种方式时最好的一种方式,在实际开发中,如果JDK满足要求的情况下,建议使用这种方式。
4.单例模式在面试种的问题
单例模式在面试种常常会遇到,其中很少有人会问到饿汉式写法,一般都会问单例模式的懒汉式的线程安全问题,所以一定把大比例模式的线程安全问题理解清楚,认真学透