好心封装工具类,结果被领导怒怼:“会不会写代码!”
因为绝大部分开发者平时对单例模式的认识,可能仅仅停留在“会用”的阶段。为什么会有这个模式?为什么要用这个模式?在哪里用单例模式最合适?乱用了会有什么负面影响?
这些可能大多数人都一知半解。今天就让我们大家一起来扒光单例模式的外衣,有深度的认识一下单例模式。
通过这篇文章你能学到什么
单例模式的定义
单例模式在Android源码中的应用
单例模式的九种写法以及优劣对比
单例模式的使用场景
单例模式存在的缺点
单例模式的定义
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。 单例模式是一种对象创建型模式。
1、实例唯一性
2、自行创建
3、全局访问
说了这么多了,还不知道单例模式到底啥样呢?接下来我们一起来着手设计这个“国王”的单例类。我们先看一下单例模式的类图:
单例模式的九种写法
一、饿汉式(静态常量)
/**
* 饿汉式(静态常量)
*/
class King {
private static final King kingInstance = new King();
static King getInstance() {
return kingInstance;
}
private King() {
}
}
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
二、饿汉式(静态代码块)
/**
* 饿汉式(静态代码块)
*/
class King {
private static King kingInstance;
static {
kingInstance = new King();
}
private King() {
}
public static King getKingInstance() {
return kingInstance;
}
}
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
三、懒汉式(线程不安全)
/**
* 懒汉式(线程不安全)
*/
public class King {
private static King kingInstance;
private King() {
}
public static King getKingInstance() {
if (kingInstance == null) {
kingInstance = new King();
}
return kingInstance;
}
}
优点:懒加载,只有使用的时候才会加载。
缺点:但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
四、懒汉式(线程安全)
/**
* 懒汉式(线程安全,同步方法)
*/
public class King {
private static King kingInstance;
private King() {
}
public static synchronized King getKingInstance() {
if (kingInstance == null) {
kingInstance = new King();
}
return kingInstance;
}
}
优点:懒加载,只有使用的时候才会加载,获取单例方法加了同步锁,保障线程安全。
缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。
五、懒汉式(线程安全,同步代码块)
/**
* 懒汉式(线程安全,同步代码块)
*/
public class King {
private static King kingInstance;
private King() {
}
public static King getKingInstance() {
if (kingInstance == null) {
synchronized (King.class) {
kingInstance = new King();
}
}
return kingInstance;
}
}
优点:改进了第四种效率低的问题。
缺点:不能完全保证单例,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。
六、双重检查(DCL)
/**
* 双重检查(DCL)
*/
public class King {
private static volatile King kingInstance;
private King() {
}
public static King getKingInstance() {
if (kingInstance == null) {
synchronized (King.class) {
if (kingInstance == null){
kingInstance = new King();
}
}
}
return kingInstance;
}
}
优点:线程安全;延迟加载;效率较高。
缺点:JDK < 1.5 的时候不可用
不可用原因:由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,而JDK 1.5 以及之后的版本都修复了这个问题。(面试装逼用,谨记!!!)
七、静态内部类
/**
* 静态内部类
*/
public class King {
private King() {
}
private static class KingInstance{
private static final King KINGINSTANCE = new King();
}
public static King getInstance(){
return KingInstance.KINGINSTANCE;
}
}
优点:避免了线程不安全,延迟加载,效率高。
缺点:暂无,最推荐使用。
特点:这种方式跟饿汉式方式采用的机制类似,但又有不同。
两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
八、枚举
/**
* 枚举
*/
public enum King {
KINGINSTANCE;
}
优点:不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。
缺点:JDK 1.5之后才能使用。
九、容器类管理
/**
* 使用容器实现单例模式(可以用于管理单例,有兴趣的可以尝试一下)
* */
class InstanceManager {
private static Map<String, Object> objectMap = new HashMap<>();
private InstanceManager(){}
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);
}
}
/**
* 使用方式
* Dog类就不贴出来了
* 自己随便写个就行
* 可以运行一下看看 打印的地址是否一致
*/
class Test {
public static void main(String[] args) {
InstanceManager .registerService("dog", new Dog());
Dog dog = (Dog) InstanceManager .getService("dog");
Dog dog2 = (Dog) InstanceManager .getService("dog");
Dog dog3 = (Dog) InstanceManager .getService("dog");
Dog dog4 = (Dog) InstanceManager .getService("dog");
System.out.println(dog);
System.out.println(dog2);
System.out.println(dog3);
System.out.println(dog4);
}
}
优点:在程序的初始,将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作, 降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。
缺点:不常用,有些麻烦
九种写法的优劣对比
名称 | 优点 | 缺点 | 是否推荐 |
---|---|---|---|
饿汉式(静态常量) | 写法简单,类装载时完成实例化。避免了线程同步 | 急切实例化, 容易内存泄漏 | 可用 |
饿汉式(静态代码块) | 同上 | 同上 | 可用 |
懒汉式(线程不安全) | 懒加载 | 只能在单线程下使用 | 多线程不可用 |
懒汉式(线程安全,同步方法) | 懒加载,方法同步锁 | 效率低 | 不推荐用 |
懒汉式(线程安全,同步代码块) | 同上,同时改变效率低问题 | 不能完全保证单例 | 不推荐用 |
双重检查(DCL)① | 线程安全;延迟加载;效率较高 | JDK < 1.5 的时候不可用 | JDK >1.5 推荐用 |
静态内部类② | 线程安全,延迟加载,效率高。 | 暂无发现 | 墙裂推荐 |
枚举 | 写法简单,防止反序列化 | JDK 1.5之后才能使用 | JDK >1.5 推荐用 |
容器实现 | 可用管理多个单例对象 | 不常用,多创建了一个Map | 可用 |
①:不可用原因:由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,而JDK 1.5 以及之后的版本都修复了这个问题。(面试装逼用,谨记!!!)
单例模式在Android源码中的应用
1、EventBus中获取实例:
private static volatile EventBus defaultInstance; public static EventBus getDefault() {
if (defaultInstance == null) {
synchronized (EventBus.class) {
if (defaultInstance == null) {
defaultInstance = new EventBus();
}
}
}
return defaultInstance;
}
2、InputMethodManager获取实例
static InputMethodManager sInstance;
public static InputMethodManager getInstance() {
synchronized (InputMethodManager.class) {
if (sInstance == null) {
IBinder b = ServiceManager.getService(Context.INPUT_METHOD_SERVICE);
IInputMethodManager service = IInputMethodManager.Stub.asInterface(b);
sInstance = new InputMethodManager(service, Looper.getMainLooper());
}
return sInstance;
}
}
单例模式在日常开发中的应用场景
1、图片加载
2、网络请求
3、工具类封装
Quote from 《 Use your singletons wisely 》 Will every application use this class exactly the same way? (keyword: exactly)
Will every application ever need only one instance of this class? (keyword: ever & one)
Should the clients of this class be unaware of the application they are part of?每一个应用(组件/模块)是否以完全一致的方式来使用这个类?
每一个应用(组件/模块)是否真的只需要这个类的一个实例呢?
对于这个类的客户端类来说,对他们自己是应用中的一部分这件事是否应该保持毫无察觉的状态呢?
单例模式的优点
单例模式的缺点
总结
码个蛋专属活动:打卡入口
为什么要做这个活动?
关注「码个蛋」,一起打卡成长