Java教程

面试造火箭之单例模式

本文主要是介绍面试造火箭之单例模式,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

原文链接:www.upheart.top/2020/03/23/…

保证一个类只有一个实例,并且提供一个全局访问点

饿汉式

public class Singleton {
    private final static Singleton INSTANCE = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){
        return INSTANCE;
    }
}
复制代码

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。 缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

懒汉式

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
复制代码

这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

懒汉式(线程安全,同步方法)

public class Singleton {
    private static Singleton singleton;
    private Singleton() {}
    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
复制代码

解决上面第三种实现方式的线程不安全问题,做个线程同步就可以了,于是就对getInstance()方法进行了线程同步。 缺点:效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。

懒汉式(同步代码块)

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

这种同步并不能起到线程同步的作用。假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例,线程1获得锁实例化,释放锁,线程2获得锁继续实例化就会有问题

双重检查(Double Checked Lock)

public class Singleton {
    private static volatile Singleton singleton;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
复制代码

但是还是有个小问题的,问题就在singleton= new Singleton();语句上。 这语句在这里看起来是一句代码啊,但实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了3件事情:

  • 1.给Singleton的实例分配内存
  • 2.调用Singleton()的 构造函数,初始化成员字段
  • 3.将singleton对象指向分配的内存空间(此时singleton就不是null了)

编译器(JIT),CPU 有可能对指令进行重排序,上面的第二和第三的顺序是无法保证的。也就是说,执行顺序可能是1-2-3也可能是1-3-2.如果是后者,并且在3执行完毕、2未执行之前,被切换到线程B上,这时候singleton因为已经在线程A内执行3了,instance已经是非null,所有线程B直接取走singleton,再使用时就会出错,这就是DCL失效问题,而且这种难以跟踪难以重现的问题很可能会隐藏很久。

双重检测锁单例

public class Singleton {    
	private static volatile Singleton instance = null;    
	private Singleton() {
    }    
    public static Singleton getInstance() {        
    	if (instance == null) {            
    		synchronized (Singleton.class) {                
    			if (instance == null) {
                    instance  = new Singleton();
                }
            }
        }        
        return instance;
    }
}
复制代码

静态内部类

public class Singleton {
    private Singleton() {}
    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }
}
复制代码

这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。 类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM的类加载器帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的

反射攻击实例

Constructor<InnerClassSingleton> declaredConstructor=InnerClassSingleton.class.getDeclaredConstructor();
declaredConstructor.setAccessible( true );
InnerClassSingleton innerClassSingleton=declaredConstructor.newInstance();
InnerClassSingleton instance=InnerClassSingleton.getInstance();
System.out.println(innerClassSingleton==instance);
//静态内部类防止反射破坏:
private InnerClassSingleton(){
    if (InnerClassHolder.instance!=null){
        throw new RuntimeException( " 单例不允许多个实例 " );
    }
}
复制代码

枚举

public enum Singleton {
    INSTANCE
}
复制代码

构造方法是私有的也只能私有,不允许被外部实例化,符合单例 天然不支持反射创建对应的实例

没有延时初始化,随着类的初始化就初始化了

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

序列化

可以利用 指定方法来替换从反序列化流中的数据 如下

Object readResolve() throws ObjectStreamException;
class InnerClassSingleton implements Serializable{
    static final long serialVersionUID = 42L;
    private static class InnerClassHolder{
        private static InnerClassSingleton instance=new InnerClassSingleton();
    }
    
    private InnerClassSingleton(){
        if (InnerClassHolder.instance!=null){
            throw new RuntimeException( " 单例不允许多个实例 " );
        }
    }
    public static InnerClassSingleton getInstance(){
        return InnerClassHolder.instance;
    }
    Object readResolve() throws ObjectStreamException{
        return InnerClassHolder.instance;//保证了序列化和非序列化的实例对象一样
    }
}
复制代码

使用CAS

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。实现单例的方式如下:

public class Singleton {
    private static final AtomicReference INSTANCE = new AtomicReference();
    private Singleton() {}
    public static Singleton getInstance() {
        for (;;) {
        Singleton singleton = INSTANCE.get();
        if (null != singleton) {
                    return singleton;
            }
singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}
复制代码

用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。 CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。

另外,如果N个线程同时执行到singleton = new Singleton();的时候,会有大量对象创建,很可能导致内存溢出。

源码中的应用

// Spring & JDK
java.lang.Runtime
org.springframework.aop.framework.ProxyFactoryBean
org.springframework.beans.factory.support.DefaultSingletonBeanRegistry
org.springframework.core.ReactiveAdapterRegistry
// Tomcat
org.apache.catalina.webresources.TomcatURLStreamHandlerFactory
// 反序列化指定数据源
java.util.Currency
复制代码
这篇关于面试造火箭之单例模式的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!