单例模式

单例 (Singleton) 设计模式

参考单例模式

单例设计模式,就是采取一定的方法保证某个类只能存在一个对象实例

考虑到一个对象的创建和销毁需要较多的资源,可以采用单例模式只生成一个实例,从而减少系统的性能开销。

实现方式

要让类只能产生一个对象,我们首先必须将类的构造器的访问权限设置为private,这样,就不能用new操作符在类的外部产生类的对象了,但在类内部仍可以产生该类的对象。因为在类的外部无法通过构造器来创建对象,只能调用该类的某个静态方法以返回类内部创建的对象,静态方法只能访问类中的静态成员变量,所以,指向类内部产生的该类对象的变量也必须定义成静态的。

1.饿汉式-线程安全

代码实现

public class test {
    public static void main(String[] args) {
        //测试
        Singleton instance1 = Singleton.getInstance();
        Singleton instance2 = Singleton.getInstance();
        System.out.println(instance1 == instance2); // true
    }
}
​
//饿汉式(静态变量) 
class Singleton {
    //构造器私有化,外部不能new对象 
    private Singleton() {
    }
​
    //内部创建类的对象,此对象也必须声明为static的
    private static Singleton instance = new Singleton();
​
    //提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        return instance;
    }
}
复制代码

构造器私有使得外界无法通过构造器实例化Singleton类,要取得实例只能通过getInstance()方法。

优缺点

优点:类装载时就完成实例化,无需考虑线程同步,本身就是线程安全。

缺点:1.如果从始至终从未使用过这个实例,则会造成内存的浪费 2.对象加载时间过长

2.懒汉式-线程不安全

class Singleton {    
    private Singleton() {       //私有化类的构造器
    }   
    private static Singleton instance=null; 
    //声明当前类对象,没有初始化
    //此对象也必须声明为static的
    
    //声明一个static、public的返回当前类对象的方法
    //当使用到该方法时,才去创建instance,即懒汉式
    public static Singleton getInstance() {
        if (instance == null) {     //如果instance为null,说明还未创建对象,那么就进行实例化 
            instance = new Singleton();     // 创建实例
        }
        return instance;        //不是null,直接return
    }
}
复制代码

优缺点

  1. 懒汉式:起到了懒加载的效果,延迟对象的创建。在需要对象的时候才进行实例化操作。
  2. 在多线程下,线程A进入了if (singleton == null)判断语句,还未来得及往下执行,线程B也进入了这个判断语句,并判定singleton == null,此时线程B创建了一个实例对象;线程A接着执行创建实例的操作,也创建了实例对象,这就导致了出现创建多个实例对象的情况。因此只能在单线程下使用,在多线程环境下由于没有同步措施不可使用这种方式 。

3.懒汉式-线程安全、同步方法

class Singleton {
    private static Singleton instance;
    private Singleton() {
    }
    //提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题,即懒汉式
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
复制代码

优缺点

  1. 使用synchronized关键字来对getInstance()方法做同步处理,解决了多线程并发的线程安全问题。
  2. 带来了效率问题:每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而我们的目的是只创建一个实例,即其实这个方法只执行一次创建实例就够了,也正是这个地方才需要同步;后面的想获得该类实例, singleton非空就会直接return就行了,而不用每次都在同步代码块中进行非空验证。
  3. 如果getInstance()被多个线程调用,synchronized将导致巨大的性能开销。

4.懒汉式-线程安全、同步代码块

为了解决上面出现的问题只对创建实例处进行同步:

class Singleton{
    private static Singleton singleton;    
    private Singleton(){}
    
    public static Singleton getInstance(){
        if(singleton == null){
            synchronized(Singleton.class){                
                singleton = new Singleton();   //创建实例
            }
        }
        return singleton;
    }
}
复制代码

存在的问题: 不能起到线程同步的作用。多个线程同时执行到条件判断语句时,会创建多个实例。

原因: 问题在于当一个线程创建一个实例之后,singleton就不再为空了,但是后续的线程并没有做第二次非空检查

5.双重检查-线程安全

为了解决上面出现的问题,就是进行二次非空检查,这样就可以保证线程安全了。

为什么要进行第二次判空?

假如现在没有第二次验校,线程A执行到第一次验校那里,它判断到single ==null。此时它的资源被线程B抢占了,B执行程序,进入同步代码块创建对象,然后释放同步锁,此时线程A又拿到了资源也拿到了同步锁,然后执行同步代码块,因为之前线程A它判断到single ==null,因此它会直接创建新的对象。所以就违反了我们设计的最终目的。

这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null), 直接return实例化对象,也避免的反复进行方法同步.

public class DoubleCheckedLocking {                     
    private static Instance instance;                   
    public  static Instance getInstance(){              
        if(instance ==null) {                           //第一次检查
            synchronized (DoubleCheckedLocking.class) { //加锁
                if (instance == null)                   //第二次检查
                    instance = new Instance();          //创建实例
            }                                           
        }                                              
        return instance;                                
    }                                                   
}
复制代码

如果第一次检查instance不为null,那就不需要执行下面的加锁和创建实例的操作。因此,可以大幅降低synchronized带来的性能开销。

这样似乎很完美,但这是一个错误的优化!在线程执行到第4行,代码读取到instance不为null时,instance引用的对象可能还没有完成初始化。

存在的问题

创建实例,这一行代码可以分解为如下的3行伪代码。

memory=allocate();        //1:内存分配:分配对象的内存空间
ctorInstance(memory);     //2:初始化对象
instance = memory;        //3:返回对象引用:设置instance指向刚分配的内存地址
复制代码

JVM可能会对指令进行重排序。2和3重排序之后的执行时序如下。

memory=allocate();        //1:分配对象的内存空间
instance = memory;          //3:设置instance指向刚分配的内存地址
                            //注意,此时对象还没有被初始化!
ctorInstance(memory);     //2:初始化对象
复制代码

image.png

如果发生重排序,对应到创建实例就是singleton已经不是null,而是指向了堆上的一个对象,但是该对象却还没有完成初始化动作。当线程B发现singleton不是null而直接使用这个对象时,就会出现这个对象可能还没有被A线程初始化的问题。

在知晓了问题发生的根源之后,我们可以通过不允许2和3重排序来实现线程安全的延迟初始化。

使用volatile禁止重排序

把instance声明为volatile型,就可以实现线程安全的延迟初始化

class Singleton {
    private static volatile Singleton instance;
    private Singleton() {
    }
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();     //instance为volatile,现在没问题了
                }
            }
        }
        return instance;
    }
}
复制代码

当声明对象的引用为volatile后,之前的3行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止。上面的示例代码江安如下的时序执行。

image.png
这个方案是通过禁止上图2和3之间的重排序,来保证线程安全的延迟初始化。

解决方案: JDK1.5之后,可以使用volatile关键字修饰变量来解决无序写入产生的问题,因为volatile关键字的一个重要作用是禁止指令重排序,即保证不会出现内存分配、返回对象引用、初始化这样的顺序,从而使得双重检测真正发挥作用

6.静态内部类

基于类初始化的解决方案

JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

基于这个特性可以实现另一种线程安全的延迟初始化方案。

public class InstanceFactory{
    private static class InstanceHolder{
        public static Instance instance = new Instance();
    }
​
    public static Instance getInstance(){
        return InstanceHolder.instance;         //这里将导致InstanceHolder类被初始化
    }
}
复制代码

假设两个线程并发执行getInstance()方法,下面是执行示意图。

image.png
两个线程并发执行的示意图

这个方案的实质是:允许之前的3行伪代码中的2和3重排序,但不允许非构造线程(这里指线程B)“看到”这个重排序。

总结

通过对比基于volatile的双重检查锁定的方案和基于类初始化的方案,我们会发现基于类初始化的方案的实现代码更简洁。但基于volatile的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。

字段延迟初始化降低了初始化类或创建实例的开销,但增加了访问被延迟初始化的字段的开销。在大多数时候,正常的初始化要优于延迟初始化。如果确实需要对实例字段使用线程安全的延迟初始化,请使用上面介绍的基于volatile的延迟初始化方案;如果确实需要对静态字段使用线程安全的延迟初始化,请使用基于类初始化的方案。

不用手写,了解其它的实现方式

使用静态内部类为什么能够实现延迟加载?

// 静态内部类完成, 推荐使用
class Singleton {
    private static volatile Singleton instance;
    //构造器私有化private Singleton() {}
    //写一个静态内部类,该类中有一个静态属性 Singleton 
    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }
    //提供一个静态的公有方法,直接返回 SingletonInstance.INSTANCE 
    public static synchronized Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}
复制代码

优缺点:

  1. 这种方式采用了类装载的机制来保证初始化实例时只有一个线程。
  2. 静态内部类方式在 Singleton 类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance 方法,才会装载 SingletonInstance 类,从而完成 Singleton 的实例化。
  3. 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM 帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
  4. 优点:避免了线程不安全,利用静态内部类特点实现延迟加载,效率高
  5. 结论:推荐使用.

7.枚举

enum Singleton {
    INSTANCE;
    //属性
    public void sayOK() {
        System.out.println("ok~");
    }
}
复制代码

这借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享