单例模式

单例模式

注意:

  • 1、单例类只能有一个实例。

  • 2、单例类必须自己创建自己的唯一实例。

  • 3、单例类必须给所有其他对象提供这一实例。

考查的核心知识点:

  1. 线程安全
  2. 内存模型
  3. 类加载机制

1.单例模式是什么?为什么需要单例模式?

单例模式 : 顾名思义, 就是在整个运行时域(runtime), 一个类只有一个实例对象.

为什么需要单例模式 : 有的类的创建和销毁对资源来说消耗不大, 比如String; 有的类就比较庞大和复杂. 如果频繁的创建和销毁这些对象, 并且这些对象是完全可以复用的情况下, 将会造成不必要的性能浪费.

例子:

创建一个数据库的链接对象, 只需用单例模式创建一次就OK.

2.如何实现单例模式?

多种写法,多种思维.

要实现单例模式, 主要考虑3点:

  1. 是否线程安全.
  2. 是否是懒加载.(Lazy loading)
  3. 是否能够通过反射破坏.

3.实例:

3.1 懒汉式:(线程不安全的)

public class Singleton {
    
    private Singleton() {
        // 构造器私有--1
    }
    // 初始化对象为null
    private static Singleton instance = null;
    // 通过getInstance()方法来使用Singleton对象
    public static Singleton getIntance() {
        // 判断instance是否被构造过.
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
    
}

1.外部无法 Singleton s = new Singleton();

懒加载:

实例对象是第一次被调用的时候才真正构建, 而不是程序一启动就构建好等你调用. 这种滞后的加载就是懒加载.

3.2 懒汉式:(线程安全的)

public class Singleton {
    public Singleton() {}
    public static Singleton instance = null;
    public static synchronized getIntance() {
        if(instance == null){
            Singleton s = new Singleton();
        }
        return instance;
    }
}

这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低. 每次获取对象是都要进行同步操作, 对性能影响非常大.

3.3 饿汉式:

public void Singleton {
    // 编译期构建
 	private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton getInstance() {
        return instance;
    }
}

描述:这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。

拓展:

对象在类中被定义为private static,通过getInstance(),通过java的classLoader机制保证了单例对象唯一。

3.4 双检锁(DCL,即 double-checked locking)

有没有即是线程安全, 又是懒加载的单例模式, 双检锁就出现了.

改造 懒汉式:(线程安全的)

public class Singleton {  
    private static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        
        // 调用时构建,if里有多个线程,a执行完后,b也执行.导致重复构建.
        synchronized (Singleton.class) {
            singleton = new Singleton();  
        }  
    }  
    return singleton;  
    }  
}

使用双if:

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static Singleton getSingleton() {
        if(singleton == null) {
            synchronized (Singleton.class) {
                if(singleton == null) {
                    singleton = new Singleton(); 
                }
            }  
        }
        return singleton;
    }
}
//还会有一个指令重排序问题.

为什么要使用volatile修饰?

虽然已经使用synchronized进行同步,但在创建singleton对象时,会有下面的伪代码:

memory=allocate(); // 1:分配对象的内存空间
ctorInstance();   // 2: 初始化对象
singleton=memory; // 3: 设置instance指向刚分配的内存地址

当线程A在执行上面伪代码时,2和3可能会发生重排序,因为重排序并不影响运行结果,还可以提升性能,所以JVM是允许的。

如果此时伪代码发生重排序,步骤变为1->3->2,线程A执行到第3步时,线程B调用getsingleton方法,在判断singleton==null时不为null,则返回singleton。但此时singleton并还没初始化完毕,线程B访问的将是个还没初始化完毕的对象。当声明对象的引用为volatile后,伪代码的2、3的重排序在多线程中将被禁止!

使用了volatile就能阻止作用在instance上的指令重排问题.

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

3.5 静态内部类

在程序启动是不会加载, 只有在第一调用时才会加载.

public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}

//线程安全的,懒加载.

描述:这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

3.6 枚举

不能通过反射破坏

无法满足懒加载

自动避免序列化/反序列化攻击

public enum Singleton {  
    INSTANCE;    
}
原文地址:https://www.cnblogs.com/gzp5608/p/13784113.html