在Java面试,单例模式是面试官经常问到的一个知识点。
- 单例模式类型
- 不同类型有什么特点
- 手写多个单例模式
一、单例模式类型
- 懒汉式单例
- 1、适用于单线程环境(不推荐)
- 2、适用于多线程环境,但效率不高(不推荐)
- 3、双重检验锁
- 4、静态内部类方式(推荐)
- 饿汉式单例
- 1、饿汉式(推荐)
- 2、枚举方式(推荐)
1、懒汉式 单线程环境
private static SingleInstance instance;
private SingleInstance(){
}
public static SingleInstance getInstance(){
if(instance ==null){
instance = new SingleInstance();
}
return instance;
}
此方式在单线程的时候工作正常,但在多线程的情况下就有问题了。如果两个线程同时运行到判断instance是否为null的if语句,并且instance的确没有被创建时,那么两个线程都会创建一个实例,
2、懒汉式 多线程环境,但效率不高
private static SingleInstance instance;
private static synchronized SingleInstance getInstance(){
if(instance ==null){
instance = new SingleInstance();
}
return instance;
}
在多线程环境下我们还是只能得到该类的一个实例,只需要在getInstance()方法加上同步关键字sychronized,就可以了。但每次调用getInstance()方法时都被synchronized关键字锁住了,会引起线程阻塞,影响程序的性能。
3、双重检验锁
// static volatile 注意
private static volatile SingleInstance instance;
private SingleInstance(){
}
public static SingleInstance getInstance(){
if(instance ==null){ // 同volatile 防止
// 锁住一个类
synchronized (SingleInstance.class){
if(instance ==null){
instance =new SingleInstance();
}
}
}
return instance;
}
在多线程环境下,不影响程序的性能,不让线程每次调用getInstance()方法时都加锁,而只是在实例未被创建时再加锁,在加锁处理里面还需要判断一次实例是否已存在。
instance =new SingleInstance(); 它并不是一个原子操作。事实上,它可以”抽象“为下面几条JVM指令
memory = allocate(); //1:分配对象的内存空间
initInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM可以以“优化”为目的对它们进行 重排序,经过重排序后如下:
memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址
//(此时对象还未初始化)
ctorInstance(memory); //2:初始化对象
可以看到指令重排之后,操作 3 排在了操作 2 之前,即引用instance指向内存memory时,这段崭新的内存还没有初始化——即,引用instance指向了一个"被部分初始化的对象"。此时,如果另一个线程调用getInstance方法,由于instance已经指向了一块内存空间,从而if条件判为false,方法返回instance引用,用户得到了没有完成初始化的“半个”单例。
解决这个该问题,只需要将instance声明为volatile变量:
private static volatile Singleton instance;
4、静态内部类方式
// 二、 静态内部类方式
private SingleInstance(){
}
private static class HoldInstance{
private final static SingleInstance instance = new SingleInstance();
}
public static SingleInstance getInstance(){
return HoldInstance.instance;
}
加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。 由于在调用 StaticSingleton.getInstance() 的时候,才会对单例进行初始化,而且通过反射,是不能从外部类获取内部类的属性的;由于静态内部类的特性,只有在其被第一次引用的时候才会被加载,所以可以保证其线程安全性。
总结:
优势:兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵)。
劣势:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象。
5、饿汉式
// 一、饿汉式 线程安全
private static final SingleInstance instance = new SingleInstance();
private SingleInstance(){
}
public static SingleInstance getInstance(){
return instance;
}
饿汉式单例类:在类初始化时,已经自行实例化。
6、枚举方式(推荐)
public class Singleton {
public static void main(String[] args) {
Single single = Single.SINGLE;
single.print();
}
enum Single {
SINGLE;
private Single() {
}
public void print() {
System.out.println("hello world");
}
}
}
创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。保证只有一个实例(即使使用反射机制也无法多次实例化一个枚举量)。
本文详细介绍了Java中单例模式的六种实现方式,包括懒汉式和饿汉式的不同变体,以及推荐使用的静态内部类和枚举方式。探讨了每种方式的特点和适用场景。
1216

被折叠的 条评论
为什么被折叠?



