学习笔记系列(1)——单例模式

297 阅读5分钟
前言

2020.11.28,今天是个平凡的周六加班日,也是我决定第一次在掘金发表文章的日子。从工作一年以来,我都会通过手写笔记、txt文本、备忘录或者markdown笔记的方式记录下自己的一些学习心得,因为自己还处于小菜鸡阶段也不好意思在平台公开发表博客,但最近受到同事启发,我想利用掘金这个平台发表自己的一些学习心得笔记,主要用来总结整理、后期回顾。
现在难免文笔稚嫩粗糙,而且笔记也都是以自己能够理解的语言文字方式表达出来用于自己总结梳理。如果有幸被大佬萌看到,非常希望各位大佬可以指出我的不足缺漏,并且可以指导我该往什么方向更深入的学习思考。
掘金——帮助开发者成长的社区,也希望自己能够从小菜鸡一点点进步成长。

单例模式

单例模式是设计模式中的创建型模式。单例其实算是最好理解的一种方式,我的学习思路是单例模式是什么(定义)——有什么用(用途)——实现方式(不同的实现方式及其各自的优缺点,缺点引发的问题-思考为什么并去寻找更好的优化方式)

基本概念

单例模式保证一个类仅有一个实例,并保证一个访问它的全局访问点。

优点:

  1. 在内存里只有一个实例,减少内存开销,提高系统性能。
  2. 保证一个全局访问点,实现了访问实例的可控性。
  3. 避免对资源的多重占用。 缺点:
  4. 顾名思义,单例并不适用于变化频繁的对象
  5. 实例化的对象长时间不使用可能会被垃圾回收导致对象状态丢失。
  6. 单例模式不可滥用,否则会带来一些负面影响(在我看来,更深入的学习了解也是为了融会贯通,能够写代码的时候将自己会的东西“用在刀刃上”,避免滥用,解决方案应给予最优解)

使用场景:

这里我就列出几个平时项目中容易用到的情况
1.多线程的线程池采用单例模式,方便线程之间互相通信,也可以更好的控制池中的线程(PS.以后单独出一篇多线程的学习笔记,记在小本本上QAQ)
2.数据库的连接池用的单例,可以节省打开或关闭数据库的效率损耗(之前有提到单例模式不可滥用,如果数据库连接池的共享对象过多,可能会导致数据库连接池溢出。)
3.Windows的任务管理器和回收站也都是单例模式哦。

实现方式

饿汉式

通过static静态初始化方式,在类第一次加载的时候实例就已经被创建出来(同时也避免了线程安全问题)。

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){

    }
    private static Singleton getInstance(){
        return instance;
    }
}

缺点:每次加载类都会实例化,会造成过多不必要的消耗。

静态内部类

可以解决饿汉式的缺点,只有在调用getInstance()时,才会装载SingletonHolder类,从而实例化instance。

public class Singleton {
    private static class Singletonholder{
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }

    private StaticInnerClassSingleton(){
    };
    public static final StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

懒汉式

不提前创建实例,在第一次被引用的时候再实例化。

普通懒汉式
public class Singleton {
    private static Singleton instance;
    private Singleton() {};
    private static Singleton getInstance() {
        if (instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

如果是在多线程的情况下,会引起线程安全问题。

线程安全的懒汉式
  • 在创建对象时加锁(双重校验锁)
  1. Synchorized关键字 可以在整个方法上加锁,但这样会效率很低,所以缩小锁的范围,所以我下面贴出在同步代码块中加锁的代码
public class Singleton {

    private static Singleton instance;
    private Singleton() {};
    private static Singleton getInstance() {
        if (instance == null){
            synchronized(Singleton.class){
                if(instance == null) {
                    instance = new Singleton();
                }
            }

        }
        return instance;
    }
}

其实这样也会出现问题。在多线程情况下,如果线程A发现变量没有初始化,它获取锁开始变量的初始化只完成了部分并没有完全完成时,变量已经更新并指向了部分初始化的变量。此时另一个线程B以为共享变量已经初始化完成所以直接返回变量,会导致程序崩溃。
2. volatile关键字
这里我简单说下自己对volatile的一些了解(后面等我学习理解了synchronized和volatile关键字的底层代码后会单独出一篇博客记录下)

volatile关键字修饰的变量,在寄存器中的值是不确定的,只能从主存中读取。
在多线程情况下,一个线程修改了一个变量的值后,其他的线程都立即可见,保证了不同线程对该变量的可见性。
volatile关键字修饰的变量禁止指令重排序。

volatile和synchronized区别

  1. volatile只能修饰变量(synchronized可以修饰变量、方法、类)
  2. volatile修饰的变量只保证了修改可见性(synchronized修饰的变量既保证了修改可见性,又保证了原子性)
  3. volatile修饰的变量不会被编译器优化(synchronized修饰的变量会被编译器优化)
  4. volatile修饰的变量不会造成线程阻塞(synchronized修饰的变量会造成线程阻塞)

下面贴出代码

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

注意:遇到序列化问题时,也会产生问题。
反射会破坏单例模式。简单说下:调用构造函数会破坏单例模式,因为构造方法只是private 修饰,防止外部类访问,但是反射方法访问,不受限制。
而序列化问题会破坏单例模式,是因为序列化过程中会通过反射调用无参的构造方法创建一个新的对象。 为了防止序列化的破坏,可以用readResolve解决,加入如下代码:

private Object readResolve() {
        return singleton;
    }
  • 此外,枚举类和双重校验锁使用final也可以实现线程安全的懒汉式单例。