嘻哈说:设计模式之单例模式

一、嘻哈说

首先,请您欣赏单例模式的原创歌曲java

嘻哈说:单例模式
做曲:懒人
做词:懒人
Rapper:懒人

某个类只有一个实例
并自行实例化向整个系统提供这个实例
须要私有构造方法毋庸置疑
自行实例化各有各的依据
提供单一实例则大致一致
饿汉静态变量初始化实例
懒汉初始为空
获取实例为空才建立一次
方法加上锁弄成线程安全的例子
DCL双重检查锁两次判空加锁让并发不是难事
建立对象并非原子操做由于处理器乱序
volatile的关键字开始用武之地
静态内部类中有一个单例对象的静态的实例
枚举天生单例
容器管理多个单例
复制代码

试听请点击这里设计模式

闲来无事听听曲,知识已填脑中去;安全

学习复习新方式,头戴耳机不小觑。bash

番茄课堂,学习也要酷。并发

二、定义

在Java设计模式中,单例模式相对来讲算是比较简单的一种建立型模式。app

什么是建立型模式?学习

建立型模式是设计模式的一种分类。优化

设计模式能够分为三类:建立型模式、结构型模式、行为型模式。ui

建立型模式:提供了一种在建立对象的同时隐藏建立逻辑的方式,而不是使用 new 运算符直接实例化对象。spa

结构型模式:关注类和对象的组合,用继承的概念来组合接口和定义组合对象得到新功能的方式。

行为型模式:关注对象之间的通讯。

咱们来看一下单例模式的定义。

确保某一个类只有一个实例,并且自行实例化并向整个系统提供这个实例

也就是,保证一个类仅有一个实例,并提供一个访问它的全局访问点

单例模式在懒人眼中就是,注孤生,悲惨世界

三、特性

从定义中,咱们能够分析出一些特性来:

单例类只能有一个实例

确保某一个类只有一个实例,must be 呀。

单例类必须自行建立本身的惟一的实例

自行实例化。

单例类必须给全部其余对象提供这一实例 。 向整个系统提供这个实例。

内存中会长期持有单例实例,若是不是对全部对象提供访问,例如只对包内类提供访问权限,存在的意义就不大了。

四、套路

怎样确保某一个类只有一个实例?

套路1:私有化空构造方法,避免多处实例化。

套路2:自行实例化,保证明例化在内存中只存在一份。

套路3:提供公有静态getInstance()方法,并将单一的实例返回。

套路1与套路3是固定的套路,基本不会有变。

套路2则有不少灵活的实现方式,只要保证只实例化一次就是能够的。

OK,那我开始撸代码。

五、代码

一、饿汉模式

package com.fanqiekt.singleton;

/**
 * 饿汉单例模式
 *
 * @author 番茄课堂-懒人
 */
public class EHanSingleton {

	private static EHanSingleton sInstance = new EHanSingleton();

	//私有化空构造方法
	private EHanSingleton() {}

	//静态方法返回单例类对象
	public static EHanSingleton getInstance() {
		return sInstance;
	}

	//其余业务方法
	public void otherMethods(){
		System.out.println("饿汉模式的其余方法");
	}
}
复制代码

套路1:私有化空构造方法。

套路2:自行实例化,保证明例化在内存中只存在一份

实现方式:静态实例变量的初始化

实现原理:类加载时就会初始化单例对象,而且只初始化一次。

套路3:提供公有静态getInstance()方法,并将单一的实例返回。

为何叫饿汉?

由于饿汉很饿,须要尽早初始化来喂饱本身。

从线程安全,优缺点总结一下。

线程安全:利用类加载器的机制,确定是线程安全的。

为何这么说呢?

ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。

优势:类加载时会初始化单例对象,首次调用速度变快。

缺点:类加载时会初始化单例对象,容易产生垃圾。

二、懒汉模式

package com.fanqiekt.singleton;

/**
 * 懒汉模式
 *
 * @author 番茄课堂-懒人
 */
public class LazySingleton {

	private static LazySingleton sInstance;

	//私有化空构造方法
	private LazySingleton() {}

	//静态方法返回单例类对象
	public static LazySingleton getInstance() {
		//懒加载
		if(sInstance == null) {
			sInstance = new LazySingleton();
		}
		return sInstance;
	}

	//其余业务方法
	public void otherMethods(){
		System.out.println("懒汉模式的其余方法");
	}
}
复制代码

套路1:私有化空构造方法。

套路2:自行实例化,保证明例化在内存中只存在一份

实现方式:getInstance()里进行实例判空

实现原理:为空则建立实例;不为空,则直接返回实例。

套路3:提供公有静态getInstance()方法,并将单一的实例返回。

为何叫懒汉?

由于懒汉懒惰,懒得初始化,用到了才开始初始化。

线程安全吗?

很明显,不是线程安全的,由于getInstance()方法没有作任何的同步处理。

怎么办?

给getInstance()加锁。

//静态方法返回单例类对象,加锁
	public static synchronized LazySingleton getInstance() {
		//懒加载
		if(sInstance == null) {
			sInstance = new LazySingleton();
		}
		return sInstance;
	}
复制代码

这样就变成线程安全的懒汉模式了。

懒汉模式有什么优缺点呢?

优势:第一次使用时才会初始化,节省资源

缺点:第一次使用时须要进行初始化,因此会变慢。给getInstance()加锁后,getInstance()调用也会变慢。

那有没有办法能够去掉getInstance()锁后还线程安全呢?

三、DCL

package com.fanqiekt.singleton;

/**
 * Double Check Lock 单例
 *
 * @author 番茄课堂-懒人
 */
public class DCLSingleton {

	private static DCLSingleton sInstance;

	//私有化空构造方法
	private DCLSingleton() {}

	//静态方法返回单例类对象
	public static DCLSingleton getInstance() {
		//两次判空
		if(sInstance == null) {
			synchronized(DCLSingleton.class) {
				if(sInstance == null) {
					sInstance = new DCLSingleton();
					return sInstance;
				}
			}
		}
		return sInstance;
	}

	//其余业务方法
	public void otherMethods(){
		System.out.println("DCL模式的其余方法");
	}
}
复制代码

与懒汉模式的区别在于:

去掉getInstance()方法上的锁,在方法内部实例为空后再进行加锁。

好处:只有当实例没有初始化的状况下才会同步锁,避免了给getInstance()整个方法加锁的状况。

dcl的全称是Double Check Lock,双重检查锁。所谓的双重检查就是两次判空。

为何要进行第二次判空,这不是脱裤子放屁,画蛇添足嘛。

可能以为它只是个屁,但实际上是窜稀,因此,脱裤子也是有必要的。

有这样一种状况,线程一、2同时判断第一次为空,在加锁的地方的阻塞了,若是没有第二次判空,那么线程1执行完毕后线程2就会再次执行,这样就初始化了两次,就存在问题了。

两次判空后,DCL就安全多了,通常不会存在问题。但当并发量特别大的时候,仍是会存在风险的。

在哪里呢?

sInstance = new DCLSingleton()这里。

是否是很奇怪,这句很普通的建立实例的语句怎么会有风险。

状况是这样的:

sInstance = new DCLSingleton()并非一个原子操做,它转换成了多条汇编指令,大体作了3件事情:

第一步:分配内存。

第二步:调用构造方法初始化。

第三步:将sInstanc对象指向分配空间。

因为Java编译器容许处理器乱序执行,因此这三步顺序不定,若是依次执行确定没问题,但若是执行完第一步和第三步后,其余的线程使用sInstanc就会报错。

那如何解决呢?

这里就须要用到关键字volatile了。

volatile有什么用呢?

第一个:实现可见性。

什么意思呢?

在当前的Java内存模型下,线程能够把变量保存在本地内存(好比机器的寄存器)中,而不是直接在主存中进行读写。

这就可能形成一个线程在主存中修改了一个变量的值,而另一个线程还继续使用它在寄存器中的变量值的拷贝,形成数据的不一致。

volatile在这个时候就派上用场了。

读volatile:每当子线程某一语句要用到volatile变量时,都会从主线程从新拷贝一份,这样就保证子线程的会跟主线程的一致。

写volatile: 每当子线程某一语句要写volatile变量时,都会在读完后同步到主线程去,这样就保证主线程的变量及时更新。

第二个:防止处理器乱序执行。

volatile变量初始化的时候,就只能第一步、第二步、第三步这样的顺序执行了。

因此咱们能够把sInstance的变量声明的代码更改下。

private volatile static DCLSingleton sInstance;
复制代码

不过,因为使用volatile屏蔽掉了JVM中必要的代码优化,因此在效率上比较低,所以必定在必要时才使用此关键字。

感受实现起来有点复杂,那有没有同样优秀还更简单点的单例模式?

四、静态内部类

package com.fanqiekt.singleton;

/**
 * 静态内部类单例模式
 *
 * @author 番茄课堂-懒人
 */
public class StaticSingleton {

	//私有静态单例对象
	private StaticSingleton() {}

	//静态方法返回单例类对象
	public static StaticSingleton getInstance() {
		return SingleHolder.INSTANCE;
	}

	//单例类中存在一个静态内部类
	private static class SingleHolder {
		//静态类中存在静态单例声明与初始化
		private static final StaticSingleton INSTANCE = new StaticSingleton();
	}

	//其余业务方法
	public void otherMethods(){
		System.out.println("静态内部类的其余方法");
	}
}
复制代码

套路1:私有化空构造方法。

套路2:自行实例化,保证明例化在内存中只存在一份

实现方式:声明一个静态内部类,静态内部类中有个单例对象的静态实例,getInstance()返回静态内部类的静态单例对象

实现原理:内部类不会在其外部类被加载的时候被加载,只有当内部类被使用的时候才会被使用。这样就避免了类加载的时候就被初始化,属于懒加载。

静态内部类中的静态变量是经过类加载器初始化的,也就是在内存中是惟一的,保证了单例。

线程安全:利用了类加载器的机制,肯线程安全

静态内部类简单,线程安全,懒加载,因此,强烈推荐

还有一个你们可能想象不到的实现方式,那就是枚举。

五、枚举

package com.fanqiekt.singleton;

/**
 * 枚举单例模式
 *
 * @Author: 番茄课堂-懒人
 */
public enum EnumSingleton {
    INSTANCE;

    //其余业务方法
    public void otherMethods(){
        System.out.println("枚举模式的其余方法");
    }
}
复制代码

枚举的特色:

保证只有一个实例。

线程安全。

自由序列化。

能够说枚举就是一个天生的单例,并且还能够自由序列化,反序列化后也是单例的。

而上边几种单例方式反序列化后是会从新再生成对象的,这就是枚举的强大之处。 那枚举的原理是什么呢?

咱们能够看一下生成的枚举反编译一下,我在这里只粘贴下核心部分。

public final class EnumSingleton extends Enum{
    private EnumSingleton(){}

    static {
        INSTANCE = new EnumSingleton();
    }
}
复制代码

Enum就是一个普通的类,它继承自java.lang.Enum类。因此,枚举具备类的全部功能。

他的实现方式优势相似于饿汉模式。

并且,代码还作了一些其余的事情,例如:重写了readResolve方法并将单一实例返回,所以反序列化也会返回同一个实例。

六、容器

package com.fanqiekt.singleton;

import java.util.HashMap;
import java.util.Map;

/**
 * 容器单例模式
 *
 * @Author: 番茄课堂-懒人
 */
public class SingletonManager {

    private static Map<String, Object> objectMap = new HashMap<>();

    //私有化空构造方法
    private SingletonManager(){}

    //将单例的对象注册到容器中
    public static void registerService(String key, Object instance){
        if(!objectMap.containsKey(key)){
            objectMap.put(key, instance);
        }
    }

    //从容器中得到单例对象
    public static Object getService(String key){
        return objectMap.get(key);
    }
}

复制代码

实现方式:一个静态的Map,一个将对象放到map的方法,一个获取map中对象的方法

实现原理:根据key存对象,若是map中已经存在key,则不放入map;不存在key,则放入map,这样能够保证每一个key对应的对象为单一实例。

容器单例的最大好处是,能够管理多个单例。

Android源码中就用到了这种方式,经过Context获取系统级别的服务(context.getSystemService(key))。

六、END

单例模式实现的方式虽然有不少,但都是为了让某一个类只有一个实例

今天就先说到这里,下次是建造者模式,感谢你们。

相关文章
相关标签/搜索