码迷,mamicode.com
首页 > 其他好文 > 详细

设计模式之单例模式

时间:2019-10-21 13:14:39      阅读:95      评论:0      收藏:0      [点我收藏+]

标签:做了   系统   可见性   一个   总结   过程   targe   完美   问题   

参考资料:
老司机来教你单例的正确姿势
《Android源码设计模式解析与实战》

单例模式可以说是应用最广泛的模式了, 面试也经常被问到, 经常会被要求能够手写单例, 所以我今天也来总结一下

单例模式的定义

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

单例模式的使用场景

确保某个类有且只有一个对象的场景, 避免产生多个对象消耗过多的资源, 或者某种类型的对象只应该有且只有一个.例如, 创建一个对象需要消耗的资源过多, 如要访问IO和数据库等资源

单例的UML类图

技术图片
插点题外话说一下怎么看UML类图, 例如这里的Singleton矩形框就代表一个类, 第一层显示类的名称, 抽象类用斜体表示; 第二层是类的特性, 通常是字段和属性; 第三层是类的操作, 通常是方法. ‘+’表示public, ‘-‘表示private, ‘#’表示protected, 例如这里的getInstance()方法是public的, 构造方法是private的.
与类图有区别的是接口图, 顶端有<<interface>>显示
实现单例模式有一下几个关键点:

  1. 构造函数不对外开放, 一般为private, 是的客户端不能通过new的形式手动构造单例类的对象
  2. 通过一个静态方法或者枚举返回单例类对象
  3. 在多线程环境中也需要确保单例类的对象有且只有一个
  4. 确保单例类对象在反序列化时不会重新构建对象

    最简单的单例之饿汉式

    1
    2
    3
    4
    5
    6
    7
    8
    public class  {
    private static Singleton INSTANCE = new Singleton();
    private () {}

    public static Singleton getInstance() {
    return INSTANCE;
    }
    }

这种单例的写法最简单,在类加载的时候就创建了单例对象, 并且保证了Sigleton对象的唯一性,以后不再改变,所以天生是线程安全的。但是缺点是一旦类被加载,单例就会初始化,没有实现懒加载。而且有时候这个对象的构造方法需要一个参数比如context的时候这种方法就不太适合了.

懒汉式

1
2
3
4
5
6
7
8
9
10
11
public class  {
private static Singleton INSTANCE;
private () {}

public static Singleton getInstance() {
if(INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
}

这段代码虽然实现了懒加载, 但是多线程环境下会出现线程安全的问题, 当多个线程调用getInstance()方法时, 可能会创建多个实例, 所以我们改成这样: 给getInstance()方法加个synchronized关键字.使用synchronized关键字修饰一个方法, 该方法中所有的代码都是同步的.静态的synchronized方法它的锁对象就是该类的字节码对象

1
2
3
4
5
6
public static synchronized Singleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
return INSTANCE;
}

但这样又出现了性能问题,简单粗暴的同步整个方法,导致同一时间内只有一个线程能够调用getInstance方法。
再次优化代码, 仅仅对初始化部分的代码进行同步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class  {
private static Singleton INSTANCE;
private Singleton() {}

public static Singleton getInstance() {
if(INSTANCE == null) {
synchronized(Singleton.class) {
if(INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}

执行两次检测很有必要:当多线程调用时,如果多个线程同时执行完了第一次检查,其中第一个进入同步代码块的线程创建了实例,后面的线程因第二次检测不会创建新实例。这种写法还有一个名字叫做Double Check Locking(双重加锁),简称DCL.看似完美, 但是这段代码还是有问题.
INSTANCE = new Singleton();
这个语句时其实做了三步工作,

  1. 给 INSTANCE 分配内存
  2. 调用构造函数初始化成员字段
  3. 将INSTANCE对象指向分配的内存空间(INSTANCE 不再为 null),由于 Java 虚拟机是乱序执行的,所以执行顺序可能是1-2-3,也有可能是1-3-2.
    如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错, 严重会导致程序崩溃.
    为了解决这个问题, 可以这样:
1
2
3
4
5
6
7大专栏  设计模式之单例模式r/>8
9
10
11
12
13
14
15
16
public class Singleton {
private volatile static Singleton INSTANCE;
private Singleton (){}

public static Singleton getSingleton() {
if (INSTANCE == null) {
synchronized (Singleton.class) {
if (INSTANCE == null) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}

}

使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说, 在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的 “后面” 是时间上的先后顺序)

到了这里就不得不说说, volatile这个关键字了, 也是个面试常客呢.我经常看到的说法是这样的, volatile可以保证在一个线程的工作内存中修改了该变量的值,该变量的值立即能回显到主内存中,从而保证所有的线程看到这个变量的值是一致的.参考这篇 面试官最爱的 volatile 关键字Java 之 volatile 详解我来简单谈谈.
被 volatile 修饰的共享变量,就具有了以下两点特性:

  1. 保证了不同线程对该变量操作的内存可见性;
  2. 禁止指令重排序

    可见性

    可见性的含义是指:一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
    i = 8
    如果 i 用 volatile 修饰的话,当有一个线程在主存中读取 i, 并在自己的工作内存中进行修改的时候,修改后的值会立即强制同步到主存中,并且其他线程中这个值的缓存也都无效。相比之下普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

    禁止重排序

    重排序的含义是:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
    volatile 禁止指令重排序的含义:
  • 当程序执行到 volatile 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行
  • 在进行指令优化时,不能将在对 volatile 变量的读操作或者写操作的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。

举例说明:

1
2
3
4
5
x = 10;          //语句1
y = 3; //语句2
volflag = true; //语句3
x= 5; //语句4
y = 9; //语句5

这个例子中,由于 volflag 被 volatile 修饰,所以语句 3 不会被重排到语句 1、语句 2 前面,也不会被重排到语句 4、语句 5 的后面,但语句 1、2 和语句 4、5 的顺序是不能保证的。
另外 volatile 可以保证在执行到语句 3 的时候语句 1、2 是执行完毕的,语句 4、5 是没有执行的,并且语句 1、2 的执行结果是对语句 4、5 是可见的
这里我只是参考了别人的文章关于 Java volatile 关键字简单总结了一下, 其实这个volatile深入起来还有很多东西可以说, 以后有时间的话我开一篇博客详细总结一下.

静态内部类单例模式

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private static Singleton INSTANCE;
private Singleton(){}

public static Singleton getInstance() {
return SingletonHolder.instance;
}

private static class SingletonHolder {
private static final Singleton instance = new Singleton();
}
}

这是我平时比较喜欢用的单例, 使用内部类来维护单例的实例.当 Singleton 被加载时,其内部类并不会被初始化,故可以确保当 Singleton 类被载入 JVM 时,不会初始化单例类。只有 getInstance() 方法调用时,才会初始化 instance。同时,由于实例的建立是时在类加载时完成,故天生对多线程友好,getInstance() 方法也无需使用同步关键字。

最佳实践单例之枚举

1
2
3
4
5
6
7
public enum Singleton {
INSTANCE;

public void doSomething() {

}
}

使用
Singleton.INSTANCE.doSomething();
《Effective Java》一书推荐此方法,说 “单元素的枚举类型已经成为实现 Singleton 的最佳方法”。不过 Android 使用 enum 之后的 dex 大小增加很多,运行时还会产生额外的内存占用,因此官方强烈建议不要在 Android 程序里面使用到enum

总结

在实际开发过程中, 我比较喜欢用内部类的单例模式, 其次是安全的DCL模式, 再次是饿汉式, 其他的基本不会使用.

设计模式之单例模式

标签:做了   系统   可见性   一个   总结   过程   targe   完美   问题   

原文地址:https://www.cnblogs.com/dajunjun/p/11712830.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!