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

栏目: 后端 · 发布时间: 7年前

内容简介:首先,请您欣赏闲来无事听听曲,知识已填脑中去;

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

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

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

试听请点击这里

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

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

番茄课堂,学习也要酷。

2、定义

Java 设计模式中,单例模式相对来说算是比较简单的一种创建型模式。

什么是创建型模式?

创建型模式是 设计模式 的一种分类。

设计模式可以分为三类:创建型模式、结构型模式、行为型模式。

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

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

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

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

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

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

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

3、特性

从定义中,我们可以分析出一些特性来:

单例类只能有一个实例。

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

单例类必须自行创建自己的唯一的实例。

自行实例化。

单例类必须给所有其他对象提供这一实例。 向整个系统提供这个实例。

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

4、套路

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

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

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

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

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

套路2则有很多灵活的实现方式,只要保证只实例化一次就是可以的。

OK,那我开始撸代码。

5、代码

1、饿汉模式

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关键字。

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

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

2、懒汉模式

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()锁后还线程安全呢?

3、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, 双重检查 锁。所谓的双重检查就是两次判空。

为什么要进行第二次判空,这不是脱裤子放屁,多此一举嘛。

可能觉得它只是个屁,但其实是窜稀,所以,脱裤子也是有必要的。

有这样一种情况,线程1、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中必要的代码优化,所以在效率上比较低,因此一定在必要时才使用此关键字。

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

4、静态内部类

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()返回静态内部类的静态单例对象

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

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

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

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

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

5、枚举

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方法并将单一实例返回,因此反序列化也会返回同一个实例。

6、容器

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))。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Definitive Guide to MongoDB

The Definitive Guide to MongoDB

Peter Membrey、Wouter Thielen / Apress / 2010-08-26 / USD 44.99

MongoDB, a cross-platform NoSQL database, is the fastest-growing new database in the world. MongoDB provides a rich document orientated structure with dynamic queries that you’ll recognize from RDMBS ......一起来看看 《The Definitive Guide to MongoDB》 这本书的介绍吧!

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

html转js在线工具
html转js在线工具

html转js在线工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具