源码学习之设计模式(单例模式)

众所周知,单例模式分为饿汉式和懒汉式,昨天在看了《spring5核心原理与30个类手写实战》以后才知道饿汉式有不少种写法,分别适用于不一样场景,避免反射,线程不安全问题。下面就各类场景、采用的方式及其优缺点介绍。java

饿汉式 (绝对的线程安全)

代码示例

1.第一种写法 ( 定义即初始化)spring

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

  1. 第二种写法 (静态代码块)
public class Singleton{
    private static final Singleton instance = null;
    static {
        instance = new Singleton();
    }
    private Singleton() {}
    public static Singleton getInstance() {
        return instance;
    }
}复制代码

饿汉式基本上就这两种写法。在spring框架中IoC的ApplicantsContext 就是使用的饿汉式单例,保证了全局只有一个ApplicationContext ,在应用启动后就能获取实例,以便于进行接下来的操做.编程

优势

因其在程序启动后就已经初始化,也不须要任何锁保证线程安全 ,因此执行效率高复制代码
缺点

由于在程序启动后就已经进行了初始化,即使是不用也进行了初始化,因此不管什么时候都占用内存空间,浪费了内存空间。

复制代码

懒汉式 (线程安全须要另外的操做)

代码示例

  1. 第一种写法
public class Singleton{
    private static final Singleton instance = null;
 
    private Singleton() {}
    public static Singleton getInstance() {
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }

}复制代码

上面的代码不难看出,在单线程下执行是没有问题的,但在多线程状况下,线程执行速度和顺序没法控制肯定,故有可能会产生多个实例对象,这样就违背了单例模式的初衷了。安全

  1. 第二种写法

加锁保证线程安全(synchronized 关键字)多线程

public class Singleton{
    private static final Singleton instance = null;
 
    private Singleton() {}
    public synchronized static Singleton getInstance() {
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }

}复制代码

​ 能够看到在getInstance()上加了synchronized 关键字,就能保证线程同步。但又有一个问题:使用synchronized关键字是,当一个线程调用获取实例的方法时,会锁住整个类,其余的线程再调用,会使线程状态由 RUNNING 变成 MONITOR ,进而致使线程阻塞,执行效率降低;知道这个线程执行完实例方法,其余线程才能继续执行,两个线程时,效率降低还在能够接受范围内,但在实际应用场景中,使用线程池来管理线程的调度,会有大量的线程,若是这些线程都阻塞了,其结果能够预见。并发

上述问题有什么更好的问题解决呢?使用双重检查锁机制能够完美的解决这个问题。其代码以下复制代码
public class Singleton{
    private volatile  static final Singleton instance = null;
 
    private Singleton() {}
    public  static Singleton getInstance() {
        if(instance == null){
            synchronized(Singleton.class){
                if (instance == null) {
                    instance = new Singleton();
                }
            }
            
        }
        return instance;
    }

}复制代码

这里须要解释下,童鞋们都知道一个对象使用要经历一下步骤:app

  • 为对象分配内存
  • 初始化对象
  • 实例对象指向第一布分配的内存地址
在java中JVM为了提升执行效率,会进行指令重排。那什么时指令重排呢?**指令重排**是指JVM为了优化指令,提升程序的运行效率,在不影响单线程执行结果的状况下,进行指令重排序,以期提升并行度。复制代码

有上述能够指令重排在单线程状况下,对程序的执行不会产生影响,但在多线程状况下就不必定了。因此上述过程的执行顺序可能发生变化,进而致使程序并不会按照预想的执行。框架

为解决上述问题以及保证并发编程的正确性,java中定义了 **happens-before**原则。在 《JSR-133:Java Memory Model and Thread Specification》 书中关于happens-before定义是这样的:复制代码

1.若是一个操做happens-before另外一个操做,那么第一个操做的执行结果将对第二个操做可见,并且第一个操做的执行顺序排在第二个操做以前。ide

2.两个操做之间存在happens-before关系,并不意味着Java平台的具体实现必需要按照happens-before关系指定的顺序来执行。若是重排序以后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法。性能

在Java中 为 避免指令重排出现,引入了volatile 关键字。正如你所看到那样在实例对象前就能保证执行结果的正确性。当一个线程调用` getInstance()` 方法时,执行到synchronized关键字时就会上锁,其余线程也调用时就会发生阻塞,固然这种阻塞不是锁住整个类,而是仅仅锁住了方法。如过方法中的逻辑不是太复杂的话,对于外界来讲是感知不到的。复制代码

​ 这种方法终归仍是要加锁的,只要加锁就会对程序性能产生影响。有什么解决办法能够实现不加锁,又能保证线程安全呢?

内部类:是指 一个类定义在另外一个类里面或者一个方法里面 的类。有如下特色:

  • 隐藏机制:内部封装性好,即使是同一个包下的类也不能直接访问
  • 内部类能够访问外围类的私有数据
  • 内部类对象能够不依赖外部实例被实例化

静态内部类:顾名思义 就是在内部类上加个static关键字 ,其特色有:

  • 能够访问外部类静态成员
  • 能够定义静态成员,非静态内部类不能够

静态内部类在载入Java的时候默认不加载,只有调用时进行加载。根据此特色双锁检查机制的单例模式能够改进使用静态内部类。

  1. 使用静态内部类

代码示例

public class Singleton{
    private Singleton() {}
    public  static Singleton getInstance() {
    
        return SingletonIner.instance;
    }
    //static是为了单例内存共享,保证这个方法不会被重写,重载
    private static class SingletonIner{
        private static Singleton instance = new Singleton();
    }

}复制代码

上述方法及解决了饿汉式的内存浪费问题,又解决了懒汉式的锁的性能问题。

进一步思考

反射破坏单例

你们都知道在Java的各个框架中由于要实现某种功能,不可避免的使用到反射。反射有破坏封装性和性能低下的问题。在这里不考虑性能,只考虑封装性被破坏的问题。调用者使用反射,破坏了封装性,进而使实例有可能不止一个,这样就违背了使用单例模式的初衷。

如何解决呢?很简单,就是在建立另外的对象抛出异常,警告调用者,使其按照咱们预想的方式进行调用。

代码示例

public class Singleton{
    private Singleton() {
        if(SingletonIner.instance!=null){
            throw new RuntimeException("不容许建立多个实例");
        }
    }
    public  static Singleton getInstance() {
    
        return SingletonIner.instance;
    }
    private static class SingletonIner{
        private static Singleton instance = new Singleton();
    }

}复制代码

上面代码可使调用者按照咱们的想法使用。

序列化破坏单例

在实际应用中,为保存对象到磁盘或其余的存储介质,不可避免的要使用序列化。一个单例建立好以后,将其序列化保存在磁盘上,下次使用时在反序列化取出放到内存中使用。反序列化后的对象会从新分配内存,即从新建立,这样就违反了单例模式的初衷。以使用静态内部类的代码为咱们单例模式类,下面进行简单测试。

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Main{
    public static void main(String[] args) {
        Singleton s1=null;
        Singleton s2 = Singleton.getInstance();

        FileOutputStream fos = null;
        try {
            fos=new FileOutputStream("singleton.obj");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(s2);
            oos.flush();
            oos.close();

            FileInputStream fis =new FileInputStream("singleton.obj");
            ObjectInputStream ois = new ObjectInputStream(fis);
            s1 = (Singleton)ois.readObject();
            ois.close();
            System.out.println(s1==s2)

        }
        catch(Exception e){
                e.printStackTrace();
        }
    }
}复制代码

上面代码运行后发现,输出居然时false,这就说明反序列化后和序列话前的对象不是同一个,实例化了两次,根本不符合单例模式的原则。

如何改进呢? 改进 的方法也很简单就是增长readResolve() 方法就能够。下面看代码

import java.io.Serializable;

public class Singleton implements Serializable{
    private Singleton() {
        if(SingletonIner.instance!=null){
            throw new RuntimeException("不容许建立多个实例");
        }
    }
    public  static Singleton getInstance() {
    
        return SingletonIner.instance;
    }
    private static class SingletonIner{
        private static Singleton instance = new Singleton();
    }
    private Object readResolve() {
        return SingletonIner.instance;
    }

}复制代码

深究一下,为何会这样呢?下面咱们来看看ObjectInputStream 里的readObject() 方法一探究竟。代码以下:

/**
     * Read an object from the ObjectInputStream.  The class of the object, the
     * signature of the class, and the values of the non-transient and
     * non-static fields of the class and all of its supertypes are read.
     * Default deserializing for a class can be overridden using the writeObject
     * and readObject methods.  Objects referenced by this object are read
     * transitively so that a complete equivalent graph of objects is
     * reconstructed by readObject.
     *
     * <p>The root object is completely restored when all of its fields and the
     * objects it references are completely restored.  At this point the object
     * validation callbacks are executed in order based on their registered
     * priorities. The callbacks are registered by objects (in the readObject
     * special methods) as they are individually restored.
     *
     * <p>Exceptions are thrown for problems with the InputStream and for
     * classes that should not be deserialized.  All exceptions are fatal to
     * the InputStream and leave it in an indeterminate state; it is up to the
     * caller to ignore or recover the stream state.
     *
     * @throws  ClassNotFoundException Class of a serialized object cannot be
     *          found.
     * @throws  InvalidClassException Something is wrong with a class used by
     *          serialization.
     * @throws  StreamCorruptedException Control information in the
     *          stream is inconsistent.
     * @throws  OptionalDataException Primitive data was found in the
     *          stream instead of objects.
     * @throws  IOException Any of the usual Input/Output related exceptions.
     */
    public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }复制代码

根据注释,咱们知道readObject() 方法读取一个对象的类,类的签名以及该类机器全部超类的非瞬时和非静态的值。咱们看到在try后面又调用了重写的readObject0() 方法,其代码以下:

/**
     * Underlying readObject implementation.
     */
    private Object readObject0(boolean unshared) throws IOException {
               .......

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));
             .......
           
    }复制代码

因篇幅的问题我省略了不重要的代码。

由上面看到,在TC_OBJECT处又调用了`readOrdinaryObject()` 方法,其源码以下:

/**
     * Reads and returns "ordinary" (i.e., not a String, Class,
     * ObjectStreamClass, array, or enum constant) object, or null if object's
     * class is unresolvable (in which case a ClassNotFoundException will be
     * associated with object's handle).  Sets passHandle to object's assigned
     * handle.
     */
    private Object readOrdinaryObject(boolean unshared)
        throws IOException
    {
        if (bin.readByte() != TC_OBJECT) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        desc.checkDeserialize();

        Class<?> cl = desc.forClass();
        if (cl == String.class || cl == Class.class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }

        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }

        passHandle = handles.assign(unshared ? unsharedMarker : obj);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(passHandle, resolveEx);
        }

        if (desc.isExternalizable()) {
            readExternalData((Externalizable) obj, desc);
        } else {
            readSerialData(obj, desc);
        }

        handles.finish(passHandle);

        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())
        {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                if (rep != null) {
                    if (rep.getClass().isArray()) {
                        filterCheck(rep.getClass(), Array.getLength(rep));
                    } else {
                        filterCheck(rep.getClass(), -1);
                    }
                }
                handles.setObject(passHandle, obj = rep);
            }
        }

        return obj;
    }复制代码

由上述代码可知,由调用了ObjectStreamClassisInstanctiable() 方法,方法体很是简单,源码以下 :

/**
     * Returns true if represented class is serializable/externalizable and can
     * be instantiated by the serialization runtime--i.e., if it is
     * externalizable and defines a public no-arg constructor, or if it is
     * non-externalizable and its first non-serializable superclass defines an
     * accessible no-arg constructor.  Otherwise, returns false.
     */
    boolean isInstantiable() {
        requireInitialized();
        return (cons != null);
    }复制代码

其做用就是构造方法是否为空,构造方法不为空就返回true。这意味着只要时无参构造方法就会实例化。

再回去看 readOrdinaryObject() 的源码。先是判断readResloveMethod 是否为空,经过全局查找可知在私有方法ObjectStreamClass() 给其赋值,赋值代码以下:

readResolveMethod  = gerInheritableMethod(c1,"readResolve",null,Object.class);复制代码

以后上述的逻辑找到一个 readResolve() 方法若是存在就调用 invokeReadResolve() 方法,其代码以下:

/**
     * Invokes the readResolve method of the represented serializable class and
     * returns the result.  Throws UnsupportedOperationException if this class
     * descriptor is not associated with a class, or if the class is
     * non-serializable or does not define readResolve.
     */
    Object invokeReadResolve(Object obj)
        throws IOException, UnsupportedOperationException
    {
        requireInitialized();
        if (readResolveMethod != null) {
            try {
                return readResolveMethod.invoke(obj, (Object[]) null);
            } catch (InvocationTargetException ex) {
                Throwable th = ex.getTargetException();
                if (th instanceof ObjectStreamException) {
                    throw (ObjectStreamException) th;
                } else {
                    throwMiscException(th);
                    throw new InternalError(th);  // never reached
                }
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }复制代码

invokeReadResource() 方法又使用反射调用 readResolveMethod() ,进而执行readResolve() 方法。

经过分析源码能够看出,readResolve() 方法虽然解决了单例模式被破坏的问题,可是其实例化两次,只不过新建立的对象被覆盖了而已 。若是建立的对象动做发生加快,就意味着内存开销也随之增大。这个问题如何解决呢?使用注册式单例便可完美解决上诉问题。

注册式单例

  1. 枚举式单例
代码示例

public enum EnumSingleton{
    INSTANCE;
    private Object data;
    

    /**
     * @return Object return the data
     */
    public Object getData() {
        return data;
    }

    /**
     * @param data the data to set
     */
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}复制代码

通过反编译分析源码可知枚举式单例是在静态代码块中为INSTANCE赋值,使饿汉式单例的体现。

那么序列化和反序列化可否破坏吗枚举式单例呢? 答案是不能。同查看源码可知枚举类型是经过类名和对象名找到全局惟一的对象。因此,枚举对象不可能加载屡次。

那么反射呢?答案也是不能。在程序运行时会报java.lang.NoSuchMethodException 异常,其意思为没有找到无参的构造方法。查看java.lang.Enum 源码可知枚举类型只有一个protect 构造方法。通过测试,使用反射直接实例化枚举对象时会出现 Cannot reflectively create objects 查看Constructor newInstsnce() 方法可知,在方法体作了判断,若是是枚举类型则直接抛出异常。

看到这个词,有的小伙伴的内心就想什么是容器式单例。容器式单例就是在单例类中维护一个相似与Map的容器,这种方式在Spring中是很是常见的,众所周知,Spring的Bean是全局单例的;Spring在内部维护着一个Map结构。在 org.springframework.beans.factory.support 包下 SimpleBeanDefinitionRegistry 为咱们完美的解释容器式单例,其源码以下:

public class SimpleBeanDefinitionRegistry extends SimpleAliasRegistry implements BeanDefinitionRegistry {

    /** Map of bean definition objects, keyed by bean name. */
    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<>(64);


    @Override
    public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
            throws BeanDefinitionStoreException {

        Assert.hasText(beanName, "'beanName' must not be empty");
        Assert.notNull(beanDefinition, "BeanDefinition must not be null");
        this.beanDefinitionMap.put(beanName, beanDefinition);
    }

    @Override
    public void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
        if (this.beanDefinitionMap.remove(beanName) == null) {
            throw new NoSuchBeanDefinitionException(beanName);
        }
    }

    @Override
    public BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException {
        BeanDefinition bd = this.beanDefinitionMap.get(beanName);
        if (bd == null) {
            throw new NoSuchBeanDefinitionException(beanName);
        }
        return bd;
    }

    @Override
    public boolean containsBeanDefinition(String beanName) {
        return this.beanDefinitionMap.containsKey(beanName);
    }

    @Override
    public String[] getBeanDefinitionNames() {
        return StringUtils.toStringArray(this.beanDefinitionMap.keySet());
    }

    @Override
    public int getBeanDefinitionCount() {
        return this.beanDefinitionMap.size();
    }

    @Override
    public boolean isBeanNameInUse(String beanName) {
        return isAlias(beanName) || containsBeanDefinition(beanName);
    }

}复制代码

其中BeanDefinition 是一个接口,储存着各个单例对象的信息,由其实现类实现。对象名做为Map的Key,BeanDefinition 做为Map的值,维护着这个map 保证每一个对象全局单例.

由于Spring比较复杂,讨论暂告一段落。下面会到咱们的主题,那咱们的 singleton 类如何实现容器式单例呢。下面看代码:

import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.HashMap;

public class Singleton {
    private static Map <String,Object > ioc =new ConcurrentHashMap();
    private Singleton() {}
    public  static Object getInstance(String name) {
     synchronized(ioc) {
         if (!ioc.containsKey(name)){
             Object o=null;
             try {
                 o=Class.forName(name).newInstance();
                 ioc.put(name, o);
                
             }catch(Exception e) {
                 e.printStackTrace();
             }
             return o;
         }
         else {
             return ioc.get(name);
         }
     }
    }
    

}复制代码

容器式单例适用于单例实例对象比较多的状况下,方便管理。值得注意的是,他是线程不安全的。

注册式单例就包括上面两种形式,每一个都有不一样的应用场景以及特色,要根据实际状况灵活选择。

下面我来介绍一种特殊的单例模式-----拥有 ThreadLocal 单例模式。

扩展

ThreadLocal 与单例模式

话很少说,直接看代码。

public class Singleton {
    private static final ThreadLocal<Singleton> instance = new ThreadLocal<> (){
        @Override
        protected Singleton initialValue() {
            return new Singleton();
        }
    };
    private Singleton() {}
    public  static Object getInstance() {
        return instance.get();
    }
    

}复制代码

为何说他特殊呢?由于加了 ThreadLocal 关键字的单例类是线程内单例的,单线程共享不是单例的。你们能够测试下,使用下面的测试代码。

public class Main{
    public static void main(String[] args) {
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());


        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(Singleton.getInstance());
            }
        } ;

        Thread t1 = new Thread(r);
        Thread t2 = new Thread(r);
        t1.start();
        t2.start();
        System.out.println("end");
    }
}复制代码

执行结果以下:

image-20191216180630142

测试后发现主线程不管执行多少次,获取的实例都是同一个,而两个子线程却得到了不一样的实例。

声明

本文章为做者原创,其中参考了《spring5核心原理与30个类手写实战》以及互联网上的内容。如要转载请注明来源。若有错误,请评论或者私聊我,欢迎探讨技术问题

相关文章
相关标签/搜索