单例模式是Singleton.md

概述:

单例模式用来保证一个类只有一个实例存在,避免对象的重复创建,减少创建对象的时间消耗,如果一个对象可以贯穿整个应用程序,起到统一控制管理的作用,例如线程池,那么单例模式是一个不错的选择。

下面介绍单例模式的实现方法:

1.饿汉模式

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

类的构造方法定义为private,保证其他类不能实例化此类,然后提供了一个静态实例并返回给调用者。饿汉模式在类加载的时候就创建,优点是只在类加载的时候创建一次实例,多线程同时调用getInstance()方法不会出现线程安全问题,多个线程同时调用不会在内存中创建多个对象,只要调用这个方法就会立即返回对象,这是一种用空间换取时间的行为,但是缺点就是可能创建好之后在内存中不会被使用,造成了空间的浪费饿汉模式适合单例占用内存比较小,而且会被用到的概率比较大的情况,如果单例比较大,就使用下面的懒加载模式

2.懒汉模式

public class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton newInstance(){
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

这种模式是需要单例的时候采取创建,但是最大的缺点就是线程安全问题,如果多个线程同时调用getInstance()方法,那么就可能会创建多个实例


下面是加锁形式:

3.双重检查锁

public static Singleton getInstanceDC() {
    if (_instance == null) {                // Single Checked
        synchronized (Singleton.class) {
            if (_instance == null)         // Double checked
                _instance = new Singleton();            
        }
    }
    return _instance;
}

终于是理解了双重检查锁,这是在多线程并发访问的情况下解决问题的一种方法。下面就用自己的大白话说一下自己的理解,假如说有三个线程同时有这么一个语句:Singleton.getInstance(),在instace == null时,三个线程同时进入了第一层if,但是只有一个线程能够获得synchronized的锁,其他两个线程都要在外面等待,当变量获得锁的线程创建完单例类的实例后,变量_ instance也就有了引用的对象,值就不为null,这是另外两个等待的线程中,有一个获得了锁进入,一进去就看到判断条件_instance不为空,直接返回第一个线程创建好的对象,这就是两个if的理解。

第一个if语句的作用是避免非必要性加锁,如果instance为空,那么当前线程就参与到锁的竞争,如果其他线程正在占用锁,那么当前线程等待锁的释放,当前线程得到锁之后,发现instance引用已经指向了实际的对象,不为空,那么直接返回instance对象,避免的创建多个对象,如果当前线程进来的时候,instance不为空,那么就可以直接返回instance对象,不需要进行非必要性的加锁,如果没有这第一层if判断,那么不管instance对象是否为空,都需要参与锁的竞争。

双重检查锁失效问题:

这个博客终于将一直以来的疑惑解开了,这个失效问题是非常著名的,但是综合网上的各种说法来讲,这个问题在Java5之后得到了解决,这得助于volatile关键字,这也是一会要着重理解的。

instance = new Singleton();

这个语句在内存中是分为三个步骤的,

1.在堆内存开辟一块空间,

2.在堆内存中实例化也就是初始化Singleton里面的各个参数

3.返回引用给变量instance

由于jvm存在乱序执行功能,所以很可能 在2没有执行或者没有执行完时就执行了3,这是instance就已经非空了,当切换到另外一个线程时,也就是处于等待状态的其中一个,这个线程将instance直接拿来用,就出现了异常,需要使用volatile关键字来解决,volatile的主要作用就是两点,保证可见性和禁止指令重排,由于这篇文章主要还是将单例模式的,关于volatile关键字将在另外一篇文章中解释---volatile关键字的理解

4.静态内部类实现

CodeSheep说这是面试官最希望看到的形式,因为这其中涉及到了jvm装载的知识,面试官可以继续往下面问了,

public class SingleTon{
  private SingleTon(){}
  private static class SingleTonHoler{
     private static SingleTon INSTANCE = new SingleTon();
}
  public static SingleTon getInstance(){
    return SingleTonHoler.INSTANCE;
  }
}

外部类加载时不需要立即加载内部类,内部类SingleTonHoler不被加载就不会创建instance对象,当调用getInstance方法时,虚拟机才加载内部类,这时SingletonHoler在Singleton的运行时常量池里面,将符号引用替换为直接引用,内部类在创建instacne对象时,是如何保证线程安全的呢?这一点是虚拟机实现的,如果多个线程同时去初始化一个类,那么只有一个线程可以执行类的client方法,其他线程进行阻塞,即使是当前线程执行完client后,这些进行被唤醒也不会再对类进行初始化,一个类在一个类加载器中,只能初始化一次,而静态变量instance就是在类的初始化阶段进行赋值的,所有可以保证唯一。

这种内部类的方式既可以实现懒加载,又类似于饿汉模式,内部类直接创建出instance对象,可以保证线程安全。但是世界哪有完美的事情,使用静态内部类的方式,外部不能传入参数,这也是一个局限性。

5.枚举方式

public enum SingleTon{
  INSTANCE;
}

这是线程安全的,而且很精简。

Copyright: 采用 知识共享署名4.0 国际许可协议进行许可

Links: https://hadoo666.top/archives/单例模式是singletonmd