单例模式(Singleton)
- 下面是参考博文,感谢!!!
- 这篇博客算是我真正开始写博客的开篇吧!
- 之前太菜啦~,写的博文都已经删掉啦,自己都看不下去了,哈哈
- 如果有什么不对的,还希望各位大神指出,我会非常感激的!!!
- 希望能够帮助到正在学习单例模式的你~~
- 本文代码_Github地址
什么是单例模式
- 就是在整个程序周期中,某个类(单例类)只有一个实例存在,而且这个类提供了一个得到这个实例的静态方法
- 注意:Application类就是一个单例模式,在整个程序中只有一个实例;就是说,你在一个活动中拿到Application这个实例后,对它进行更改,那么,其他活动拿到的实例也会被改变
单例模式的特点
- 单例类的 构造方法是private的,意味着在这个类之外,不能通过构造方法来创建实例
- 单例类必须 自己创建自己的唯一实例
- 并给其他对象提供这个实例(通过静态方法的形式)
- 单例一般命名为
instance
两个需要了解的基本概念
简单理解——类加载机制
- 类加载机制这个东西涉及到了JVM方面的知识,所以,对于本文的单例模式来说,有个了解进行啦~
- 类的加载过程分为五个阶段:加载 → 连接(验证 →准备 → 解析) → 初始化
- 我们这里就了解一下:类什么时候进行初始化?
- 在Java代码中,当类被使用的时候,就进行了初始化
- 创建一个类的实例
- 访问类中的属性、方法,也包括反射
- 初始化一个类的子类 (首先会先初始化它的父类)
- JVM启动时标明的启动类(就是文件名和类名相同的那个类)
简单理解——延迟加载机制
- 所谓延迟加载,简单的讲,就是当我们真正需要一个数据的时候,才会去创建这个数据
- 联想到单例模式中,就是当我们需要一个类的单例的时候,才会去创建这个单例
单例模式的各种写法
- 总结下来,一共有 6 种:饿汉式、懒汉式、双重效验锁、静态内部类、缓存、枚举
- 缓存那个方式这里就不做讲解啦,就是通过
map
集合来实现的,有兴趣的小伙伴自己去了解啦~
- 缓存那个方式这里就不做讲解啦,就是通过
- 特点
- 饿汉模式没有实现延迟加载,其他都实现了
- 饿汉模式、静态内部类都是类加载机制,天生无多线程问题
饿汉式
- 最简单的单例模式!!
- 顾名思义,就是太"饿"了,所以单例类被加载的时候就创建好了单例
- 线程安全:天生线程安全
- 为什么呢?因为这个特殊的类加载机制呀!!!
- 一个类在整个声明周期中,只会被加载一次,而这唯一的一次就已经将单例创建好了
- 无论有几个线程,每次拿到的单例也将会是这仅有的一个!
- 适用于:单例 占用内存比较小,在 初始化时 就会 被用到 的情况
- 特点: 1. 单例类加载的时候就会创建单例对象 (类加载机制); 2. 天生线程安全
- 缺点:由于其特殊的类加载机制,如果没有用到这个单例类的话,就会 浪费掉内存
- 优点:类加载机制,保证天生线程安全,避免了多线程同步问题
/**
* @author: Richard
* @date: 2019/7/18
* @describe: 单例类_饿汉式,在类加载时就创建了实例;
*/
public class MySingletonTest_1 {
//在类加载时就创建了实例;
private static MySingletonTest_1 instance = new MySingletonTest_1();
//构造方法是private的
private MySingletonTest_1() {
}
//提供一个静态方法,返回单例
public static MySingletonTest_1 getInstance() {
return instance;
}
}
懒汉式
- 顾名思义,比较“懒”,所以在要使用的时候才会去加载单例实例,这也就是延迟加载
- 线程安全:不安全
- 因为这个延迟加载,单例的初始化是在提供单例的方法中进行的,当有多个线程去调用提供单例的方法的时候,会多次创建实例
- 解决方法:给提供单例的方法上加上锁,使线程一个一个的进行到方法内部
- 适用于:单例 使用的次数少,并且创建的单例 占用的资源较多 的情况
- 特点:1. 就是延迟加载,在需要的时候才会去加载单例;2. 实现单例通过 判空 操作来实现的
- 缺点:多个线程可能会 并发的调用 类中提供单例的方法,导致存在多个实例,出现 多线程同步问题
- 优点:单例是在要 需要的时侯才会去创建,节约资源
/**
* @author: Richard
* @date: 2019/7/19
* @describe: 单例模式_懒汉式,这是改良的线程安全的模式,一般的就去掉锁
*/
public class MySingletonTest_2 {
//开始并没有实例化
private static MySingletonTest_2 instance_2 = null;
//构造方法私有
private MySingletonTest_2() {
}
//提供一个静态方法,获取单例实例,在方法上加上锁,保证线程安全
//synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了
public static synchronized MySingletonTest_2 getInstance() {
//判断是否为空,从而控制实例,数量
if (instance_2 == null) {
instance_2 = new MySingletonTest_2();
}
return instance_2;
}
}
双重效验锁
-
线程安全:安全
- 因为加了 线程同步锁 和 判空操作,就避免了 线程并发问题
- 这个锁的位置与 懒汉式的改良版 不同,比较特殊,下面进行讲解
-
适用于: 推荐
-
特点:1. 实现了延迟加载;2. 进行了两次判控操作,并且锁住了类,解决了线程并发问题;3. 还有改良版的 volatile 关键字的使用
-
缺点:(了解就行,涉及到的东西太过深入了)
- 双检锁隐患:
- 这个问题的关键就在于由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。
- 在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。
- 此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错
- 双检锁隐患:
-
优点:解决了线程并发问题,同时还解决了执行效率问题
不过还好在JDK1.5及之后版本增加了volatile关键字,解决了双重校验锁会失效的问题
volatile变量可以保证下一个读取操作会在前一个写操作之后发生
/**
* @author: Richard
* @date: 2019/7/19
* @describe: 单例模式_双重效验锁
*/
public class MySingletonTest_3 {
//开始并没有实例化
//volatile变量可以保证下一个读取操作会在前一个写操作之后发生
private static volatile MySingletonTest_3 instance_3 = null;
//构造方法为私有的
private MySingletonTest_3() {
}
//提供一个静态方法,获取实例
public static MySingletonTest_3 getInstance() {
//第一次判断:是否存在实例
if (instance_3 == null) {
//加一个锁,防止线程并发调用
synchronized (MySingletonTest_3.class) {
//第二次判断:当有两个线程都执行到了第一次判断,都认为instance_3是null时,
// 当一个线程执行完了同步代码块创建实例后,后面的线程还是会执行一次,又创建了一个实例;
if (instance_3 == null) {
instance_3 = new MySingletonTest_3();
}
}
}
return instance_3;
}
}
静态内部类
- 线程安全:安全
- 和饿汉式的线程安全类似,都是类加载机制,一个类在整个生命周期中只会被加载一次,当调用方法获取这个单例的时候,内部类将会被加载,且只有这一次
- 适用于:推荐
- 特点:1. 使用 类加载机制 来保证只创建一个实例,不存在多线程并发的问题
- 缺点:第一次加载时反应不够快
- 优点:因为 单例实例在内部类中,因此只要 不使用 内部类可以同时保证 延迟加载 和 线程安全
/**
* @author: Richard
* @date: 2019/7/19
* @describe: 单例模式_静态内部类
*/
public class MySingletonTest_4 {
//私有构造方法
private MySingletonTest_4() {
}
//静态内部类
private static class MySinleton {
private static MySingletonTest_4 mySingletonTest_4 = new MySingletonTest_4();
}
//静态方法,访问内部类属性,获取单例实例
public static MySingletonTest_4 getInstance() {
return MySinleton.mySingletonTest_4;
}
}
枚举
- 虽然枚举可以实现单例模式,但是在实际的开发中,并不推荐使用 枚举来实现单例模式
- 线程安全:安全
- 这个我在想,应该和类加载机制类似的吧,枚举值只会被实例化一次,所以,在整个生命周期中,只会有这一次创建的实例
- 适用于:单例 占用内存比较小,在 初始化时 就会 被用到 的情况
- 特点:通过枚举实例只能被实例化一次的特性,实现了单例实例的唯一性
- 缺点:我认为,代码太复杂啦~
- 优点: 在我们访问 枚举实例 时会执行构造方法,同时每个枚举实例都是默认static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。也就是说,因为enum中的实例被保证 只会被实例化一次,所以我们的INSTANCE也被保证实例化一次。
/**
* @author: Richard
* @date: 2019/7/19
* @describe: 单例模式_枚举
*/
//单例类
public class MySingletonTest_5 {
//私有构造方法
private MySingletonTest_5() {};
//提供一个静态方法,获取单例实例
public static MySingletonTest_5 getInstance() {
return Singlrton.INSTANCE.getMySingletonTest_5();
}
private static enum Singlrton {
//枚举值:是static、final类型的、只能被实例化一次
INSTANCE;
MySingletonTest_5 mySingletonTest_5 = null;
//枚举的私有构造方法里面进行实例化
//JVM会保证此方法绝对只调用一次
private Singlrton() {
mySingletonTest_5 = new MySingletonTest_5();
}
//提供一个静态方法,返回实例
public MySingletonTest_5 getMySingletonTest_5() {
return mySingletonTest_5;
}
}
}
应用
- OKHttp的 OkHttpClient对象可以使用双重验证锁来实现单例模式
- EventBus的
getDefault()
方法得到的实例就是一个单例,我以前还傻傻的自己写一个EventBus的单例呢,后面点进去看了看源码才发现的,哈哈,真是太蠢了