本篇文章将本人在学习设计模式之初以及后续产生的问题进行汇总并逐一解答。
如:
1.双重检查加锁为什么可以提高效率。
2.懒汉,饿汉模式中实例对象的引用都具有static,二者的具体区别。
一. 单例模式的定义
**保证一个类只有一个实例,并提供它的一个全局访问点。**
二、应用单例模式解决问题的思路
要控制一个类只能有一个实例,就要把创建实例的权限收回,让类自己负责创建实例的工作,然后由这个类提供对外的可以访问该对象的方法,便实现了单例模式。
三、单例模式的几种实现
1.饿汉式
所谓饿汉式,顾名思义,就是在创建对象时比较着急,因此在装载类的时候直接创建一个静态实例。
1)构造器私有
private HungryMan(){}
2)创建一个HungryMan类型的常量HUNGRY_MAN
private static HungryMan HUNGRY_MAN = new HungryMan();
3)提供对外访问对象的方法
public static HungryMan getInstance(){
return HUNGRY_MAN;
}
将三步合并后,就是如下所示:
class HungryMan{
//1.构造器私有
private HungryMan(){
}
//2.创建一个HungryMan类型的常量HUNGRY_MAN
private static HungryMan HUNGRY_MAN = new HungryMan();
//3.提供对外访问对象的方法
public static HungryMan getInstance(){
return HUNGRY_MAN;
}
}
饿汉单例模式是线程安全的,因为虚拟机只会装载类一次,后续无论是哪个线程调用,return的都是同一个静态变量,不会出现并发。
2.懒汉式
1)构造器私有
private LazyMan(){}
2)创建一个LazyMan对象的引用(因为是懒汉,所以不急着创建,等需要的时候在创建)
private static LazyMan lazyman = null;
3)提供对外访问对象的方法
public static LazyMan getInstance(){
if(lazyman == null)
lazyman = new LazyMan();
return lazyman;
}
对以上三步进行合并:
class LazyMan{
//1.构造器私有
private LazyMan(){
}
//2.创建一个LazyMan对象的引用(因为是懒汉,所以不急着创建,等需要的时候在创建)
private static LazyMan lazyman = null;
//3.提供对外调用的方法
public static LazyMan getInstance(){
if(lazyman == null)
lazyman = new LazyMan();
return lazyman;
}
**此处应注意:
成员变量lazyman由于要在静态方法getInstance中使用,所以这个属性必须被迫加上static关键字,因为静态成员只能调用静态成员,也就是说,懒汉模式并没有使用static的特性。
相反,饿汉单例模式下,static修饰的实例变量HUNGRY_MAN体现了static的特性。
**
分析:懒汉单例是线程不安全的。 当某一线程执行到if语句准备创建对象时,另一线程可能也会进入,所以需要进行改进。
2.1延时加载的思想:
单例模式中的懒汉模式体现了延迟的思想,那么,什么事延迟加载呢?
延时加载是典型的以时间换空间 ,通俗的说,就是一开始不要加载数据或资源,等到不得不加载的时候再去进行加载,这么做可以尽可能地节约资源。
//这里就体现了延时加载的思想,在创建前先进行判断,若果没有实例,在进行创建。
if(lazyman == null)
lazyman = new LazyMan();
return lazyman;
3.双重检查加锁(Double Check Lock)
public static LazyMan getInstance(){//1
if(lazyman == null)//2
lazyman = new LazyMan();//3
return lazyman;
}
上面说的第一种懒汉模式是线程不安全的.
为了设计出更好的程序,立刻进行尝试!
尝试一:
假设有两个线程A和B同时调用getInstance方法,当线程A执行完第二行代码准备执行第三行时,而线程B刚运行完第一行,正在进行判断,程序继续进行,
1.由于线程B运行较快,一下就判断出lazyman == null,为true
2.而此时线程A正在创建实例,new LazyMan();
3.但线程B也已经判断结束,也进行到第三行,
4.这是就产生了问题,创建了两个实例对象
这是要想解决线程不安全的问题,使用synchronized关键字即可
public static synchronized LazyMan getInstance()
但是这样以来,会使得每个调用getInstance方法的线程都需要在方法外徘徊,也就是说即使lazyman 不为空,可以直接return,但也会被拒之门外,所以大大降低了程序执行的效率。
所以第一种尝试不可用!!!
尝试二,因为此次尝试是正确的,所以直接上代码
(在getInstance方法内部进行加锁以及双重判断)
class LazyMan2 {
//1.构造器私有
private LazyMan2() {
}
//2.创建一个LazyMan对象的引用
private static volatile LazyMan2 lazyman2 = null;
//3.提供对外调用的方法
/**
* 双检锁,又叫双重校验锁,综合了懒汉式和饿汉式两者的优缺点整合而成。
* 看下面代码实现中,特点是在synchronized关键字内外都加了一层 if 条件判断,
* 这样既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。
*/
public static LazyMan2 getInstance() {
//①先检查实例是否存在,不存在才进入下面的同步块
if (lazyman2 == null) {
synchronized (LazyMan2.class) {
//②再次检查实例是否存在,如果不存在才才真正创建实例
if (lazyman2 == null)
lazyman2 = new LazyMan2();
}
}
return lazyman2;
}
}
第一次判断是为了排除实例已存在的情况,也就是说只有当实例不存在时,线程才会被拒之门外进行等待,反之,若实例存在,则无需排队,直接return!
第二次判断是为了避免创建更多的实例,假设有两个线程在门外等待,线程A进去以后创建一个实例,这时轮到线程B了,如果没有第二次判断,他也会接着创建一个线程,所以必须双重检查。
故第一次判断是为了提高效率,第二次判断才是避免重复创建。
需要注意的是volatile关键字可能会屏蔽掉虚拟机内部的一些代码优化,因此若没有特殊要求,也不要大量使用。
4.内部类单例模式
要想很简单的实现线程安全,可以采用静态初始化器的方法,他可以由JVM来保证线程的安全性,例如饿汉单例模式,但是这样一来则会提前浪费一部分空间,如果现在有一种方法能够在类初始化的时候不去加载实例对象不就行了?一种可行的方法就是采用内部类单例模式。
实例代码如下:
class StaticLazyMan{
//2.将创建对象放在一个静态内部类中
private static class StaticLazyManInner{
private static final StaticLazyMan slmi = new StaticLazyMan();
}
//1.构造器私有
private StaticLazyMan(){
}
//3.通过调用内部类,实现单例对象的初始化
public StaticLazyMan getInstance(){
return StaticLazyManInner.slmi;
}
}
当getInstance方法第一次被调用的时候,他第一次读取StaticLazyManInner.slmi,导致StaticLazyManInner.slmi类得到初始化,而这个类在装载并初始化时,会初始化它的静态域,从而创建StaticLazyMan的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并有虚拟机来保证他的线程安全性。
这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。
5、枚举单例模式
- Java的枚举类型实质上是功能齐全的类,因此可以有自己的实行和方法
- Java枚举类型的基本思想是通过共有的静态final域为每个枚举常量导出实例的类。
- 从某个角度讲,枚举是单例的泛型化,本质上是单元素的枚举。
用枚举实现单例非常简单,只需要编写一个包含单个元素的枚举类型即可。
示例:
enum SingleTon{
/**
*定义一个枚举类型的元素,就代表了一个实例
*/
INSTANCE;
public void method(){
//功能处理
}
}
四、何时选用单例模式
当需要控制一个雷=类的实例只有一个,而且客户只能从一个全局访问点访问它时,可以选用单例模式,这些功能恰好是单例模式要解决的问题。
参考文献:《研磨设计模式》