二十三种设计模式之单例模式

单例模式 - Singleton

目的:保证一个类只有一个实例,并提供一个访问它的全局访问点(无论在系统中哪个地方调用它,都是同一个实例对象)

1.单例模式是如何实现单例功能的?

根据饱汉、饿汉、双锁模式的书写方式,主要是两个重点

  1. static修饰全局变量Singleton:实现类加载时创建变量Singleton,谁都可以引用Singleton该类。

  2. 私有构造器private Singleton(){}:杜绝每次引用都构造一个新的对象,不允许使用new来创建对象。

2.双锁机制中volatile的作用

在java内存模型中,volatile关键字作用可以是保证可见性或者禁止指令重排。这里是因为singleton = new Singleton(),它并非是一个原子操作,事实上,在JVM只不过上述语句至少做了一下三件事

  1. 给singleton分配内存空间;

  2. 开始调用singleton的构造函数等来初始化singleton;

  3. 将singleton对象指向分配的内存空间(执行完这步singleton就不是null了)。

这里要注意123的顺讯,因为存在指令重排序的优化,也就是收23的顺序是不能保证的,最终的执行顺序,可能是1-2-3,也有可能是1-3-2.

如果是132,那么在3执行完以后,singleton就不是null了,可是这时2并没有执行,singleton对象未完成初始化,他的属性的值可能不是我们所预期的值。假设此时2进入getInstance方法,由于singleton已经不是null了,所以会通过第一重检查并直接返回,但其实这时的singleton并没有完成初始化,所以使用这个实例的时候会报错。

3.为什么饿汉模式-不加锁的情况下是非线程安全的?

计算机CPU的使用的分时间段使用的,当如下情况:

public static Singleton getInstance(){
    if(s1 == null){
        s1 = new Singleton();
    }
    return s1;
}
  • 当线程a运行到第二行判断s1 == null时,此时没有Singleton对象,判断结束,线程a的时间用完了,开始跑线程b。

  • 当线程b运行到第二行判断s1 == null,此时也没有Singleton对象,且线程b还有时间,执行了第三行创建了一个Singleton对象。当线程b时间用完了,又开始跑线程a。

  • 此时线程a已经执行了第二行,且判断结果为:没有Singleton对象,故线程a又执行第三行创建了一个Singleton对象。

所以,多线程的情况下,可能生成两个对象。

4.生成的单例对象,如果一直没有被引用,会被GC清除吗?

垃圾收集算法使用根搜索算法。这个算法的基本思想是:对任何活的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。通过一系列名为根的引用作为起点,从这些根开始搜索,经过一系列的路径,如果可以达到java堆中的对象,那么这个对象就是活的,是不可回收的。可以作为根的对象有:

  • 虚拟机栈中引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中的常量引用的对象。
  • 本地方法栈中JNI的引用的对象。

方法区是JVM的一块内存区域,用来存放类相关的信息。很明显,java中单例模式创建的对象被自己类中的静态属性所引用,符合第二条,因此,单例对象不会被jvm垃圾收集。

单例类本身如果长时间不用会不会被回收?如果单例类被收集,那么堆中的对象就会失去到根的路径,必然会被垃圾收集掉,jvm卸载类的判定条件如下:

  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

只有三个条件都满足,jvm才会在垃圾收集的时候卸载类。显然,单例的类不满足条件一,所以不会被回收。根据搜索算法,对象是否被垃圾收集与未被使用时间长短无关,仅仅在于这个对象是不是活的。

综上所述:堆中存在对象,且该对象是方法区中的类静态属性引用的对象。那么堆中的对象不会被回收。

六种单例模式的书写

1.懒汉模式 - 线程不安全

描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁synchronized,所以严格意义上他并不算单例模式。

//懒汉模式--线程不安全
public class Singleton {
    private static Singleton singleton;

    private Singleton() {
    }

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

2.懒汉模式-线程安全

这种范式具备很好的lazy loading,能够在多线程中很好的工作,但是,效率很低,99%情况下不需要同步。

优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁synchronized才能保证单例,但加锁会影响效率。
getInstance的性能对应用程序不是很关键。

//懒汉模式-线程安全
public class Singleton {
    private static Singleton singleton;

    private Singleton() {
    }

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

3.饿汉模式

这种方式比较常用,但容易产生垃圾对象。优点是没有加锁,执行效率快。

它基于classloader机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调getInstance方法,但是也不能确定有其他的方式导致类装载,这时候初始instance显然没有达到lazy loading的效果。

//饿汉模式
public class Singleton {
    private static Singleton singleton = new Singleton();

    private Singleton() {
    }

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

4.双锁模式 DCL double check lock

这种模式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance的性能对应用程序很关键

//双锁模式
public class Singleton {
    private volatile static Singleton singleton;

    private Singleton() {
    }

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

5.登记式/静态内部类

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

这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

//登记式/静态内部类
public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    private Singleton() {
    }

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

6.枚举模式

这种方式是实现单例模式的最佳方法,它更简洁,自动支持序列化机制,绝对防止对此实例化。

这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。

//枚举模式
public enum Singleton {
    INSTANCE;

    public void getMessage() {
        System.out.println("Hello Word 6 !");
    }
}

直接Singleton.INSTANCE获得单例

注意!此信息未认证,请谨慎判断信息的真实性!

全部评论
空

相关内容推荐

头像
点赞 评论 收藏
转发
头像
点赞 评论 收藏
转发
头像
点赞 评论 收藏
转发
头像
点赞 评论 收藏
转发
头像
点赞 评论 收藏
转发
头像
点赞 评论 收藏
转发
头像
点赞 评论 收藏
转发
头像
点赞 评论 收藏
转发
头像 头像
点赞 评论 收藏
转发
头像
点赞 评论 收藏
转发
头像
点赞 评论 收藏
转发
头像
点赞 评论 收藏
转发
点赞 评论 收藏
转发
头像
点赞 评论 收藏
转发
头像 头像
点赞 评论 收藏
转发
头像
2022-12-22 16:38
江苏大学_2023
点赞 评论 收藏
转发
头像
点赞 评论 收藏
转发
点赞 评论 收藏
转发
点赞 收藏 评论
分享

全站热榜

正在热议